Writing my own init with Go
mustafaak.in
mustafaak.in
That was my favorite line in the entire post. I love seeing this kind of fearless experimentation.
When I first learned to program, I desperately wanted everything to work and would try very hard not to break things. As I've matured into senior roles, I spend of a lot of effort creating "safe" development and testing environments like the author did here, just so that I can break things and make observations and then throw the environment away when I've learned what I needed to learn. Tools like vagrant and cheap cloud computing have made this easier to do than ever.
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.
(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...)
Deleted comment
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.
http://git.suckless.org/sinit/tree/ (look at config.def.h and sinit.c)
and build up from there. Given how minimal sinit is, it's a great place to start your functionality from, and see the power (and limitations) of a basic SystemV init system is.
Next step, bare metal runtime! :)
However given Oberon's influence on Go, I find positive other devs that like Go, do use it for such purposes.
OCaml already has MirageOS advertising it for system level coding. :)
Edit: Regarding concurrency, even systems programming languages older than C have built in support for concurrency. C and C++ are probably the outsiders in terms of built-in support for concurrency, language or std libs.
Of course, very few Unices actually survive out-of-memory - Linux runs an OOM killer by default, OpenBSD tends to crash the kernel, etc. Apparently Solaris does have a good story here. (Of course, you'd have to ensure that the system doesn't spend all its time swapping, regardless.)
Upstart is much more readable even though it has various idiosyncrasies.
This in turn because if you follow Poetterings project history it starts with him creating Pulseaudio to handle moving between sound devices while in use, then crated Avahi to provide a means of Pulseaudio equipped computers to find each other on a network, and then came Systemd based on what he learned about daemon security while writing Avahi.
Sadly i can't shake the feeling that the security in systemd is all too often leaning towards "security theater".
http://www.mustafaak.in/2016/02/09/forking-process-in-myinit...
Tough to use it in OpenWRT, DDWrt type of embedded system where system might only have a few megabytes of RAM/ROM.
From a technical point of view, what are the reasons you chose Go over alternatives, such as C, C++, OCaml or Rust?
(Just curious, as all those languages have compilers that produce fast, optimized, self-contained binaries.)
Memory safety may arguably be, but I see no reasonable argument that garbage collection (a particular strategy for memory management) is.
Rust has a very advanced memory lifecycle management. The promising part is that it may give you almost all of the advantages of a garbage collector while not needing an actual garbage collector in most cases. Like RAII in C++, but much more advanced.
I'd be interested whether this actually works well for a nontrivial project such as an init system.
Your critical thinking has not been engaged. Linux is a useful bit of code but "perfect" is not what it is known as everywhere.
My side of the room like this little ditty
Linux, by amateurs, for amateurs -- Dave Presotto [1]
Or
i’ve wondered whether Linux sysfs should be called syphilis -- Charles Forsyth [2]
Or if you like your detractors with a bit more fame
Unix has retarded OS research by 10 years and linux has retarded it by 20. -- Dennis Ritchie as quoted by by Boyd Roberts in 9fans.
I won't go on
Either that or wait for GNU/Hurd.
I said "Linux isn't perfect, like you think it is"
Nothing is "perfect", and no one has claimed that "Linux is perfect".
Truly an empty comment, as it adds nothing except makes you look smarter than someone else. You don't offer anything insightful, you are trying to be insightful by proximity. So anyway, you reinforced your complex when you said "Try reading what I wrote". So I called you a dick.
Now take your own advice and "try to read what _I_ wrote". I only called you a dick. Why are you trying to make me defend a position I never took about perfection?
oh we're going down that route. Well if it's literalism we're wasting time with then try reading your own comment
> You are being a dick, offering no reasons other than the opinions of people you presumable agree with.
That isn't just "calling me a dick".
but lets stop here
Sure you can, you can even use a shell script: https://wiki.gentoo.org/wiki/Custom_Initramfs#Init
And yet the author seems to be comfortable using those linux-specific features:
“Also, the kernel paremeters must have the rw flag or you have to remount the /dev/sda1 as rw, but changing kernel parameters are much more easier.” (from the follow-up post).