S6 mega thread

Splitting off the discussion about s6 from the neoflex thread. This thread is dedicated to everything s6.

Explaining a bit on @PhaseLockedLoop’s request, I mentioned that s6-rc takes daemon-tools up to 11.

daemon-tools and runit are pretty similar in nature. So is s6-linux-init.

I’d suggest you read all the link bellow, because I can’t explain all the things as good, but I’ll do a high-level overview.

links

s6: why another supervision suite
s6: an overview
s6-linux-init - tools for a Linux init system
s6-linux-init: why?
s6-rc - a service manager for s6
https://skarnet.org/software/s6-rc/why.htmll
skarnet.org: a word about systemd

The general idea under linux (and most unixes) is that you have 3 layers to the OS start-up process.

  1. init
  2. pid 1 (the process that starts the userland up)
  3. service supervisor

The init part can be handled by anything, init can even be a shell script. This is the piece of code that the kernel first loads up and tasked with the early system initialization. The init can be short-lived and exec into something else after its done its early tasks. This is where s6-linux-init lives. I won’t get into much details here. The only thing I’ll mention is that s6-linux-init launches the s6-svscan process, the PID 1 of an s6-based OS.

The PID 1 is the process that is tasked to handle all long-lived system processes (daemons) and restart them if they die. This is the task of “s6” or s6-svscan. Other tools that would live here after being launched by the init are runit or daemon-tools. The PID 1 is also the place where all orphaned processes go to (and get reaped), but I won’t get into that.

So think of PID 1 like a daemon-lord that constantly spawns daemons after they die. That’s its sole purpose. This must be minimal, be able to handle OOM scenarios and has to be very stable. If PID 1 dies, your whole system crashes.

Where the new concept comes into play is the service supervision and dependencies (and that’s s6-rc). It’s not exactly a “new” concept, OpenRC is a service supervisor, but it’s a very serialized one. Sysvinit also had a service supervisor that nobody used. Systemd does service supervision too (via the unit files).

So what the heck’s a service supervision? For that, I’ll have to first come back to PID 1 and explain more about daemon-tools style tools.

So in a traditional daemon-tools-based OS, you have no dependency, all services are started in parallel by PID 1. Services can be scripts and you can handle some checks in the scripts to verify if a certain process is alive before you go into the main service, or sleep if it is not, but sleep is not a good way of handling dependencies.

So if you have sshd that depends on the networking service and they are both started in parallel by PID 1, then sshd will die at least once, until networking is up and then sshd will be up. The PID 1 ensures it gets started every time it dies. This puts strain on your CPU and wastes cycles.

To fix this, you have to be able to tell PID 1 to only start services if their dependencies are met. So s6-rc is the one handling that. Everything starts off with a “down” file, meaning everything has to be down.

The only thing that sv-svscan (PID 1) starts, is the s6-rc service with an argument to start a bundle of services (by default it’s called “default” and it’s what the kernel passes to it - just like you have the “default” runlevel in openrc or runit). Then, s6-rc checks its database for how all the services should start up. It first sees services with no dependencies, like udev, so it removes the “down” file from s6-svscan and thus, udev is started. Once s6-rc confirms udev is up (and not in some weird “starting” state non-sense), then it checks what services depend on udev.

In this case, file system mounting and loading kernel modules depends on udev. But FS also depend on some kernel modules, like zfs or btrfs modules. So s6-rc removes the down file from s6-svscan for the modprobe service. Once this has finished, it removes the “down” file from the FS service, to start mounting the file system.

Traditional PID 1s, like runit and openrc, handle this in runlevels. You must first script your way after the init (init launches “runlevel 1”) to get all your things started up in serial, udev → modprobe → fs and so on. Then the actual PID 1 begins (runlevel 2) which starts everything up (either in serial in openrc or in parallel in runit). For runit, since it’s simpler to explain, it just starts launching everything at once, some things crash, they get started again and eventually you end up with a properly started system.

Does this sound insane to you? Well, it is, but it works. But s6-rc (and systemd) have dependency resolution to prevent things from ever crashing from their dependencies not being up.

So s6-rc kinda removes the need for runlevels in general. After the init starts PID 1, which starts the service supervisor, everything is handled automatically and starts as soon as possible if their dependencies are met.

Systemd solved this issue a long time ago and it’s why it became the de-facto standard on linux. But the way it was done is insane (PID 1 in systemd is huge and wouldn’t really call it stable and the service supervision side is also complicated and poorly thought out - why you’d have an “after” if your service doesn’t explicitly depend on a service? and why you’d have a “requires” if you’re going to start both services in parallel anyway, unless you also use the “after” service definition?).

