zurück zum Artikel

Schneller booten mit Upstart

| Mirko Dölle

Ein Großteil der Bootzeit heutiger Linux-Systeme geht für die Systeminitialisierung und den nicht-parallelisierten Start Dutzender Daemons drauf. In Ubuntu 9.10 haben die Entwickler begonnen, den starren Startvorgang durch den massiven Einsatz von Upstart zu parallelisieren und zu beschleunigen.

Aktualisierte Version des Artikels "Blitzstarter, Upstart als Alternative zu SysV-Init unter Linux" aus c't [1] 9/09, S. 176

Die Ladezeit des Linux-Kernels macht nur noch einen kleinen Teil der Zeit aus, die man bei jedem Systemstart auf den Login-Prompt wartet. Die meiste Zeit wartet der Rechner darauf, dass das von Unix System V abstammende init durch die verschiedenen Runlevel wechselt und dabei unzählige Init-Skripte nacheinander abarbeitet. Bei Ubuntu und Fedora hat Upstart schon länger das traditionelle SysV-Init abgelöst.

Daran, dass die Dienste nacheinander in fester Reihenfolge starten, hat sich bislang jedoch nichts geändert – es hatte einfach noch niemand damit begonnen, die Init-Skripte der Dienste an die Möglichkeiten von Upstart anzupassen. Stattdessen emulierte man nur die Runlevel von SysV-Init (die es auf Systemen mit Upstart eigentlich gar nicht mehr gibt) und rief weiterhin die alten Init-Skripte auf. Bei Ubuntu 9.10 haben die Entwickler endlich einen Teil der Dienste auf Upstart umgestellt.

Sowohl Upstart als auch SysV-Init werden vom Kernel als erster Prozess mit der ID 1 gestartet, sobald dieser gebootet und etwaige Boot-Skripte aus der Initial Ramdisk (Initrd) abgearbeitet hat. Bei SysV-Init ist die Datei /etc/inittab der Dreh- und Angelpunkt für die Systeminitialisierung. Hier findet SysV-Init den Default-Runlevel, den Namen des ersten Initialisierungs-Skripts sowie die Kommandos zur Initialisierung der jeweiligen Runlevel. Bei der Initialisierung der Runlevel werden dann die im jeweiligen Runlevel-Verzeichnis (etwa /etc/rc5.d) verlinkten Init-Skripte nacheinander gestartet.

Dabei müssen sich alle Dienste als Daemon in den Hintergrund legen und von der Konsole abkoppeln, weil ansonsten das Init-Skript blockiert würde, bis der Dienst beendet ist. Durch die Abkoppelung kann Init nur mit erheblichem Aufwand feststellen, ob ein Dienst läuft oder sich beendet hat – meist, indem der Daemon eine Datei mit seiner Prozess-ID (PID) in /var/run hinterlegt, wobei es dem Init-Skript obliegt, festzustellen, ob die genannte PID überhaupt zum gewünschten Daemon gehört. Eines der letzten Init-Skripte startet die grafische Oberfläche. Hat SysV-Init alle Skripte ausgeführt, werden schließlich die in der Datei /etc/inittab aufgeführten Dienste wie Login-Konsolen gestartet und überwacht.

Upstart hingegen arbeitet Event-orientiert mit sogenannten Jobs, wobei jede Job-Datei im Verzeichnis /etc/init für den Start eines Dienstes oder einen bestimmten Teil der Systeminitialisierung zuständig ist. Eine feste Reihenfolge gibt es nicht, stattdessen gibt jeder Job an, auf welche Events er reagieren möchte. Tritt ein Event auf, startet Upstart parallel alle Jobs, die auf dieses Event gewartet haben.

Das erste Event, startup, erzeugt Upstart beim Aufruf automatisch selbst. Zudem erzeugen Start und Ende eines jeden Jobs weitere Events, nämlich started Jobname und stopped Jobname. Für das Event startup interessieren sich unter Ubuntu 9.10 diverse Jobs, darunter hostname zur Einrichtung des Rechnernamens; die zugehörige Job-Datei ist /etc/init/hostname.conf. Das nachfolgende Beispiel zeigt eine stark vereinfachte Variante des Jobs:

start on startup
task
exec hostname -b -F /etc/hostname

