A daemon-writer’s guide to NetBSD’s rc.d system

750

Author: Peter Seebach

The NetBSD startup process is extensible, flexible, and a little
daunting at first. This article looks at the configuration mechanisms used
to enable or disable features, and compares NetBSD’s startup
procedures to those of other systems.

Introduction

A typical desktop Unix system may easily run dozens of processes,
most of which are started without user intervention. Disks are mounted,
log files created, and daemons started, and these things must be
done in a particular order; if you start a daemon that tries to write its
logs in /var/log before mounting the filesystem containing /var, prepare for failure.

In times of yore, vendors handled process startup simply by writing the startup script,
/etc/rc, correctly, doing things in the right order.
When users wanted to add services, vendors began to provide a second script, /etc/rc.local, which holds local additions
to be run only after /etc/rc completes.

Eventually, Unix split into the BSD and System V communities. System V
systems, such as AT&T’s SVR2 on the 3b1 and 3b2, Amiga Unix, and modern Solaris,
invented a whole family of scripts to start and stop system services, and
created links to them in special directories; the links had names that
indicated the order in which the scripts were to run. For instance,
a script called “S01disks” would run before a script called “S02daemons.”
Each directory corresponded to a “runlevel”; the system started in runlevel 1,
and worked its way up, so that runlevel 2 might be a normal multiuser
boot, and runlevel 3 a multiuser boot with network services started.

The BSD communities, starting with 386BSD and BSD/386, went with gradually more
elaborate variants on the rc.local script, with BSD/OS, for instance, having
scripts called “netstart,” “rc,” “rc.local,” and “rc.first.” The gradual
proliferation allowed the vendor to maintain a standard rc script, with most
user changes going in either rc.local (if they could run after the standard
system boot) or rc.first (if they had to run before it). However, more
elaborate configurations were difficult.

Configuration in /etc/rc.conf

In 1998 NetBSD 1.3 added a new file called rc.conf to set
variables that enabled or disabled common system services. In this file each
service could have two variables set. The first would be the name of a
service, and could be set to “yes” or “no,” indicating whether or not to
run the service. The second would be a set of
flags with which a service might be invoked. For instance, an NFS server
might have these lines in /etc/rc.conf:

	nfsd=yes
	nfsd_flags="-tun 4"

With the introduction of rc.conf each
actual rc file could almost always be left untouched, with changes made in
a configuration file controlling it, rather than in rc itself. Still, the
consensus among NetBSD developers was that
the existing rc scripts were not sufficiently scalable, and that the System V
mechanism had too many flaws and quirks.

After a number of debates, some some frustrated systems architects
sat down to design a new system that would address the concerns of
people opposed to adopting the System V init.d mechanism.
A detailed analysis of this system is beyond the scope of this document,
but it’s discussed in Luke Mewburn’s paper, “The Design and Implementation
of the NetBSD rc.d system
“. To make a long story short, the goal of the
rc.d system is to let each service have its own startup/shutdown script,
with as much code as possible shared, and some kind of robust ordering
mechanism.

With the new startup system, the rc.conf file is no longer a system
standard file. Instead, there’s a system-provided /etc/defaults/rc.conf file,
which provides system defaults, and an admin-provided /etc/rc.conf file,
which overrides the default settings. The system file can be upgraded
without affecting local settings.

The rc.d scripts are designed to work without a traditional BSD
/usr filesystem being mounted, and without requiring many commands to migrate
from /usr to the root filesystem, because NetBSD still actively supports
weird machines with tiny filesystems.

Understanding an rc.d script

Here’s
a script taken out of a NetBSD-current system. This script controls the
mouse daemon, which reads data from a serial mouse and passes it to the
system’s standard mouse interface.

	#!/bin/sh
	#
	# $NetBSD: moused,v 1.1 2001/10/29 23:25:01 augustss Exp $
	#

 	# PROVIDE: moused
	# REQUIRE: DAEMON

	. /etc/rc.subr

 	name="moused"
	rcvar=$name
	command="/usr/sbin/${name}"

 	load_rc_config $name
	run_rc_command "$1"

The first four lines do nothing particularly interesting. The next two
lines, ignored by /bin/sh, are used by the rcorder script (discussed
later in this article) to figure out
when a script can be run. The first real line of the script loads the
standard rc.subr functions, support routines designed for use by the programs
in rc.d. These functions use a handful of standard variables, such as
$name, $rcvar, and $command.
In this case, all of them end up getting set to the same value.

Next, the script calls two functions. load_rc_config loads any relevant configuration
files; this includes /etc/rc.conf, and (if it exists) /etc/rc.conf.d/$name.
run_rc_command actually starts the daemon.