But as most of you can attest, systemd is heavy and it’s non-portable (can’t run on non-glibc libc libraries and it’s hard to port to different cpu architectures, although most of that work was already done). I’m not here to bash on systemd, we can make another thread for that. This was just as example of how s6 improves on it.

The s6-linux-init is light and only launches the s6-svscan (PID 1) and then the service supervisor begins working on system initialization. And if s6-rc crashes (although chances are very unlikely), PID 1 will just revive it.


The reason I got into s6 was because I needed serious supervision on rebooting linux servers (now that I actually shutdown most of my lab, although I plan to keep stuff up longer once I get some homeprod stuff going). My example, which I complained on my blog on the forum was iscsi.

When the system started, all I had to do was do a process where networking started, followed by iscsid service, followed by a one-shot script that did iscsi-login, followed by mounting a file system, followed by starting VMs or containers. This worked using some service status checks and a sleep loop and for the login script, an infinite after sleep (to make PID 1 believe the “service” is up).

It worked for what it’s worth, but on shutdown, I had no fixed dependency resolution, meaning that everything got killed at the same time, but some services might die earlier than others. If iscsid dies before iscsi-logout, then that’s a problem, we have a hung iscsi connection (that thankfully the freebsd target always handled beautifully). If the file system dies before containers and VMs are shut down first, you get FS corruption (say, a VM was writing something to disk, but got interrupted in the middle by a FS getting unmounted).

That’s how I got a new appreciation for systemd actually (and I don’t mind anymore that nixos uses it, although I wish I could learn how to make the flakes use s6, there’s already “vpsAdminOS” that runs on runit and nix, somehow). I was contemplating whether to switch to nixos on my container box, or stick to something lighter (to allocate as many resources to containers, as I’m running everything on arm sbcs).

Any questions, appreciations or frustrations you have for s6, put them in this thread.

5 Likes

This is an awesome overview of s6 tbch. I knew something’s from wikis but there were some nuances I did not understand

1 Like

So wrapping back a week later after studying. For me runit is enough. Its fast and does all I need but my god s6 should be the way not systemd if it needs to be as friendly as systemd is to learn to other people

That said s6 very much feels like runit extended and I think thats super cool. Its not nearly as lightweight looking at the code base. One of the runit project’s principles is to keep the code size small. As of version 2.0.0 of runit, the runit.c source contains 330 lines of code; the runsvdir.c source is 274 lines of code, the runsv.c source 509. This minimizes the possibility of bugs introduced by programmer’s fault, and makes it more easy for security related people to proofread the source code. The runit core programs have a very small memory footprint and do not allocate memory dynamically. Any programs ran as a service but not use dynamic memory allocation either so its quite secure.

I think both can be compiled to be used with musl instead of glibc?

Man I think its time this forum had a real post talking about the differences in init systems. Where to find good support to each and links to each indepth post like yours… ( I may write one on runit)… If there is interest for this please let me know?

I think its a disservice to a the open service community that everyone just eats systemD when they may or may not need that complicated capability set. Let alone I dont believe systemD has parallel startup like s6 and runit?

3 Likes

I’m almost finished with the WIP one on how to deploy. I’m still a bit behind with the documentation on how to use it (improving the old Starter Pack I made back in 2021).

Same for s6, but because s6 has a lot of different functions, some code is necessary. In s6, the equivalent to runit (the init) is s6-linux-init and the equivalent of the runsvdir is s6-svscan.

You can literally run s6-rc under runit, as long as you can modify s6-rc to work with runsvdir instead of s6-svscan. And it shouldn’t be much to modify (I think), just that processes would need to have the “down” file touched in the runit service, to have them all down and allow s6-rc to start them up, according to the dependency tree. In addition, you must force runit to not kill s6-rc on shutdown, but give it a custom exit command (s6-rc -bDa change then SIGTERM), which I’m not sure if it’s possible in the default runit.

Ok, maybe running an s6-svscan (in its own servicedir) supervised by runsvdir might make more sense. Then, you’d abandon most of runit services and have s6-svscan launch most of these (they can literally be the same run files, but you’d have to add service definitions for s6-rc).