Das Schlüsselwort start on benennt das Event, bei dem dieser Job laufen soll. Gibt es mehrere Events, auf die der Job reagieren soll, so müssen diese Events logisch miteinander verknüpft werden:

start on (runlevel [016]
and (stopped gdm
or stopped kdm
or stopped xdm))

Anders als noch in Ubunut 9.04 darf start on nicht mehrfach vorkommen. Muss der Job zu einem späteren Zeitpunkt wieder beendet werden, definiert man per stop on zusätzliche Stop-Events, bei denen der Job wiederum abgearbeitet wird.

Die Upstart-Jobs von Ubuntu 9.10

Will man einen Job außer der Reihe starten oder anhalten, so kann man dies per

initctl start Jobname

respektive

initctl stop Jobname

tun. Welches Programm der Job aufruft, steht hinter exec. Ein großer Unterschied zwischen Upstart und SysV-Init ist, dass in den Init-Skripten Dienste immer im Hintergrund starten, weil sie sonst Init blockieren. Upstart hingegen erwartet, dass der hinter exec genannte Prozess im Vordergrund läuft – denn nur so lange dieser Prozess läuft, betrachtet Upstart den Job als laufend (running). Endet ein mit exec gestarteter Prozess, so endet für Upstart auch der Job und wartet darauf, dass wieder ein passendes Event auftritt (waiting). Dabei merkt sich Upstart den Zustand jedes Jobs, der in /etc/init gelistet ist. Diese Informationen können Sie mit den Befehlen initctl list und initctl status Jobname abrufen.

Das Event-gesteuerte Konzept von Upstart unterscheidet sich grundlegend von dem von SysV-Init, wo die Init-Skripte stur der lexikalischen Reihenfolge im jeweiligen Runlevel-Verzeichnis nach aufgerufen werden. Das macht Upstart sehr viel flexibler: Besteht zum Beispiel beim Start des Mail-Daemons (MTA) noch keine Netzwerkverbindung, muss man bei SysV-Init den Timeout abwarten, bevor das System weiter bootet. Bei Upstart hingegen würde der MTA erst dann gestartet, wenn die Netzwerkverbindung steht: Als Start-Event würde beim MTA-Job das Event network up konfiguriert, das der für die Netzwerkeinrichtung zuständige Dienst erst nach erfolgreicher Netzwerkkonfiguration auslöst – auf einem Notebook unterwegs im Zweifel also gar nicht.

Dadurch, dass es keine feste Startreihenfolge der Jobs gibt, kann ein System mit Upstart schneller booten als eines mit SysV-Init. Hinzu kommt, dass durchaus mehrere Jobs gleichzeitig abgearbeitet, also Initialisierungsaufgaben parallelisiert werden – auch dadurch lässt sich potenziell Zeit sparen.

Eine große Neuerung von Ubuntu 9.10 ist, dass Upstart mit Leben erfüllt und etliche Systemeinstellungen und Daemon-Aufrufe auf Upstart umgestellt wurden, darunter das Einbinden der Laufwerke, der Start von Udev und Cron sowie der grafischen Oberfläche. Allerdings ist man noch nicht so weit, gänzlich auf die alten Init-Skripte verzichten zu können, sodass es weiterhin den Wrapper-Job rc gibt, der die SysV-Init-Runlevel nachbildet und die unter /etc/init.d und /etc/rc?.d verlinkten Init-Skripte aufruft.

Kurz nachdem der Kernel gestartet und die ersten Pseudo-Dateisysteme eingebunden wurden, startet der Job /etc/init/rc-sysinit.conf. Das Start-Event des Jobs ist filesystem und seine Aufgabe ist es, das Event runlevel S zu erzeugen und somit das Äquivalent zu Runlevel S zu setzen. Anschließend läuft der Job /etc/init/rcS.conf an, der die symbolischen Links in /etc/rcS.d abarbeitet und so die Grundkonfiguration des Systems übernimmt. Anschließend wird das Signal für Runlevel 2 erzeugt, womit weitere Upstart-Jobs gekoppelt sind und die in /etc/rc2.d verlinkten Init-Skripte ausgeführt werden.

Auch die Datei /etc/inittab, die bei Systemen mit SysV-Init unter anderem den Start der Login-Konsolen zum Abschluss der Initialisierung erledigt, wurde durch mehrere Upstart-Jobs ersetzt, hier ein Auszug:

