14

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? – ctrl-alt-delor Jul 20 at 14:54
  • You can ask for signals to be delivered over pipes. That is what I do. – ctrl-alt-delor Jul 20 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 Jul 21 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. – Steve Summit Jul 21 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. – Steve Summit Jul 22 at 0:46
14

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 Jul 20 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 Jul 21 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 Jul 21 at 6:59
  • 6
    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. – KJ Tsanaktsidis Jul 21 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 Jul 21 at 21:40
9

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

8

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 Jul 20 at 14:32
  • 1
    There have been discussions about pidfd_open as well in Linux, see for example lwn.net/Articles/789023 – dhag Jul 22 at 15:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.