Unix programming can be tricky. There are many subtleties many developers are not aware of. In this post, I will describe just two of them... my favorite Unix quirks, if you will.
Interruptible system calls
On Unix, any system call which blocks can potentially fail with an errno of
EINTR, which indicates that the caller must retry the system call. The
EINTR error can be raised at any time for any reason, so essentially every I/O operation on a Unix system must be prepared to handle this error properly. Surprisingly to some, this includes the C standard library functions such as
fwrite(), and so on.
For example, if you are writing a network server, then most of the time, you want to ignore the
SIGPIPE signal which is raised when the client closes its end of a socket. However, this ignored signal can cause some pending I/O in the server to return
A commonly-held belief is that setting the
SA_RESTART flag with the
sigaction() system call means that if that signal is delivered, system calls are restarted for you and
EINTR doesn't need to be handled. Unfortunately this is not true. The reason is that certain signals are unmaskable. For instance, on Mac OS X, if your process is blocking reading on standard input, and the user suspends the program by sending it
SIGSTOP (usually by pressing ^Z in the terminal), then upon resumption, your
read() call will immediately fail with
Don't believe me? The Mac OS X
cat program is not actually interrupt-safe, and has this bug. Run
cat with no arguments in a terminal, press ^Z, then type
%1, and you'll get an error from cat!
+ Stopped cat
cat: stdin: Interrupted system call
As far as I'm aware, Factor properly handles interruptible system calls, and has for a while now, thanks to Doug Coleman explaining the issue to me 4 years ago. Not having to deal with crap like this (not to mention being able to write cross-platform code that runs on both Unix and Windows) is one of the advantages of using a high-level language like Factor or Java over C.
Subprocesses inherit semi-random things from the parent process
fork() your process, various things are copied from the parent to the child; environment variables, file descriptors, the ignored signal mask, and so on. Less obvious is the fact that
exec() doesn't reset everything. If shared file descriptors such as stdin and stdout were set to non-blocking in the parent, the child will start with these descriptors non-blocking also, which will most likely break most programs. I've blogged about this problem before.
A similar issue is that if you elect to ignore certain signals with the
SIG_IGN action using
sigaction(), then subprocesses will inherit this behavior. Again, this can break processes. Until yesterday, Factor would ignore
SIGPIPE using this mechanism, and child processes spawned with the
io.launcher vocabulary that expected to receive
SIGPIPE would not work properly. There are various workarounds; you can reset the signal mask before the
exec() call, or you can do what I did in Factor, and ignore the signal by giving it an empty signal handler instead of a