Writing an init with Go (part 1)

__mrvik__

MrViK

Posted on December 30, 2020

Writing an init with Go (part 1)

Welcome to the first article of the series about making an init process with Go.

So recently I was making some systems programming stuff with Go (a Podman service manager in case you want to check it out).

Then thought, let's write an init, how hard can it be?

Hotting up

So, what is the PID 1 supposed to be on a Linux system?

  • Wait zombie processes
  • Start other needed services (not really attached to PID 1)
  • Mount filesystems (also not really attached to PID 1)
  • Do the teardown, sync filesystems and good night.

For sure you have been thinking how to do those things on your favourite language, at first sight it isn't so hard.

Tracing the first lines

From points before:

  • PID 1 does point 1 and 4 (partly). Then it launches a thing called "service launcher".
  • Service Launcher (name is so original!). It
    • mounts filesystems (calls mount -av) (BUG here, / is not remounted according to /etc/fstab settings)
    • listens on a unix socket (control socket)
    • launches all services, watches them (only the main process, see last links for further info).
    • watches signals
    • does teardown (stop services and unmount fs)

Heavy load was put under service-manager.

Hands on PID 1

Check out the code at cmd/init

Ok, this one is simple. If you had a glance at the code, you may notice it's heavily based on sinit, the suckless init

  • First, we start the service daemon. This call may fail, but who's afraid of an early panic on the morning?
  • Then we make sure ctrl+alt+del sends SIGINT to PID 1 instead of calling reboot routine in kernel.
  • Catch signals. Which signals?
    • SIGUSR1 - poweroff
    • SIGINT, SIGTERM - reboot
    • SIGCHLD - Wait zombie processes.
  • After signal catcher exits (or service-launcher exits) it kills service-launcher, then waits for it, does syscall.Sync() and syscall.Reboot() with selected option (from signal watcher).

Only 3 explicit syscalls here (others are done from stdlib code).

  • syscall.Sync will block until disks are synchronized.
  • syscall.Reboot requests changes in reboot logic from kernel. E.g. it changes ctrl+alt+del behavior, asks for a reboot, poweroff, halt, and so on.
  • syscall.Wait4. Go allows to wait for processes from stdlib, so why do we do this from syscall package?. That call allows us to pass WNOHANG avoiding a blocking call, we only check if is anybody dead and ready to be removed from process table.

Nothing too complicated here, overall experience was pretty straight-forward and didn't hit any limitation in the stdlib nor the language.

Also, this can be compiled as an 1.6 MiB static binary (as it has no cgo calls).
Binary size can be reduced by removing the "log" package but I didn't see any problem with that size.

So, what about the service launcher? It looks promising

Yes, it was hard and has a lot of concepts, so we'll get around it on the next article (part 2). Stay tuned.

Third (and last) article we'll be running this on qemu and contain some final thoughts about the future of this thing.

See you next time!

💖 💪 🙅 🚩
__mrvik__
MrViK

Posted on December 30, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related