(BTW, this is mostly a problem with signal handlers as a concept, not with Go.)
> The problem with using the syscall module is that Go does not export Fork and Exec. It exports ForkExec, because it needs to ensure they run on the same thread.
And because you can't safely allocate after a fork in managed languages, because fork drops your other threads, leading to the same problems above. :)
It queues your Python signal handling function, returns from the signal handler, and then runs it on the main loop at the next tick.
As far as I know this works fine. I wrote the same program in Python and it seems to work just fine, and have more functionality than the Go version. I guess one problem is that if you do a long blocking syscall on a tick in the main loop, your signal handler can be delayed indefinitely. There might be some hacks around this in the Python interpreter, but I don't recall offhand.
Though it is definitely a killer feature of Go to write in a blocking style rather than in a state machine style... but I feel that there might have been a way to do it without baking it so hard into the runtime (?). Maybe like coroutines in Python, which run on a single thread and are optional.
M:N threading causes a lot of other complications too, like calling from C to Go, etc.
In either case, the signal handler doesn't get run for an arbitrary period of time. And another signal could occur.
I didn't trace through the code to see how this is handled... but it seems like it is hard to handle simply and correctly (you're kind of recapitulating the problems of signals to begin with, in a higher level language).
Any docs / clarity on this would be interesting to me, as I will probably implement this in my own language soon.
Unless it changed, POSIX gives no guarantees of stack size and function calls re-entrancy guarantees depend a lot on the OS and compiler versions.
Hence the best approach in managed system languages is just to mark a signal as active and re-trigger it outside the handler when control gets back to user space.
Attached simple demo code of what I mean with ForkExec, Wait4.
EDIT: to clarify, the demo code is not intended to work like init, just to spawn a couple of processes and wait for them to terminate.
package main
import (
"log"
"syscall"
)
func main() {
cmds := [][]string{
{"/usr/bin/sleep", "4"},
{"/usr/bin/sleep", "5"},
{"/usr/bin/sleep", "1"},
{"/usr/bin/sleep", "2"},
}
for _, cmd := range cmds {
if len(cmd) > 0 {
if _, err := syscall.ForkExec(cmd[0], cmd, nil); err != nil {
log.Fatal(err)
}
}
}
for nwaited := 0; nwaited < len(cmds); nwaited++ {
var w syscall.WaitStatus
pid, err := syscall.Wait4(-1, &w, 0, nil)
if err != nil {
log.Println(err)
} else {
log.Println("pid", pid, "exited", w.Exited(), "exit status", w.ExitStatus())
}
}
}But the point still stands... you get a subset of the OS API. Everything has to be "canned" into that struct and you can't execute arbitrary code between fork and exec.
Especially with containers, this is a never-ending target... They apparently have user namespaces, but what about network namespaces or IPC namespaces?
[1] https://golang.org/src/syscall/exec_linux.go
Now, it might be good enough for your application, and it probably IS good enough for everything except a full-fledged production quality init system. For that case I would be worried about running into limitations.
There's no reason calls to e.g. unshare(2) and setns(2) can't be added to forkAndExecInChild in exec_linux.go like they have been added for the other syscalls needed to set up a child. I guess if anyone is reading this thread and wants that they can follow the guidelines for contributions and look at https://github.com/golang/go/issues/5968
EDIT: It's also possible to supply CloneFlags though the SysProcAttr struct, e.g., CLONE_NEWIPC, CLONE_NEWNET.
Also, the subject's been raised before, and regrettably (IMO) the proposal rejected. As a result, you'll find a nontrivial amount of C appearing in e.g. the runc project. (Which is a crying shame, because it invoking cgo means runc ceased to be trivially reproducibly buildable, last time I checked...)
Not sure what you mean by 2). If you're suggesting polling, then that sucks, because one important feature of an init system is to restart processes immediately.
All in all my point is that Go is a very poor language for this... worse than Python (which I also tried and ended up using). I think it's great that the author is experimenting though -- I think that's the only way to make these issues clear.
An init system has to call wait, that's one of its primary tasks, especially on orphaned processes it inherits.
Though I wonder if you can work around all this by using signalfd (http://man7.org/linux/man-pages/man2/signalfd.2.html) and have SIGCHLD delivered through normal file descriptors - I don't know the low levels of Go enough to know if you can integrate a raw file descriptor with its runtime, or whether messing with SIGCHLD causes other issues, but it might be worth pursuing.
At least you can't with its portable interfaces such as exec.Command() and cmd.Wait():
http://www.mustafaak.in/2016/02/09/forking-process-in-myinit...
You end burning a thread per process, which is pretty lame for an init. The wait() call blocks a goroutine AND OS thread, so the go runtime will have to start a new thread for the next wait().
Whereas in C you can do the entire thing from a simple thread (I call it "async processes"). You set SIGCHLD handler and get async exit notifications, and then process those in a single-threaded loop. I'm pretty sure there is a problem doing this in Go (besides resorting to the syscall module)... I think it has to do with the fact that Go turns signals into messages on a channel, but I don't remember right now...
EDIT: Now I remember. The problem with using the syscall module is that Go does not export Fork and Exec. It exports ForkExec, because it needs to ensure they run on the same thread.
https://golang.org/pkg/syscall/#ForkExec
This severely limits the usefulness of your init program, because in Unix there are all sorts of important things that happen in between. It's how you set the child process state. You basically can't do anything with file descriptors or containers at all with this model.