Subject: A report on implementing runlevels in NetBSD
To: None <tech-userlevel@netbsd.org>
From: Giles Lean <giles@nemeton.com.au>
List: tech-userlevel
Date: 12/04/1999 11:23:15
Hi all,

Some time back in 1996 I added runlevels to NetBSD's init.  After
doing so I decided it wasn't a useful thing to have done.  With some
prompting from Luke Mewburn I have dug out my old notes and email and
am re-presenting my results in the hope that my investigation may be
useful in the current efforts to extend /etc/rc functionality.

To put the summary up front and get my personal biases somewhat out of
the way:

1. I recommend NetBSD adopt 'start' and 'stop' scripts, since they are
   very useful for manually starting and stopping services and
   integrate readily with the package system.

2. I am ambivalent about whether these scripts should be called
   directly from /etc/rc or from rc.sh and rcorder.

   I like the simplicity of /etc/rc.  I like the flexibility of easy
   programatic addition of new scripts that rc.sh and rcorder should
   provide.  I am dubious about providing two methods, since this
   seems more a political compromise than a technical advantage.

3. Runlevels in SysV are badly designed, incompatibly implemented and
   provide too few runlevels for very flexible usage.

4. Anyone looking at this topic I would encourage to evaluate the work
   Simon Gerraty has done, which used to be available at
   ftp.quick.com.au, but I can't seem to get there today.

   Simon's rc scripts have been used on a number of platforms,
   including on systems that provide runlevels.

Now, on with the show.

SysV init offers two facilities that BSD init doesn't:

1. runlevels
2. respawning of daemons

To take the second point first, this is trivially done via:

	#! /bin/sh

	while :
	do
		"$@"
	done

Fancier setups could be done with configurable timeouts, respawn
limits, up/down states, logging, whatever.  I can readily imagine a
'start' script settting this going, and a 'stop' script shutting it
down.  There is some prior art for this sort of thing outside of SysV
init.

This functionality can be put into init, but it doesn't have to be.

The second point, runlevels, are the more contentious.  I endeavour
not to be religious about the SysV/BSD/Linux thing.  (I'm paid to
support HP-UX. I can't afford to be religious; it's the oldest
BSD/SysV hybrid there is, and has diverged from both as well.)

When I added runlevels to NetBSD's init I found:

(a) I had to add a kernel variable to store the runlevel

    SysV uses utmp, but in single user mode we don't have a filesystem
    mounted read/write so that's not appropriate for us.  Adding a new
    kernel variable isn't a real hassle (we already have securelevel,
    for example) but it does mean that the kernel knows about
    runlevels at least minimally.  Numerous people felt that this was
    "unclean".  Maybe so.

    Linux I presume does do runlevels (since they have a SysVinit
    source package) but I don't know how it works.

(b) there are gross discrepancies in the current implementations

    At the time I did this work (1996) I investigated Solaris 2.5 and
    HP-UX 10.x.  I had some memories of SCO as well, but they're just
    about gone now.  (Both SCO and my memories. :-)

    (i)   the AT&T documentation is used by both Sun and HP, and a
	  naive reading suggests that runlevels are in fact levels,
	  with a defined ordering

    (ii)  an alternative school of thought says that the runlevels
          should be independent states, with no implied ordering
	  [more discussion later]

    (iii) nobody agrees about _which_ runlevels should be available,
          and several are taken and are not available

	  0           shutdown
	  1/S         single user mode	      
	  2	      former default multiuser (HP-UX 9.x, SCO?)
	  3	      current HP-UX default runlevel
	  4
	  5	      ?? reserved by Solaris for something (or is this 4?)
	  6	      reboot (some OSes only, definitely SCO)

          This suggests runlevels are a really scare resource: you get
          the default, a couple of others, and then you're into OS
          dependent territory again.

    (iv)  there is no standard numbering scheme used by the vendors
	  for startup and shutdown scripts, and no place for application
	  vendors to register to get numbers, so vendor names for
	  startup and shutdown scripts are probably OS specific

(c) some of the existing design is asinine

    When shutting down to a runlevel, it is defined that the kill
    scripts and then the startup scripts will be run at that level.

    Suppose I have defined:

    runlevel 2: networking up, no logins
    runlevel 3: networking up, network services (httpd) up, no logins
    runlevel 4: networking up, network service up, logins allowed

    When I transition from runlevel 4 to runlevel 3 the K* kill
    scripts for runlevel 4 are not run, the kill scripts in runlevel 3
    are run, then the S* start scripts in runlevel 3 are run.

    On the face of it this merely means that the kill scripts need to
    be one level lower; services that are started in runlevel 3 need
    to be shutdown in runlevel 2.  Tolerable, but confusing to many
    administrators.

    A bigger issue is that all start scripts have to be able to notice
    that they're being re-run, and not do damage.  In my example above
    the http daemon will already be running when the S* scripts for
    level.

    Minor nit (just thought of this one, brand new today :-) is that
    inetd has no knowledge of runlevels.  Altering /etc/inetd.conf
    under inetd and signaling it on a change of runlevels is clunky.
    (Sure, we can do atomic file updates to survive across crashes,
    but this is getting uglier ...)

Now to return to (b)(ii).  HP implemented runlevels.  When switching
from runlevel 4 to 2, the kill scripts are run for levels 3 and 2,
then the start scripts for level 2 are run.

Solaris is closer to run _states_ and in a transition from 4 to 2 will
only run the kill scripts in level 2, then the start scripts in level
2.  This turns out to be really awkward: I can't simply put the kill
scripts for a particular level one level down, since they won't always
be run.  Either I have to enforce an administrative policy that says
"never jump runlevels" or I have to put *all* the kill scripts for
every service in every lower level.

I do not recall whether Solaris runs all the S* scripts when jumping
runlevels upwards or not.  HP-UX will.

I also did not like the amount of code I had had to change in init to
make all this work, and scrapped the project.  The useful results
seemed to be:

- a minor bug in our init was found and fixed
- I had a good understanding of how useless SysV runlevels are in the
  real world

Regards,

Giles