Joker_vD
2 days ago
> Docker’s lack of init by default
If anything, it's the problem with the design of the UNIX's process management, inherited thoughtlessly, which Docker decided to not deal with on its own. Why does there have to be a whole special, unkillable process whose only job is to call wait(2) in an infinite loop? Because in the original UNIX design, Ken Thompson apparently did not want to do too much work in the kernel during exit(2): if process A calls exit(2) while having 20 already exited children it didn't wait for, you either have to reap those 20 processes (which involves reading their PCBs from the swap on disk), and then potentially reap their already exited children, and their grandchildren... or you can just iterate over the process table and set the ppid of A's children to 1 and schedule PID 1 to run and let it deal with reaping one process at a time in wait(2). Essentially, the work is pushed to the scheduler, but the logic itself lives in the user space at the cost of PID space pollution.
cyphar
2 days ago
On the other hand, process managers care about the exit signal of child processes and the most straightforward way is to keep around a zombie that just contains that information and ensures the only identifier available to userspace at the time continues to reference the same pid (of course, zombies on Linux contain some more information but some of that is sometimes necessary and pidfds remove some of the need for this).
The funny thing is that there is a way to opt out of zombie reaping as pid1 or a subreaper -- set sigaction of SIGCHLD to SIG_IGN (and so it really isn't that hard on the kernel side). Unfortunately this opts you out of all child death events, which means process managers can't use it.
If you want to argue that interaction of zombies with re-parenting is borked, that is a very different discussion (though re-parenting itself is necessary for daemonisation). If there was a way to only opt-out of zombie reaping for reparented processes things would be much nicer.
IMHO the bigger issue with Docker and pid1 is that pid1 signal semantics (for instance, most signals are effectively SIG_IGN by default) are different than other processes and lots of programs didn't deal with that properly back then. Nowadays it might be a bit better, it Docker has also had a built-in minimal init for many years (just use --init) so the problem is basically solved these days.
Joker_vD
2 days ago
> the most straightforward way is to keep around a zombie that just contains that information and ensures the only identifier available to userspace at the time continues to reference the same pid.
Again, that's only really needed if the zombie process's parent has not yet died itself; in fact, this is also how Windows API operates: unless you call CloseHandle on the process descriptor obtained from the CreateProcess, the child process will not go away and will hang around. However, if there are no open descriptors to a process, then it will disappear entirely on its own the moment it calls ExitProcess; and if that process had last descriptors open to some other zombie processes, those too will go away. All of that, and without no need for a dedicated PID 1 which shall not be killed.
Reparenting, of course, is not needed for long-running services/daemons, as demonstrated by daemontools, runit, s6, Upstart, systemd, etc.
cyphar
a day ago
The handle solution is definitely better (which is why I mentioned pidfds -- I actually think it might be possible to do this today with SIG_IGN and PIDFD_GET_INFO but it's a little hacky) but Unix only had pids and most descendants only have pids too. In that paradigm the zombie solution is kind of inevitable (as with most other Unix hacks). My point was that it wasn't as simple as "just doing some more work in exit(2)" -- you would need to redesign the process API.
There are loads of other related issues to this of course -- libraries cannot really spawn subprocesses as part of their implementation because programs using the library could see the SIGCHLD by accident. With the right design, the handle approach is better here.
> Reparenting, of course, is not needed for long-running services/daemons, as demonstrated by daemontools, runit, s6, Upstart, systemd, etc.
Because they are spawned as children of the daemon. Reparenting is needed for standard Unix utilities that want to run in the background (especially in an interactive shell). Of course, you can redesign that too if you have a time machine, but it would require more work than you originally intimated.