Wandering Thoughts archives

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.)

CloseEINTR written at 00:37:47; Add Comment

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.

ShellScriptsVsFunctions written at 01:43:18; Add Comment

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 than sed. Is it possible that head hadn'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.

HeadVsSed written at 09:05:02; Add Comment


Page tools: See As Normal.
Search:
Login: Password:
Atom Syndication: Recent Pages, Recent Comments.

This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.