start on stopped rc RUNLEVEL=[2345]
stop on runlevel [!2345]
respawn
exec /sbin/getty -8 38400 tty1

Da der Job rc, der die Runlevel emuliert, nach dem Aufruf der Init-Skripte des jeweiligen Runlevels terminiert und nicht ständig während des emulierten Runlevels läuft, ist das Startsignal für die tty-Jobs das Beenden des rc-Jobs und nicht etwa dessen Start. Zudem sollen die Login-Konsolen beim Herunterfahren des Systems (Runlevel 0 und 6) sowie im Single-User-Modus abgeschaltet werden. Das Programm Getty selbst läuft im Vordergrund und wird, da mit dem Schlüsselwort exec aufgerufen, von Upstart überwacht. Mit dem zusätzlichen Schlüsselwort respawn wird Upstart angewiesen, den Prozess immer wieder neu zu starten, wenn er sich beendet – auf diese Weise erhält man nach dem Ausloggen wieder einen neuen Login-Prompt.

Um eine Überlastung des Systems zu verhindern, wenn ein Prozess ständig neu startet, lässt sich begrenzen, wie oft Upstart über welchen Zeitraum versuchen soll, den Dienst zu starten. Fedora 10 nutzt dieses Feature im Job prefdm zum Start der grafischen Oberfläche:

start on stopped rc5
stop on runlevel [!5]
respawn
respawn limit 10 120
exec /etc/X11/prefdm --nodaemon

Das Limit für den Neustart liegt bei 10 Versuchen innerhalb von 120 Sekunden, wobei sowohl die Angabe von respawn als auch von respawn limit erforderlich ist – respawn limit allein bewirkt noch keinen Neustart des Dienstes.

Beim Anhalten eines Jobs kümmert sich Upstart lediglich um den per exec im Vordergrund gestarteten Prozess: Er sendet ihm das Terminate-Signal (SIGTERM) und erwartet, dass er sich selbst beendet. Widerspruch duldet Upstart nicht – beendet sich der Prozess nicht, wird er wenige Sekunden später mittels Kill-Signal (SIGKILL) hart abgebrochen. Ein einmal ausgelöstes Stop-Event kann ein Dienst also weder blockieren noch verzögern oder gar rückgängig machen.

Mit Hilfe der Schlüsselwörter pre-stop und post-stop lassen sich Befehle angeben, die Upstart vor und nach dem Beenden des Dienstes ausführen soll. Dies ist für etwaige Aufräumarbeiten interessant. Hier ein Beispiel:

post-stop exec rm -f /var/run/tserv.pid
Zeitlicher Ablauf eines Upstart-Jobs

Zeitlicher Ablauf eines Upstart-Jobs

Zusätzlich gibt es noch die Schlüsselwörter pre-start und post-start, mit denen Befehle unmittelbar vor und nach dem Start eines Dienstes ausgeführt werden können, etwa um notwendige Verzeichnisse anzulegen oder nach dem Start des Dienstes bestimmte Systemeinstellungen anzupassen. Da exec erwartet, dass der Dienst im Vordergrund startet, kann Upstart mit der Ausführung von post-start nicht warten, bis der Dienst beendet wurde. Deshalb führt Upstart post-start parallel mit dem Start des Dienstes aus. Die nebenstehende Abbildung veranschaulicht den zeitlichen Ablauf beim Start und Ende eines Upstart-Jobs.

In den bisherigen Beispielen wurden Befehle stets unmittelbar per exec aufgerufen. Anstelle von exec kann jedoch auch ein Befehlsblock gesetzt werden, der von den Schlüsselwörtern script und end script eingeschlossen wird:

pre-start script
if [ ! -e /var/run/tserv ]; then
mkdir -p /var/run/tserv
fi
end script

Ersetzt man den exec-Aufruf für den Start des Dienstprogramms durch einen Befehlsblock, so muss man beachten, dass Befehle, die hinter dem Aufruf des Dienstes stehen, nur dann ausgeführt werden, wenn sich der Dienst unmittelbar beim Aufruf beendet. Wird der Dienst später mittels Terminate-Signal von Upstart regulär beendet, geschieht dies nicht.

