On his web page about the self-pipe trick, Dan Bernstein explains a race condition with select() and signals, offers a workaround and concludes that

Of course, the Right Thing would be to have fork() return a file descriptor, not a process ID.

What does he mean by this -- is it something about being able to select() on child processes to handle their state changes instead of having to use a signal handler to get notified of those state changes?

  • Does that article get input and output mixed up, or am I not reading it correct? Commented Jul 20, 2019 at 14:54
  • You can ask for signals to be delivered over pipes. That is what I do. Commented Jul 20, 2019 at 14:55
  • @ctrl-alt-delor, yeah, he seems to use "pipe input/output" a bit oddly, but I think it's clear where he's writing and where reading from a pipe. That text is from 2003, and I'm not sure signalfds and such were a thing back then?
    – ilkkachu
    Commented Jul 21, 2019 at 11:25
  • 5
    Dan knows what he's talking about, although he can be a bit deliberately provocative. If I were being deliberately provocative, I'd opine that Of course, the Right Thing would be to get rid of SIGCHLD. Commented Jul 21, 2019 at 12:20
  • 1
    @mosvy I'm exaggerating slightly, but every program and every programmer I've ever seen who has tried to use SIGCHLD has had problems with it. It's a race condition waiting to happen. Back when all we had was blocking wait(), there were things you couldn't do, so someone invented SIGCHLD, but it was a bad job. In my experience, and now that they exist, sprinkling nice, nonblocking wait3(), wait4(), and/or waitpid() calls at key places (perhaps your main event loop) is a vastly superior alternative. Commented Jul 22, 2019 at 0:46

4 Answers 4


The problem is described there in your source, select() should be interrupted by signals like SIGCHLD, but in some cases it doesn't work that well. So the workaround is to have signal write to a pipe, which is then watched by select(). Watching file descriptors is what select() is for, so that works around the problem.

The workaround essentially turns the signal event into a file descriptor event. If fork() just returned an fd in the first place, the workaround would not be required, as that fd could then presumably be used directly with select().

So yes, your description in the last paragraph seems right to me.

Another reason that an fd (or some other kind of a kernel handle) would be better than a plain process id number, is that PIDs can get reused after the process dies. That can be a problem in some cases when sending signals to processes, it might not be possible to know for sure that the process is the one you think it is, and not another one reusing the same PID. (Though I think this shouldn't be a problem when sending signals to a child process, since the parent has to run wait() on the child for its PID to be released.)

  • That said, I don't recall exactly the cases I've read about PID reuse being a problem, so if anyone wants to elaborate or clarify on that, or even edit the above, do feel free to do so.
    – ilkkachu
    Commented Jul 20, 2019 at 14:15
  • 2
    As you already stated, there's no excuse for a parent finding its own child pid has been reused. It is in full control of that situation because it is the one who calls wait().
    – Joshua
    Commented Jul 21, 2019 at 0:45
  • These are called zombie processes: "a process that has completed execution but still has an entry in the process table: it is a process in the "Terminated state". This occurs for child processes, where the entry is still needed to allow the parent process to read its child's exit status"
    – Lassi
    Commented Jul 21, 2019 at 6:59
  • 7
    It's worth mentioning that now Linux can return a file descriptor (pidfd) from clone, which is the actual system call that fork invokes on LInux. The flag to enable this is called CLONE_PIDFD - See for example lwn.net/Articles/784831. Commented Jul 21, 2019 at 7:44
  • 1
    @Lie Ryan, Re "Having fork() returns a global handle rather than a local handle is conceptually more correct because", Windows uses process handles. The pid and exit code stick around until all the handles to the process are closed (rather than waiting for parent to reap), avoiding race conditions common in unix systems. When the handles keep the process alive, it makes far more sense fo them to be local handles rather than global ones.
    – ikegami
    Commented Jul 21, 2019 at 21:40

Bernstein doesn't give much context for this "Right Thing" remark, but I'll hazard a guess: having fork(2) return a PID is inconsistent with open(2), creat(2) etc returning file descriptors. The rest of the Unix system could have done process manipulation with a file descriptor representing a process, instead of a PID. A system call signalfd(2) exists, which allows a somewhat better interaction between signals and file descriptors, and shows that a file-descriptor-representing a process could work out.

  • signalfd(2) looks awesome, thanks for mentioning it! Too bad it's Linux-only.
    – Lassi
    Commented Jul 20, 2019 at 14:32
  • 1
    There have been discussions about pidfd_open as well in Linux, see for example lwn.net/Articles/789023
    – dhag
    Commented Jul 22, 2019 at 15:21

It's just a musing on the lines of "it would be great if Unix was designed differently than it is".

The problem with PIDs is that they live in a global namespace where they could be reused for another process, and it would be nice if fork() returned in the parent some kind of handle that would be guaranteed to always refer to the child process, and that it could pass to other processes via inheritance or unix sockets / SCM_RIGHTS[1].

See also the discussion here for a recent effort to "fix" that in Linux, including adding a flag to clone() which will cause it to return a pid-fd instead of a PID.

But even then, that would not eliminate the need for that self-pipe hack [2] or better interfaces, since the signals notifying a parent process about the state of a child are not the only ones you would like to handle in the main loop of the program. Unfortunately, things like epoll(7) + signalfd(2) on Linux or kqueue(2) on BSD are not standard -- the only standard interface (but not supported on older systems) is the much inferior pselect(2).

[1] Preventing the PID from being re-cycled by the time the waitpid() syscall had returned and its return value was used could probably be achieved on newer systems by using waitid(.., WNOWAIT) instead.

[2] I would not comment on D.J. Bernstein claim that he invented it (sorry for the apophasis ;-)).


The point is that there are many programs that operate an event-loop style model based around monitoring file descriptors with select(2)/poll(2)/epoll(7). But there were various events that historically didn't notify using file descriptors--among them process state transition (e.g., termination) of a child process. The need to separately handle these other events (which include timer expirations, signals, and synchronization events such as semaphore changes) complicated programming of the event-loop model.

In the last several years, Linux development has been chipping away at this problem, so that nowadays we have signalfd(2) (make signals readable from a file descriptor), eventfd(2) (a synchronization primitive whose handle is a file descriptor), and timerfd_create(2) (create timers that notify via a file descriptor), all of which produce file descriptors that can be fed to select(2)/poll(2)/epoll(7).

And finally, recent versions of Linux have added the concept of a handle for a process as a file descriptor. The CLONE_PIDFD flag of clone3() can be used to create a child process for which a file descriptor as handle is returned. The file descriptor can likewise be fed to select(2)/poll(2)/epoll(7) and indicates as readable if the the child process terminates.

  • 1
    Compare FreeBSD's pdfork() introduced in version 9.
    – JdeBP
    Commented Feb 18, 2020 at 11:41

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .