You can do it non-portably in Go by using os.ForkExec and Wait4(-1). The portable exec package assumes you will call Wait(pid), and not Wait(-1), which basically implies using a thread per process. Go's runtime isn't magic -- if you call libc/syscall wait(), an entire thread will be blocked, and the runtime can't use it for anything else. In this case this is the lifetime of an entire process, which is forever for server processes.
I'm pretty sure nobody would use a real PID 1 that burned a thread per process (systemd, upstart, etc.). But yes, for most use cases, in the grand scheme of things, it's probably not a big deal. I suppose Linux has an O(1) scheduler, although I'm not quite sure how this affects scheduling (interested in any comments).
But this goes to show that portable APIs are awkward and obscure for low level code. Better to use raw Unix APIs for something like an init server. Python and Java have similar problems.
IMO all interesting code nowadays is POSIX-like, so we should drop the pretension of portability and simplify our lives. Unix works.
// run in its own goroutine
func (in *TaskInstance) awaitDeath() {
in.waitErr = in.cmd.Wait() // ties up an OS thread for the lifetime of a process
...
}Actually not anymore. The current CFS scheduler is no longer O(1) but O(logN):
Have one goroutine loop forever:
* wait for SIGCHLD
* take the childlock
* do nonblocking Wait() until no unwaited child remains
* release the childlock
The childlock would need to be taken for reading by os.Process.Kill and when a syscall that takes a PID of a child is called (after taking it we'd need to verify that the process we intend to touch isn't already dead).However, you don't need to use goroutines (or threads). You do it as you would in C (and how all real PID 1 systems are written) -- with a single thread that starts processes, receives signals, and reaps children in a non-blocking fashion.
This style of program -- a program that needs to simultaneously wait for child processes/signals and fd events -- is quite awkward in Unix, but it definitely works when you get the idea.
To wait on a fd and a signal in a single threaded program, you would use the "self pipe trick" in classic Unix. In Linux, you can ask for a signal to be delivered over a file descriptor with fdsignal(). But AFAICT there is no real reason, and portability across Unix IS a good thing IMO (but not portability to completely different OS's like Windows; in that case I would write a completely separate program using their native APIs).
node.js actually does a great job making this API easy and efficient. It is probably the only runtime (Python/Ruby/JVM/etc.), that doesn't suffer from this problem doing "async processes" (i.e. a complement to async networking).
In any case, you are not calling Wait4(<specific PID>), which is what implies the thread per process.
Re-read what I wrote. If that doesn't convince you, then download and run the code. Run "pstree" on it and observe how many child processes and threads there are. You'll learn something useful about the relationship of the Go runtime to the OS.
{ "user": ["_env", "${USER}"], "cwd": "/var/www", "standardEnv": true, "numFiles": 1024, "binary": "/usr/sbin/nginx" }
And php-fpm.json:
{ "user": ["_env", "${USER}"], "cwd": "/usr/sbin", "standardEnv": true, "numFiles": 1024, "binary": "php-fpm", "args": [ "-F" ] }
Hope that helps
Thanks for putting this out there.
@chubot mentions the thread-per-process overhead. Runit does process-per-process so in that aspect Runsit should be a bit lighter. Then again, Runit is extremely tiny, its statically compiled binaries taking up next to nothing. The wait(-1) trick sounds good, but there must be a reason why for example Runit doesn't use it, since afaik Runit only runs on POSIX systems.
Keep up the good work!
EDIT: a web interface, that's pretty spiffy! (though I've made something similar work for Runit by querying the status of a service and serving up a dashboard, with a Go webserver of course).
https://github.com/VividCortex/pm https://github.com/VividCortex/pm-web
I'm still searching for a process manager that I can love for use with Docker (I've played with runit and am currently playing with s6), but the total lack of readme or docs makes this link fairly unhelpful.
Now that I've got it build, the next hurdle is making init scripts in execline. It's a fairly simple language, and I enjoy the premise behind it, where scripts should be clear and deterministic.
Overall, s6 provides a lot more helper tools for daemon management than runit did, so it looks like it's gonna be great for my use case. I've got an automated build set up to handle making a Docker image with s6 prepared: