Unix Structured Concurrency
matklad.github.io
matklad.github.io
Session leaders are similar in some ways. When a session leader dies SIGHUP is sent to all members of that session's foreground process group and its controlling tty is released but IIRC it doesn't do anything about background process groups. (Session leaders control this with tcsetpgrp()).
A harder problem is making sure that the program shuts down if some thread panics. There are several ways to do that, but it's not automatic.
Incidentally, regular mutexes are "poisoned" if the thread holding them panics. Anyone trying to lock the mutex gets an error. However, parking_lot mutexes do not do this. If the thread holding one of those panics, the mutex unlocks. This may be unsafe.
The process group mechanism may be one of the less well-known parts of UNIX.
> There’s process group mechanism, but it is one-level deep and is mostly reserved for the shell.
So the author seems aware that they exist, at least
I've come across programs that use SIGHUP for various things like config reloading when they could use USR1/USR2 instead. UNIX signals are kind of crap in my opinion. USR1/USR2 should probably not even exist, there should be some other way to 'message' processes without IPC.
Shells push each job into a separate group, making the shell interpreter the process group leader. Thats why suspend works, it sends that whole process group to the background.
Depending on how an interpreter is setup a shell script might end up under its own progress group if the shell always creates one... usually when invoked as an interpreter it shouldn't be making a new one though.
I'm assuming all processes are under your control in this scenario. It should be very rare for any process that isn't a shell or daemon to create its own process group or session.
Sessions can optionally have a controlling terminal, one (and only one) foreground process group, plus any number of background process groups.
The foreground job is how the system decides who receives signals generated by the TTY. If the controlling TTY disconnects or is closed the session leader gets SIGHUP which it usually uses to signal all the process groups it created. If the session leader dies or exits then all processes in the foreground process group get a SIGHUP.
If you exit an interactive shell or close your SSH session the system generally cleans everything up as you would expect. The processes involved generally don't implement any special handling code to make this happen: the session leader (your login shell) uses the standard Unix job control APIs to manage the session + process groups and the expected behavior falls out naturally.
Unless there is a good reason to implement this differently: don't. Use the standard Unix job control APIs as they were designed to be used.
Which is my point. It can't really be used as a generic mechanism for killing all child processes.
But the issue here is more basic than timeouts. If we are to use process groups in the example, then B must be a process group leader for C and simultaneously belong to the process group led by A, which is impossible on Unix—that’s what TFA means by “[the] process group mechanism [is] one-level deep”. Close but no cigar.
(On the other hand, no reason this can’t be an extension to—I would even say a rationalization of—the process group mechanism. It doesn’t exist right now, though.)
This can not be achieved using a pattern equivalent to { ... }.
The parent process itself might be abruptly killed, and the finally blocks / destructors / atexit hooks are not run in this case.You call waitpid() directly, configured for timeout, and if it returns with error that the process is no longer available, then act upon it.
* parent process is killed after it forks, but before it calls `waitpid`. Nothing even attempts to kill&wait for the child
* parent process is killed after it issues `waitpid` syscall. The child continues to happily execute while the parent is dead.
- yeah this one is a bummer, write the child pids, collect them via a watchdog process
I think Solaris and some of the BSDs had something like this, so I wish Linux would add one too (though, I guess Linux has managed without one for so long that maybe that's proof enough that it's not really needed, and worst case you can always just reboot the box - nuking from orbit is the only way to be sure)
1. It is relatively complicated to use. And even harder to use properly. From what I understand, to reliably kill all processes you need to freeze the cgroup then list the pids in it, then send a signal to each of those pids. Which is pretty involved, requires a separate supervisor process, and isn't 100% reliable in cgroupv1.
2. It requires root, or at least having control of a cgroup delegated to the process. You might be able to use user namespaces, depending on the distro and kernel, but that makes the implementation even more complicated.
3. It is possible to escape the cgroup, if the child process has permission to write to the task file of another cgroup.
Cgroups are useful, and can be used for this use case in some common scenarios, such as docker and systemd.
But as a general tool for structured concurrency that normal processes can use, it doesn't quite fit the bill.
Check the output of `ps -efj` PGID's column.
It's possible to do it without systemd of course, but that involves an ad hoc, informally-specified, bug-ridden, slow implementation of half of systemd anyway.
Workes well, needs bo superuser or special filesystems, always composable. Not sure why people don't use it much.
> /dev/reads-and-writes-block-forever
Maybe not in /dev/, but if you make two named pipes ("mkfifo") you can read from one and write to the other, and they'll block since nothing is attached to the other end.
Not perfect, but possible.
My debugging was not aided by the fact that disabling the code where I set PDEATHSIG had no effect, since someone else's code was invisibly setting it regardless.
Related to this is the double-fork pattern to avoid zombie processes (and a couple other issues) when initiating a daemon process.
The parent process was an event loop based python program whose main function was to manage the creation and deletion of these child processes, and the simplest way to spawn child processes without blocking the event loop is to call fork(2) on a thread pool. My thread pool was triaging the number of worker threads based on demand, so occasionally it would decide a worker was no longer needed, and all the child processes that happened to have been created on that thread would get SIGKILL'd — something you rarely want when using a thread pool!
I didn't want the child processes to die unless the parent process's business logic decided they were no longer needed, or if the parent was itself killed (this latter reason being the motivation for setting PDEATHSIG).
Once I understood why my processes were dying, the solution was simple: make sure the worker threads never exit.
- You're running unit tests for a compiled language. Broken code may crash
- You also need to start processes from the same process.
In contrast, I basically never have unit test processes that themselves start processes. Instead all unit tests are a single process, and started with a shell script. The shell is responsible for waiting, and handles concurrency.
Although I guess the third consideration is
- the tests need to work on multiple platforms
I can sorta see that, though I still think it's fairly niche, because I think that starting both processes and threads should be parameterized. Library code generally shouldn't start processes or threads -- that should be a policy that you pass in.
---
I have come across cases where I'm specifically testing code that starts processes (a shell!), and you get orphans if either the test code or the tested code isn't quite correct. However I'm pretty sure the pattern in the OP would not be enough in those cases -- as mentioned, it requires some cooperation from children and parents.
And BTW the general problem of containing adversarial processes is impossible in Unix, and would require Linux cgroups (mentioned in the articled). There's an inherent race between enumerating children and killing processes. A malicious process can always escape your attempts to kill its children.
To be honest, what I started doing more is running tests in CI, so there is another layer of cleanup.
I think the bottom line is that tests that start processes are very much special cases, and should be separate from most of your test suite.
This is what I did: I have one thread whose sole purpose is to wait on children. It's activated by a semaphore. But it will wait and then send a message through a pipe to the thread that started that child when the child exits. (As a side benefit, the launching thread can then use poll() or other fd waiting.)
The killer is killing children; I found that it is best to kill all children that haven't been waited on when killing is necessary. The problem is that child exit messages can block the pipe, so you need to take them out, but they may arrive in a different order than you expect. If you're just killing all children launched by that thread, no problem, just read and ignore every time you kill a child.
There are a lot of details (that code has the single longest code comment I've written in my life), but most of them are in that classic "Gotchas" document matklad references.
So a SIGKILL is really the user passing the responsibility of the children onto some other process.
You've implemented something which kills all your child processes when you exit cleanly, but which leaks child processes if you exit uncleanly. This is, frankly, easy to do, and not interesting. It's not a "problem".
The article is solving the problem of: how do I make sure my child processes die even if I exit uncleanly? That's an actual hard problem to solve.
It's true that if you spawn something interactively from the shell and kill it, the shell will clean up the file descriptors before returning to read the next command. And that will look like a close to any children that might have launched. But redirect stdin from a real file and no such close is going to happen. Likewise launcher tools like systemd that want to parse the process output and control it have behavior that tends not to be shell-like.
I feel like this should be made opt in somehow. If you are a noninteractive process and stdin is a tty then you probably shouldn't be swallowing input. I frequently blind-type the next command while a long running command is active, because well behaved noninteractive programs don't swallow input from stdin.