The only limitation (that I can think of) to this setup is that you’ll still be dependent on the runit serialization process on startup and I find runit less powerful in that regards. All inits can be categorized in 3 stages: stage 1 (init), stage 2 (basically multi-user.target, kinda) and stage 3 (shutdown). For runit, you need to script a lot of stuff in the stage 1 (in void, you can view /etc/runit/{1,2,3}, each corresponding to the stage).

The runit stage 1 invokes a lot of scripts from /etc/runit/core-services, that are needed for runit to start up. Stage 2 is from where runit starts and the system shuts down (basically almost the whole “uptime” of the system). And weirdly enough, the stage 3 has cleanup to do that happens after runsvdir is already dead (which doesn’t make much sense if runit would take care of the system state).

I still have an old void ISO, although I think we could find the same info on github. The older runit/1 was way messier, but somewhat recently it got converted into serialized scripts under core-serivces, which makes things a bit cleaner, but is still hacky. You have no way of undoing these changes via a service (machine state) change and some things can be run in parallel (like mounting dev, sys and proc, instead of mounting them 1 by 1).

And it’s not just that. On startup, runit requires that you run a udevd daemon unsupervised, before runsvdir starts, which defeats the whole purpose of supervising longruns (explained better in s6-rc/why). But udevd also requires oneshots after the daemon is active (which runit can’t handle, which is where the init scripting comes into play).

The s6-rc runtime does a way better job at handling all the services and their dependencies and it’s the reason I like s6-rc so much. You can define service dependency to just scripts (they’re called oneshots). Heck, even systemd can do that (albeit in a very non-optimal way). And s6-rc (when s6 runs as pid1) handles udevd fantastically. And s6 also handles logging better, but that’s besides the point (it’s hard to lose logs from crashed services in runit anyway, but it’s theoretically possible).

With s6-rc, I translated all the core-services that void provides into s6-rc services. And the only thing that stage 1 in s6 suite does is mount a /run tmpfs (for s6-svscan), launching s6-svscan, creating the /run/service directory (for s6-svscan and s6-rc) and launching s6-rc. Can’t get simpler than this. Stage 2 then starts all the services needed (like mounting the fs, starting tty etc.).

Neither does s6. On the skarnet site, it’s explicitly mentioned in a few places that no malloc is used (i.e. dynamically allocating memory). Not sure about s6-rc (I doubt it does, but I haven’t checked). In the s6-why, it’s mentioned that neither s6-svscan or s6-supervise allocate heap memory (re. same thing). I think there’s a very good reason skarnet got sponsored by Alpine Linux to work on s6.

Both work with glibc, musl, llvm, uclibc and I believe dietlibc too. Systemd only works with glibc.

I didn’t find any better runit guide than Gentoo’s wiki on it. It’s a bit hard to digest, but you can skip most of it and go where you need to go (like custom down commands for services.

https://wiki.gentoo.org/wiki/Runit

I’ll be honest, I didn’t do a ton of homework on runit before switching to s6 suite, but that’s because I was already kinda sold on it. I did a lot of research on startup sequence and trying to define service dependency (which I made in the run service file for runsvdir services, but I couldn’t figure out the shutdown sequence, so I gave up on runit).

It does startup things in parallel and based on a (weak) dependency tree, but it’s a hackfest (requires, wants, needs, partof, bindsto - why so complicated? you should never have a service that has soft dependencies on other services).
https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html

If you don’t use “wants” and “after” together, systemd will literally want to start a dependent service in parallel with the one getting started. Say you need to start nginx which depends on mysql. If you only have wants, they are started at the same time. If you have after, nginx starts after mysql gets started, but does not guarantee that mysql is actually up when nginx starts. You must use both…

With s6-rc, a dependency is something set in stone. Dependency not up → service not up. If your website can function without mysql, but more features are enabled by having mysql running, then don’t set mysql as a dependency for nginx and allow them to start in parallel (or if mysql fails to start, you still start your web server, but you’ll get db connection errors).

I don’t remember if I mentioned here, I need a service manager that handles shutdown sequence properly. I don’t want everything killed in parallel, if I have A → depends on B → depends on C, then when I start up I must have the order of startup C, then B, then A and when I shut down, I must have A going down, then B, then C. Systemd actually does this. Runit doesn’t, it kills everything in parallel.

For runit, the easiest way to provide service dependency is to write the run service file to do an infinite wait / sleep (or fail after n attempts or seconds) in which it checks if the dependent service is up and running. Otherwise, a servicve will just crash and get started back up, until all the services are eventually started.

On shutdown, I’ve never looked deep into it in runit, but I don’t think there’s a way to stop a service before another one gets stopped.

1 Like