2011-12-18
Understanding the close() and EINTR situation
Colin Percival's POSIX close(2) is broken
points out this bit in the POSIX specification for close():
If close() is interrupted by a signal that is to be caught, it shall return -1 with errno set to [EINTR] and the state of fildes is unspecified.
(The same thing is true for an IO error during close, so this is really 'if something goes wrong, the state of fildes is unspecified'.)
Colin rightfully considers this crazy, because it means that a
conformant threaded POSIX program has no way of doing anything
sensible in reaction to an EINTR (or EIO) during close(). (A
non-threaded program can retry the close() and accept an EBADF.)
Although it invented some things, POSIX is primarily a documentation standard, one that wrote down a common subset of existing practice for Unix systems. Any time you run into something crazy and unusable like this in a documentation standard, you should immediately assume that what really happened is not that the standard authors were morons but that they found two different systems that had incompatible behavior, neither of which were willing to change. This is likely the case here; part of the Hacker News discussion found the clashing examples of HP-UX (where this does not close fildes) and AIX (where this does close fildes).
(POSIX is not a forced standard, so even if it wanted to pick a winner here it wouldn't have had any real effect. Documenting that in this situation you don't know whether or not fildes is still open is the more honest and useful approach for portable programs.)
A more interesting question is why the behavior of leaving fildes open
after EINTR is ever sensible behavior, and there are two levels to
the answer. The obvious level is that by closing the file descriptor,
you lose any chance of telling the user level program about any actual
errors that happen during the close(), errors that come from trying to
write out data that the kernel has been holding in memory after previous
write() calls. A great story, except that it's not convincing for most
systems.
(Most systems don't make you wait for file data to commit to disk before
returning from close(), so they already let errors happen without
being able to report them. If you want to catch all write errors, you
need to use fsync() first.)
Which brings us to our old friend NFS (or actually remote filesystems in
general) and things like disk space quotas. Suppose that you are very
close to your disk space quota on a remote fileserver and you run a
program that writes enough data to run out of quota and then close()'s
the file descriptor. Because NFS has no 'reserve some space for me'
operation and your local machine buffered the data until close() was
called, you can only get an 'out of disk space' error on the close();
the first the remote fileserver heard of your new data is when your
local machine started sending it writes as you closed the file. Now
suppose that this close() takes long enough that it gets interrupted
with an EINTR. If the file descriptor is now invalid, your program has
no way to find out that the data it thought it had written has in fact
been rejected.
(This issue doesn't come up with local filesystems; with a local
filesystem, the kernel could have at least told the filesystem to
reserve space when you did your write()s so the filesystem could have
immediately reported errors when you ran out of quota space.)
Unlike write errors on close() on local filesystems, which almost
never happen, quota errors on close() on remote filesystems are at
least reasonably possible. There are some environments where you can
even expect them to be reasonably frequent (it shows that I used to run
an undergraduate computing environment). Thus it's at least sensible
for Unix systems to worry about this potential case and decide that
close() should not close the file descriptor in the EINTR case.
(With that said, HP-UX is the odd man out here and I don't know where it got its behavior from.)
2011-12-14
Shell functions versus shell scripts
As part of Sysadvent I recently read Jordan Sissel's Data in the Shell, where one of his examples was a suite of tools for doing things with field-based data (things like summing a field). I approve of this in general, but there's one problem: he wrote his tools as shell functions. My immediate reaction was some approximation of a wince.
I have nothing against shell functions; I even use them in scripts
sometimes, because they can be the best tool for the job. But using
shell functions for tools like this has one big drawback: shell
functions aren't really reusable. Jordan's countby function is neat,
but if he wants to use it in a shell script he's out of luck; he's going
to have to put a copy of the shell function in the shell script. If it
was a shell script, he could have used it interactively just as he did
and he could have reused it in future shell scripts.
Your default should always be to write tools as shell scripts. As nifty as they may be, shell functions are for two special cases; when you need to manipulate the shell's current environment or when you are absolutely sure that what you're writing will only ever be used interactively in your shell and never in a shell script (even a shell script that you wrote purely to record that neat pipeline you put together and may want some day in the future). Frankly, there are very few tools that you will never want to reuse in shell scripts, most especially if the reason you're writing them in the first place is to make pipelines work better.
(Shell scripts are also generally easier to write and debug, since you
can work on them in a full editor, try new versions easily, and run them
under 'sh -x' and similar things. They are also more isolated from
each other.)
By the way, I'll note that I've learned this lesson the hard way. When I started out with my shell I wrote a lot of things as shell functions; over time it's turned out that I want to use many of them as shell scripts for various reasons and so I've quietly added shell script versions of any number of them. If I was clever I would do a systematic overhaul of my remaining shell functions to sort out what I no longer use at all, what should be a shell script, and what needs to remain as a function.
2011-12-11
head versus sed
I was recently reading More shell, less egg and ran across this:
(I can't help wondering, though, why [Doug McIlroy] didn't use
head -${1}in the last line. It seems more natural thansed. Is it possible thatheadhadn't been written yet?)
While head postdates sed, this is probably not it. There is an old
Unix bias against using head to the point that a lot of old Unix
people make a point of avoiding it in favour of sed.
This isn't a technical issue as much as a cultural issue. head was
introduced in 4BSD, and there has always been a reaction against
4BSD that saw it as the point where the core Unix philosophy
started being lost. The introduction of a documented command
that could as well be a shell script (the original version
only limited things by line) was often seen as yet another example of
BSD not really getting the point of things. To put it one way, Unix was
not about having a system command for everything; it was about a kind of
near-mathematical minimalism. Given sed, the basic 4BSD version of
head is not minimal at all.
(tail was different because you can't duplicate it with sed or
anything else feasible; it did a unique job. I suspect that this is part
of why tail appears in V7.)
I'm not quite old enough in my Unix exposure to have been part of this
first hand, but I was apparently exposed to enough old Unix hands at a
sensitive age to have picked this up as a reflexive habit. To this day,
I use 'sed Nq' instead of head. It just feels (more) right.
Sidebar: would McIlroy have had head available?
The short version is 'probably not'. The bit that's being quoted
is from 1986, well after 4BSD introduced head, but McIlroy was
at Bell Labs and was almost certainly using Research Unix. While the version of
Research Unix from that era used a kernel derived from 4BSD, my
impression is that most of the userland programs were basically V7 and
I would be somewhat surprised if the Bell Labs people had picked up
head.