[TUHS] fork

Dan Cross via TUHS tuhs at tuhs.org
Fri May 15 08:32:53 AEST 2026


On Thu, May 14, 2026 at 4:39 PM G. Branden Robinson via TUHS
<tuhs at tuhs.org> wrote:
> At 2026-05-13T07:29:15-0400, Dan Cross via TUHS wrote:
> > Btw, a fun trick to turn a signal into a synchronous event, is what is
> > sometimes called the, "self-pipe" trick. Here, you create a pipe, set
> > the write-end of it to be non-blocking, and in your signal handler,
> > write a byte into the pipe (you make it non-blocking so that you never
> > block in the signal handler, should the pipe fill up).  Then, you can
> > run `select` (or `poll` or whatever) in your "main loop" and add the
> > read-end of the pipe to the read-set you're selecting on; when you
> > receive a signal, that pipe will become readable, you read a byte out
> > of it, and you can do whatever you would have needed to do to handle
> > the signal in a normal context. I think Dan Bernstein was the first
> > person to document the idea.
>
> Forgive the Unfrozen Caveman Engineer question, but...

Not at all!

> How is that method superior to having a `volatile sig_atomic_t` of
> Boolean sense that gets set in the signal handler and checked in the
> main program's event loop?  I fear I may owe some former junior
> colleagues an apology...

To be clear, you _can_ do that. But the programmer may find it
unsatisfactory in several ways.

First, there's the matter of where one checks it, and when. Allow me
to explain by illustration. One might imagine code like this
(untested, excuse errors, for instructional purposes only, etc):

|   volatile sig_atomic_t intr = 0;
|   void handler(int ignored_signo) { intr = 1; }
|   int main(void) {
|       signal(SIGWHATEVER, handler);
|       for (;;) {
|           // Set up all the inputs to `select` here....
|           if (intr!=0) { intr = 0; /* Handle the signal... */ }
|           int err = select(nfds, rfds, wfds, NULL, NULL);
|           if (err==-1&&errno==EINTR) continue;
|           /* If we get here, there was no signal. */
|       }
|       return 0;
|   }

(This is a little more compact that I would ordinarily write it, but
I'm going trying to conserve space; hopefully gmail doesn't strip out
all the whitespace. :-/)

This may look ok: we test for whether a signal occurred before the
`select`, and handle if it so. And if the signal is delivered while
we're in `select`, that's ok: the system call will be interrupted by
its delivery, we'll see that reflected in the combination of
`signal`'s return value and the value of `errno`, and we'll go back to
the top of the loop and repeat the check.

But there's a race: suppose that a signal is delivered immediately
_after_ checking `intr!=0` but _before_ entry to `select`. The kernel
would arrange an upcall to run `handler`, and then resume the
program's normal flow of execution after. The program would then
invoke `select`, where we'd block; we wouldn't check `intr` until the
next iteration of the loop, which may be delayed indefinitely, if
nothing happens that makes `select` return. One may wonder if this is
likely; after all, signals are only delivered via a round trip through
the kernel; but the system might take an interrupt right after the
comparison instruction, but before the branch to to the `select` call,
trapping us on that journey.

One might, perhaps, render this harmless by supplying a timeout to the
`select` (or `poll` or other equivalent) call and check the `intr`
flag again whenever `select` returns, whether due an event,
interruption, or timeout. That works, but how do you choose the
timeout? Too short, and we're burning CPU needlessly; too long, and we
may have unacceptable latency dealing with the signal.

It's worth noting here that POSIX added `pselect`, which takes a
pointer to a signal mask, and (if that is non-null) atomically
replaces the caller's mask with the one given for the duration of the
call. So you _can_ solve this by blocking signal delivery, checking
the flag, and then `pselect`ing with a mask that re-enables that
signal; if `pselect` is interrupted, just go back to the check. That
avoids the race condition: a signal sent between the check and before
the `pselect` will be held pending by the kernel until it's unmasked,
then delivered: that is, it will interrupt the `pselect`, so you end
up with code that looks much like the above. But there's another
complication this does not handle, this time with multithreading.

In a multithreaded POSIX program (already quite common, but only
becoming more so), you don't necessarily know what thread a signal
sent _to a process_ will be delivered on (`pthread_signal` can be used
to send a signal to a specific thread, of course, but many things that
generate signals don't use `pthread_signal`). So a signal may not be
delivered to the thread that is running the event loop. So how do you
know when to check? The issue there, again, is latency: if that thread
is blocked inside of `select` or equivalent, it won't see the flag set
by the signal handler until the next time it comes out of the kernel,
which may be arbitrarily far in the future. Timeouts just come back to
the duration selection problem mentioned before.

The self-pipe trick solves all of these issues, because it composes
naturally with `select` (or `poll`, etc): one simply adds the read-end
of the pipe to the read FD set. If `select` returns with `EINTR` due
to delivery of a signal of interest, you simply re-start it, and it
returns immediately since there will be data available on the read-end
of the pipe, put there by the signal handler. If the signal is
delivered and handled on another thread, you'll still get a timely
notification, because it will unblock the `select` in the main loop.
You don't need a timeout to ensure bounded time for dealing with the
signal (though you may want one for other reasons).

> Is Bernstein's method the preferred idiom in Plan 9, which discarded
> `register`, `const`, _and_ `volatile` from its C compiler?  :-O

I sense jest...but seriously, no, I don't think so.

Plan 9 doesn't have signals, it has notes; you don't send a signal
identified by a number to a process, but rather you issue a `write`
the file, `/proc/<pid>/note`. That's delivered to the process via an
upcall, much like a Unix signal, but the argument is a pointer to the
contents of the `write` (which is guaranteed to be nul-terminated).
The process _has_ to terminate the note handler by invoking the
`noted` system call (or exiting).

Plan 9 also doesn't have `select`, but it does have `rfork`, which
lets programs create multiple processes sharing (almost) the same
address space. Multithreaded programs usually synchronize via shared
memory, and can kill each other if they detect some kind of
exceptional event.

Generally, notes are not as heavily used on Plan 9 as signals are on
Unix/Linux and friends; they're more for killing a program or kicking
it into the `Stopped` state for a debugger, and much less as an _ad
hoc_ IPC mechanism for signaling random events.

I'm unaware of anyone doing anything like the self-pipe trick on Plan
9, though there's no reason you couldn't if you wanted to. But perhaps
Rob or someone else will chime in if with examples, if there are any.

> https://plan9.io/sys/doc/comp.html
>
> (And yeah, I know you know exactly where that document came to my
> attention recently.  ;-) )

:-D

(For those unaware, Branden has done work recently normalizing
behavior across the various strains of `troff`, including the ones
from Plan 9 and Plan9 from Userspace.)

        - Dan C.


More information about the TUHS mailing list