This provides a superior API for process management and supervision on Linux, in the form of a minimal and unprivileged executable “supervise”. supervise is designed to be used as a wrapper for child processes you execute. Therefore supervise is designed to only wake up when there is an event to be handled, and it otherwise consumes no CPU time.
There are three main benefits of using supervise to wrap child processes:
- The ability to get notified of child process exit or status change through a file descriptor interface
- Automatic termination of your child processes when you exit (by virtue of the fd interface)
- Guaranteed termination of all the transitive children of a child process; no possibility of orphans lingering on the system
It is a fully supported design goal for supervise to be usable in a nested way. You can wrap a child process with `supervise` which in turn forks off more child processes and wraps them in `supervise`, in arbitrary configurations and depths.
When supervise is exec’d, it expects to have a stdin, stdout, and stderr.
supervise further expects to have some number of child processes already started (and it provides no mechanism to start more).
And most importantly, supervise expects to have already had CHILD_SUBREAPER
turned on through prctl
.
Once supervise starts up, supervise has three primary functions:
supervise reads from stdin,
parsing the input as struct supervise_send_signal
.
This instructs supervise to send a specific signal to a specific pid.
supervise checks if that pid is an immediate child of supervise, and if it is, then supervise sends the signal to that pid.
Otherwise it does nothing.
supervise waits for any of its immediate children to change status,
and writes the child status changes to stdout,
in the form of siginfo_t
structures as returned by waitid
.
When stdin closes, supervise exits. If supervise exits for any reason, it first SIGKILLs all its transitive child processes.
supervise takes no arguments and reads no environment variables, so those need not be supplied.
While supervise is a standalone executable, it cannot practically be used from the shell; usage requires more fine grained control over file descriptors than the shell provides. Thus it is primarily useful when used as part of a library in a programming language.
One interface for such a library could be something like this:
spawnfd : string list -> file_descriptor
which, making use of sfork, is implemented in Python something like this:
def spawnfd(args: List[str]) -> int:
parent_side, supervise_side = socketpair(AF_UNIX, SOCK_CLOEXEC)
with sfork.subprocess() as supervise_proc:
prctl.set_child_subreaper(True)
parent_side.close()
with sfork.subprocess() as child_proc:
supervise_side.close()
child_proc.exec(args[0], args)
supervise_side.dup2(0)
supervise_side.dup2(1)
supervise_proc.exec(supervise_utility_location, [])
supervise_side.close()
return parent_side
An implementation with traditional fork
is also possible.
To get the definition of struct supervise_send_signal
,
so you can send it to supervise’s stdin to signal its children,
you can link against libsupervise and include supervise.h
.
See my blog post about the Unix process API for more.