Durch das Event-Konzept empfiehlt sich Upstart besonders für Dienste, die in Abhängigkeit von anderen äußeren Einflüssen gesteuert werden sollen. Ein Beispiel dafür ist das Programm VDR, das den Rechner zum Festplattenrecorder macht.

Bei Desktop-PCs kann man getrost davon ausgehen, dass eine einmal eingebaute DVB-Empfangskarte ständig präsent ist. Bei Notebooks hingegen, die man mit einem USB-Empfänger unterwegs zusätzlich als DVB-T-Fernseher nutzen möchte, ist dies nicht immer der Fall. Sinnvoll ist es hier, VDR nur dann zu starten, wenn der DVB-T-Empfänger auch angeschlossen ist. Dazu ist jedoch eine Umstellung des Init-Skripts auf Upstart erforderlich.

Damit das zum VDR-Paket gehörige Init-Skript zukünftig nicht mehr dazwischen funkt, muss es deaktiviert werden. Dies kann man unter Ubuntu vorübergehend, bis zum nächsten Upgrade des VDR-Pakets, per update-rc.d erledigen:

update-rc.d -f vdr remove

Damit Upstart überhaupt mitbekommt, wann ein DVB-Empfänger angeschlossen wird, muss eine Udev-Regeldatei (siehe Link) unter /etc/udev/rules.d hinzugefügt werden, die ein Upstart-Event auslöst:

SUBSYSTEM=="dvb", SUBSYSTEMS=="usb", ACTION=="add", \
KERNEL=="dvb*.dvr0", RUN+="/sbin/initctl \
--quiet emit --no-wait -e UDEV_KERNEL=$kernel \
-e UDEV_DEVPATH=$devpath dvb-device-add"

Diese Udev-Regel trifft nur auf DVB-Geräte zu, die per USB angeschlossen werden und ein DVB-Ausgabegerät mit der Kernel-Bezeichnung dvbX.dvr0 anlegen. Meldet der Kernel ein entsprechendes Gerät, erzeugt Udev per initctl emit das Upstart-Event dvb-device-add. Der Parameter --quiet weist initctl an, auf die üblichen Statusmeldungen zu verzichten.

Alle weiteren Parameter betreffen den Initctl-Befehl emit: So blockiert Initctl normalerweise, bis das ausgelöste Event vollständig verarbeitet wurde – was bei einem Dienst bedeutet, dass der Initctl-Aufruf erst wieder zurückkehrt, wenn der Dienst wieder beendet wurde. Dies wird mit dem Parameter --no-wait verhindert: Initctl beendet sich sofort, nachdem das Upstart-Event abgesetzt wurde.

Der Parameter -e erlaubt es, Umgebungsvariablen an den Upstart-Job zu übermitteln – in diesem Fall sind es die Variablen UDEV_KERNEL mit dem Kernel-Namen des Geräts und UDEV_DEVPATH mit dem Pfad zum Gerätebaum unterhalb von SysFS.

Um das Event dvb-device-add kümmert sich der Upstart-Job dvb (siehe Listing am Ende des Artikels). Seine Aufgabe ist es, für jedes DVB-Device im Verzeichnis /var/run/dvb eine Datei anzulegen, in der der SysFS-Pfad zum Gerätebaum gespeichert wird. Auf diese Weise lässt sich später zurückverfolgen, welches DVB-Device zu welchem USB-Gerät gehört. Anschließend löst der Job das Upstart-Signal vdr-start aus.

Der Upstart-Job vdr zum Aufruf von VDR ist trivial, als Events erwartet er vdr-start und vdr-stop. Zudem soll VDR nur während der Runlevel 2 bis 5 laufen:

start on vdr-start
stop on (vdr-stop
or runlevel [!2345])
exec /usr/sbin/vdr-upstart

Das Skript vdr-upstart erledigt die Hauptarbeit für den Aufruf von VDR: Es prüft, ob VDR in der Datei /etc/defaults/vdr überhaupt aktiviert wurde, bindet diverse Konfigurationsdateien ein und ruft dann das Start-Skript runvdr im Vordergrund auf. Ausgangsbasis für vdr-upstart war das Init-Skript aus dem VDR-Paket.