There’s a lot of work happening behind the scenes in these functions. For instance, the
run_rc_command shell function will, if a file has been specified to hold the
process ID of a running daemon, check for an existing daemon. It also knows
about running daemons as specific users, at priorities other than the default,
and so on.

Similarly, the function can do more than just start daemons; it can stop them, restart
them, check on their status, or even wait for them to finish shutting down.

Scripts can be very complicated. The /etc/rc.d/network script
is nearly as long as this entire article, because it does all the network
startup work; it checks for IPV6 support, sets up NIS if needed, checks for
an address via DHCP if configured to, and so on.

Understanding rcorder

If you aren’t going to run files in lexicographical order, you need some way
to order them. Ideally, this would be designed in terms of dependencies, not
just an arbitrary order. Something such as “system files first” would be
unacceptable; users may need to run special services (such as a VPN
configuration script) before system services (such as an NFS mount).

The developers of the rc.d system decided to write
a new ordering program, called rcorder, which could have special
domain-specific knowledge about what it was doing.

The goal is to allow the writers of new services to impose exactly
the requirements of the services,
and no more, on the ordering of programs. If a function has to be started
after networking is up, but has no other requirements, there is no reason
to decide whether it runs before or after another program with the same
requirements. On the other hand, if a program must be started after the
general networking code is run, but before a specific network service is
run, it’s nice to be able to indicate that.

rcorder looks at a block of comments in the startup
shell scripts that contains lines using four keywords; “PROVIDES,”
“REQUIRES,” “BEFORE,” or “KEYWORD.” For instance, the rc.d script for altqd,
which changes the way network packets are routed, says:

	# PROVIDE: altqd
	# REQUIRE: mountcritremote
	# BEFORE:  SERVERS
	# KEYWORD: shutdown

The PROVIDE line tells rcorder what the name
of the service is. The REQUIRE line says what has to have been provided
already. In this case, it’s “mountcritremote,” which does just what the
name suggests — mounts any critical remote filesystems. The BEFORE line
says that, wherever this is ordered, it has to be before the SERVERS
dependency. The all-caps name on SERVERS is a convention used to indicate a
“dummy” dependency used only for ordering. The KEYWORD “shutdown” means
that the service wants to be actually stopped when the system is shutting
down, not just killed or ignored.

The rcorder script can select only files with a given keyword or skip all
files with a given keyword. For instance, on system shutdown, the system
shutdown script /etc/rc.shutdown builds a list of shutdown scripts to
run, using the -k option to rcorder as follows: files=$(rcorder -k shutdown ${rcshutdown_rcorder_flags} /etc/rc.d/*)
Files that don’t specify the shutdown flag are skipped. Other files are ordered
according to their dependencies.
The astute reader may be thinking that running shutdown scripts in dependency
order would be a bad idea; if you shut down NFS before you shut down things
which used it, you’ll have problems. The rc.shutdown script reverses the list
before running them, using a reverse_list shell function found in
/etc/rc.subr.

The BEFORE feature is what makes this scheme elegant. You don’t need to edit
the script for a given daemon to add a dependency to it; you just add it to
your BEFORE line. It’s sort of like a “come from.”

Designing an rc.d script

A well-designed script should fit into the existing framework easily. For
most scripts, it’s easy enough; put PROVIDES and the name of your new
service, and a REQUIRES tag with whatever you require; for instance,
NETWORKING if you need a network connection, or SERVERS if you’re dependent
on other servers.

A couple of keywords are noteworthy. The “nostart” keyword indicates that
a script should not be run automatically at system boot. This is not the
same as setting its rcflag to “no.” If you have set the corresponding flag
to “no,” the script will not start even if you try to start it from the
command line. If you set the flag to “yes” but give the script the
“nostart” keyword, it will be ignored by the standard system scripts, but
the command sh /etc/rc.d/foo start will start it. This might
be useful if you have a service you only want on some of the time,
but which is complicated to start.

The “shutdown” keyword indicates
that a script should be run with a “stop” argument during system shutdown.

Most ordinary daemons require very little special work. Those that do
can generally provide a start and a stop command via shell
variables named $start_cmd and $stop_cmd. A script that has nothing to do
on shutdown may set $stop_cmd to “:” (the shell builtin version of
/usr/bin/true). It often makes sense to provide small shell functions for
these features.

The best way to design your own script is to look at the existing ones in
/etc/rc.d and see which are most similar to what you’re doing. Play around
with it. The documentation is excellent; the man pages in question are
rcorder(8) and rc.subr(8).

Category:

  • BSD