Over the holidays I took some real vacation and finally got stuck back into my hobby project. As part of it I implemented a Unix shell with job control. It turns out a shell is a reasonably easy introduction to some of the crustier parts of Unix I've been avoiding for a while now. It lets you get a sense of TTYs without dealing with the really terrible parts.
(If you ever wanted to convince yourself that Unix is due for a replacement, take a look at how much kernel API surface is dedicated to terminals. It is a mess that can only be justified by historical argument.)
Here is a taste of some of the fun of shells, for those of you who find such things fun:
When you start a process in an interactive shell, the process gets a new process group id, and that pgid is brought to the foreground of the current terminal session. (See tcsetpgrp(3) for more details.) From here on out the terminal delivers signals to the new process group.
So far so good.
Now, what happens when you start a pipeline? Executing 'echo hello | rev' starts two processes, where do the signals go? That is why the signals are delivered to a process group: the shell starts echo, creates a new pgid, and then starts rev and gives it the same pgid. Easy enough, and I implemented similarly to what you will find in the GNU libc manual.
Except it sometimes did not work.
Turns out what was happening was that 'echo hello' is so short-lived, and its output tiny enough to fit into the kernel buffer, that it would exit before my shell had a chance to assign its pgid to 'rev'. By the time I got there, the pgid was invalid.
Digging around in the bash sources revealed that I was not the first to deal with this, jobs.c:200:
"Surely I spoke of things I did not understand, things too wonderful for me to know."
/* Pipes which each shell uses to communicate
with the process group leader until all of
the processes in a pipeline have been
started. Then the process leader is
allowed to continue. */
int pgrp_pipe[2] = { -1, -1 };