Durch Einfügen der Udev-Regel sowie der beiden Upstart-Jobs ist nun sichergestellt, dass VDR nur dann startet, wenn auch mindestens ein DVB-Gerät angeschlossen wurde. Schließt man mehrere an, so macht dies nichts: Der dvb-Job endet stets, nachdem er die Datei mit dem SysFS-Pfad unter /var/run/dvb angelegt und das Upstart-Event vdr-start ausgelöst hat, und wird für jedes weitere Gerät erneut abgearbeitet.

VDR hingegen läuft im Vordergrund, der Job wird von initctl list daher mit dem Status "running" geführt. Somit ignoriert Upstart das Start-Signal vdr-start, sodass VDR bei mehreren DVB-Empfängern nicht mehrfach gestartet wird.

Deutlich komplexer als das Starten von VDR ist das Beenden: Sinnvollerweise sollte VDR erst dann abgeschaltet werden, wenn auch der letzte DVB-Empfänger entfernt wurde. Solange VDR jedoch läuft, hält das Programm die DVB-Devices unter /dev/dvb geöffnet – weshalb sie der Kernel selbst dann nicht entfernt, wenn der USB-Empfänger längst herausgezogen wurde, es sich also nur noch um Phantome handelt. Es gibt dementsprechend auch kein Udev-Event, dass ein DVB-Gerät entfernt wurde, solange VDR läuft. Hinzu kommt, dass der SysFS-Baum des Geräts seit Kernel 2.6.29 ebenfalls erst dann abgeräumt wird, wenn das letzte Device geschlossen wurde. Es ist also ein wenig Heuristik nötig, um bei laufendem VDR zu erfahren, dass der DVB-Empfänger bereits eingepackt wurde.

Die Lösung ist, per Udev sämtliche Events zu beobachten, die entfernte USB-Geräte betreffen:

SUBSYSTEMS=="usb", ACTION=="remove", \
RUN+="/sbin/initctl --quiet emit --no-wait \
-e UDEV_DEVPATH=$devpath device-remove"

Der Upstart-Job dvb reagiert auch auf das Event device-remove und vergleicht den Device-Path des gerade entfernten USB-Geräts mit den Gerätepfaden der USB-DVB-Empfänger, die im Verzeichnis /var/run/dvb abgelegt sind. Stimmt der Basispfad überein, geht der Job davon aus, dass der betreffende DVB-Empfänger entfernt wurde, und löscht die zugehörige Datei in /var/run/dvb. Erst wenn der letzte DVB-Empfänger entfernt wurde, löst der dvb-Job das Upstart-Event vdr-stop aus – woraufhin VDR beendet wird und Udev die DVB-Geräteeinträge unterhalb von /dev/dvb abräumt.

Das Beispiel zeigt, welche Flexibilität sich durch den Einsatz von Upstart erreichen lässt – aber auch, wie kompliziert eine sinnvolle Umstellung der althergebrachten Init-Skripte auf das Upstart-Konzept ist. Bis auch das letzte SysV-Init-Skript auf Upstart migriert wurde, dürfte es daher noch eine ganze Weile dauern. (mid [2])

Mehr Infos

Upstart-Job dvb.conf

env RUNDIR=/var/run/dvb


start on (dvb-device-add
or device-remove)

emits vdr-start vdr-stop

script
case "$UPSTART_EVENT" in
dvb-device-add)
mkdir -p $RUNDIR
echo ${UDEV_DEVPATH%/dvb/${UDEV_KERNEL}} >\
$RUNDIR/$UDEV_KERNEL
/sbin/initctl --quiet emit --no-wait vdr-start
;;
device-remove)
if [ -d $RUNDIR ]; then
for d in $RUNDIR/*; do
if [ -f $d ]; then
read basedev < $d
if [ -z "$basedev" -o "${UDEV_DEVPATH#${basedev}}" != \
"${UDEV_DEVPATH}" ]; then
rm -f $d
fi
fi
done
rmdir --ignore-fail-on-non-empty $RUNDIR
if [ ! -d $RUNDIR ]; then
/sbin/initctl --quiet emit --no-wait vdr-stop
fi
fi
;;
esac
done
end script

(mid [3])


URL dieses Artikels:
https://www.heise.de/-844394

Links in diesem Artikel:
[1] http://www.ct.de
[2] mailto:mid@heise.de
[3] mailto:mid@ct.de