homepage/papers/arch-runit.mdwn

350 lines
11 KiB
Markdown

Title: Runit on Arch Linux
I like how runit manages things,
especially the restarting of dead daemons.
I was growing tired of sysvinit when systemd started making inroads,
and when Arch moved to systemd,
I figured it was a good time to make the switch.
I know a lot of people feel very passionately about systemd;
I just like runit better.
My wife's computer uses systemd and I have no issues with that.
Distribution people have done a swell job making the machine work using systemd,
and that's just fine by me.
If you want to rage about systemd,
there are many web forums available for you to do so.
What's the advantage?
---------------------
Runit lets—no, forces—you to write your own startup scripts.
You can write them in any language you want,
but Bourne shell is pretty convenient.
I think this is the biggest selling point for me.
I like doing things with specialized programs,
and I know Bourne shell pretty well,
so it's easy for me to figure out what runit is doing,
and extend it.
If a daemon dies, runit restarts it in 2 seconds.
I find that convenient,
but some prefer for things with problems to be restarted manually.
Runit wants daemons to run in the foreground.
Having written many daemons,
I like this philosophy a lot.
I never understood why the "fork twice" hack needed to be duplicated in every daemon ever;
Runit takes care of that for you.
Runit encourages things to log to stdout (or stderr),
instead of syslog or custom logging code.
Writing to stderr is also very convenient from the standpoint of the daemon's author.
It's a natural way to provide information to the user,
and all that's needed for "debugging mode" is to launch the daemon at the command line
instead of from runit.
stdout from a service is sent to stdin on a log service,
which runit also keeps track of.
That log service can be anything you want:
`svlogd` does a pretty good job timestamping lines,
and it also rotates logs automatically without needing to stop and start the daemon.
Peculiarities of my setup
-------------------------
I like the runit that comes with busybox,
so I'm using that.
If you prefer Gerrit Pape's runit,
the procedure will be similar,
but there are subtle differences you will need to watch out for.
In particural,
Gerrit's `runsvdir` does not have the `-s` option:
that functionality is provided by `runit-init`.
I put my runit service directory in `/service`.
Everything else seems to be making top-level directories these days,
and, hell, it's my computer.
You can put yours wherever you want,
just change `/service` in my examples to your directory.
Ctrl-alt-del does an immediate reboot.
Once I'm more comfortable with this setup,
I may change that by writing to the approprate file in `/proc`,
but I actually like this behavior for now.
Warning
-------
If you screw this up,
you might not be able to boot your computer up anymore.
If you're using Arch,
presumably you're already comfortable administering your computer.
But messing with `init` can break things in exciting new ways.
Arch's initrd has a nice `break=postmount` kernel commandline option
to open a shell after mounting root,
which I used to recover things several times.
You might want to play with that and understand what the initrd is,
before you are put into a position where you *have* to use it and can't start a web browser.
Let's go
--------
The version of `busybox` packaged for Arch comes with many "applets" compiled in:
enough for us to set it all up.
Because it's statically linked,
we don't even need to worry about libc updates.
To prevent a bad `busybox` update from bringing down the entire works,
let's make a copy.
cp /bin/busybox /usr/local/sbin/busybox.static
ln -s busybox.static /usr/local/sbin/runsv
ln -s busybox.static /usr/local/sbin/runsvdir
ln -s busybox.static /usr/local/sbin/sv
We also need to create a new `/sbin/init` to replace `systemd` (or sysvinit) and launch `runsvdir`.
Arch actually has a pretty nice init setup,
almost as though they had this use case in mind when they were designing it.
The early userspace init sets up /, pivots root, and runs /sbin/init.
At that point, we can take over, run `/etc/rc.sysinit`,
and hand off to `runsvdir`.
Putting the system initialization stuff into a shell script
was a nice move on the part of the arch folks,
and makes this almost trivial.
The other thing `init` needs to handle is being called by programs like `reboot` and `poweroff`,
which want to signal `init` by changing runlevel.
So if our new `init` is not PID 1,
we'll emulate `telinit` from sysvinit,
by checking what runlevel is being requested and sending the appropriate
signal to PID 1.
Be sure to move the old `init` to soming like `init.sysv`,
then create a new `init` similar to this
(don't forget to `chmod +x`):
#! /bin/sh
PATH=/usr/local/sbin:/usr/sbin:/sbin:/usr/local/bin:/usr/bin:/bin
export PATH
if [ $$ -ne 1 ]; then
case $1 in
6)
exec kill -15 1
;;
0)
exec kill -12 1
;;
esac
echo "LOL: runit doesn't have run levels" 1>&2
exit 1
fi
# Run arch's sysinit
if ! /etc/rc.sysinit; then
# Kludge it if there's no rc.sysinit
mount -t proc proc /proc -o nosuid,noexec,nodev
mount -t sysfs sys /sys -o nosuid,noexec,nodev
mount -t tmpfs run /run -o mode=0755,nosuid,nodev
mount -t devtmpfs dev /dev -o mode=0755,nosuid
mkdir -p /dev/{pts,shm}
mount -t devpts devpts /dev/pts -o mode=0620,gid=5,nosuid,noexec
mount -t tmpfs shm /dev/shm -o mode=1777,nosuid,nodev
# This doesn't ever run fsck :<
mount -o remount,rw /
: < /etc/hostname > /proc/sys/kernel/hostname
hwclock --systz
# Start/trigger udev, load MODULES, and settle udev
udevd_modprobe sysinit
fi
if grep -q 'break=init' /proc/cmdline; then
echo 'Breaking before init, type "exit" to continue booting'
/bin/sh
fi
# XXX: Who creates this?
rm /run/nologin
# Hand off to runit
exec runsvdir -P -s runit-signal /service
This will still run `udev` and `bootlogd` from `/etc/rc.sysinit`.
I tried to set up `mdev` from busybox as a `udev` replacement,
but Xorg wants `udev`,
and I was having other problems getting drivers loaded,
so I'm just trusting the the Arch devs here.
If you can figure out another way,
please email me about it, I'd love to know.
You may also want to install the `dash` package,
and link `/bin/sh` to that,
if you worry (as I do) about libc breaking things
(`dash` is statically linked).
runit-signal
------------
Busybox's version of `runsvdir` has a `-s` option.
When runsvdir gets a signal,
it runs whatever was provided to `-s` with the signal number as the first argument.
Here's my `/usr/local/sbin/runit-signal`
(make sure you `chmod +x`):
#! /bin/sh
##
## Signal handler for runit
##
if [ $PPID != 1 ]; then
echo "This program should only be invoked by PID 1."
# The reason is that killall5 won't kill anything in the same
# process group. That means it won't kill your invoking shell,
# getty, or svrun. That in turn prevents filesystems from
# unmounting, or even being remounted ro, since svrun (at least) has
# a FIFO open for writes. And if we reboot without unmounting
# filesystems, that's bad.
echo "Feel free to read $0 to learn why :)"
exit 1
fi
waitall () {
for i in $(seq 50); do
# If all processes are in group 0, we're done
awk '($5){exit 1;}' /proc/[0-9]*/stat && return 0
usleep 200000
done
return 1
}
cleanup () {
echo "Stopping services..."
sv stop /service/*
echo "Asking processes to exit..."
killall5 -1
killall5 -15
if waitall; then
echo "Forcing processes to exit..."
killall5 -9
waitall
fi
echo "Unmounting file systems..."
umount -a -r
# Sometimes when we reach here we still haven't been able to umount
# everything. Not much more we can do about that, other than flush
# write buffers and hope for the best.
sync
}
case $1 in
1) # SIGHUP
;;
15) # SIGTERM: reboot
cleanup
echo "Rebooting..."
reboot -f
;;
10) # SIGUSR1: halt
cleanup
echo "Halting..."
halt -f
;;
12) # SIGUSR2: power
cleanup
echo "Shutting down..."
poweroff -f
;;
*) # Everything else
;;
esac
Create a getty
--------------
Before we reboot, we need to make sure to create a way to log in.
The following in `/service/tty2/run` will start a getty on the second virtual console
(don't forget to `chmod +x`):
#! /bin/sh
pwd=$(pwd)
TTY=${pwd##*/}
exec agetty $TTY
You can make more than one getty by copying this to `/service/tty3/run` and so on.
Reboot!
-------
Double check you did it all right:
* You understand how to use the initrd shell you get passing the `break=postmount` boot argument
* `/sbin/init` should be an executable shell script
* At least a getty service will start from the `service` directory you set up
That's not a big checklist.
Ready to go?
Tell the currently-running init to reboot:
/sbin/init.sysv 6
Cross your fingers!
If it doesn't work
------------------
You can always move `/sbin/init.sysv` back to `/sbin/init` and reboot
into your old setup.
Nothing in this page will destroy the old bootup process
(other than renaming `init`, of course).
If it does work
---------------
Congratulations, you're now using runit.
You now need to write startup scripts for things you like to run,
like `dhcpcd`, `ntpd`, maybe `xdm`.
You're an Arch Linux sysadmin,
you should know what you need,
and I can't help you past here.
Getting rid of `systemd`
------------------------
Some readers might be disappointed that I didn't explain how to get rid of `systemd`.
Sorry, guys.
X11 requires `udevd`.
Until that dependency goes away,
or until Wayland is usable,
I don't see any way around having systemd installed.
When it *is* possbile to remove systemd,
you might be able to use the `mdev` program in busybox to replace `udevd`.
I played with this a little bit,
and it looked promising,
but I gave up when I saw that I wasn't going to get to run X.
Have fun!