Rauf und Runter (Linux-Magazin, Juni 2000)

Ob Apache oder Datenbank -- alle Dämonen müssen irgendwann rauf- oder runtergefahren werden. Das heute vorgestellte Skript startet Prozesse und schießt sie auf Anforderung ab.

apachectl, das Tool um den Apache zu starten, macht's so: Wird es mit einem Parameter start aufgerufen, fährt es den Webserver hoch und legt die PID des Hauptprozesses in einer Datei http.pid im log-Verzeichnis ab. Ruft man anschließend apachectl stop auf, wird in http.pid nachgesehen, welche Prozessnummer der Apache hat und ein kill-Befehl darauf angesetzt.

Ganz wie Apache

Das Skript in Listing pc (für Process Control) macht genau das selbe mit beliebigen Prozessen:

    pc start myproc

startet das Programm myproc als Hintergrundprozess und legt die Prozess-ID in myproc.pid ab. Ein anschließendes

    pc stop myproc

liest myproc.pid aus und sendet dem Prozess ein SIGTERM-Signal, das diesen beendet, falls er es nicht bewusst abfängt und ignoriert. Im Notfall lässt sich mit

    pc -s KILL stop myproc

auch das SIGKILL-Signal einstellen, das kein Prozess abfangen kann und das ihn unweigerlich vom Himmel holt. Wem die Geschwätzigkeit von pc zuviel wird, kann es mit der Option -q zum Schweigen bringen:

    pc -q start myproc

sagt nicht Starting myproc ... started, sondern gar nichts, falls alles glatt ging. Soll vor dem Starten in ein anderes Verzeichnis gewechselt werden, lässt sich das mit

    pc -d /my/directory start myproc

bewerkstelligen. Soll nur die Datei, in der die PID gespeichert wird, in einem anderen Verzeichnis landen, kommt die l-Option zum Einsatz:

    pc -l /tmp start myproc

legt myproc.pid in /tmp ab. Bei relativen Pfadangaben ist darauf zu achten, dass diese vom gegenwärtigen Verzeichnis aus gelten, oder von dem mittels der -d-Option aus festgelegten.

Das Skript pc überprüft genau, ob es auch mit dem start/stop-Befehl und mindestens einem Programmnamen aufgerufen wurde. Andernfalls bricht es ab und gibt die Aufrufsyntax aus:

    pc [-d dir] [-l logdir] [-s signal] [-q] start|stop proc

Der Aufruf des Programms, das pc starten soll, kann auch zusätzliche Parameter enthalten. So startet zum Beispiel

    pc start sleep 60

einen 60 Sekunden währenden sleep-Prozess im Hintergrund und legt die PID in der Datei sleep.pid ab. Den Reigen beendet

    pc stop sleep

(mit oder ohne Parameterangabe 60) abrupt wieder.

Wie pc funktioniert

Das Getopt::Std-Modul, das Zeile 7 in Listing pc hereinzieht, behandelt die Kommandozeilenoptionen und setzt entsprechende Einträge im Hash %opts. Der Befehl

    getopts('d:l:s:q', \%opts);

legt fest, dass die Optionen -d, -l und -s, falls sie gesetzt sind, jeweils ein Argument erwarten. -q steht für sich selbst, ist also ein boolean Flag. Wird pc mit

    pc -s KILL -q stop sleep

aufgerufen, ist $opts{s} auf "KILL" gesetzt und $opts{q} auf 1.

Zeile 10 definiert den Prototyp der usage-Funktion und legt fest, dass sie einen Skalar als Argument erwartet. Danach kann usage auch ohne Klammern aufgerufen werden, wie z.B. in Zeile 14. Dort wird überprüft, ob die Anzahl der Parameter, die übrigbleiben, nachdem getopt alle definierten Optionen bearbeitet und entfernt hat, zwei unterschreitet. In diesem Fall fehlt entweder eine Aktion oder das Skript, das pc starten oder stoppen soll. Zeile 15 prüft, ob start oder stop vergessen wurde.

Zeigt die -d-Option an, dass pc zunächst in ein bestimmtes Verzeichnis wechseln soll, führt Zeile 18 dies durch und bricht ab, falls die Aktion aus irgend welchen Gründen fehlschlägt. Wie alle Fehlermeldungen wird auch diese über die usage-Funktion mit einer kleinen Hilfsanweisung mit den gültigen Aufrufparametern von pc begleitet, bevor das Programm abbricht.

Zeile 21 definiert den Namen der Datei, in der die Prozess-ID eines gestarteten Programms abgelegt wird. Wurde mit -l ein anderes Verzeichnis spezifiziert, hängt Zeile 22 den Dateinamen hinter den angegebenen Pfad. Das Modul File::Spec tut dies betriebssystemunabhängig, unter Unix kommt dies auf's selbe heraus wie "$opt{l}/$pidf".

Die if-else-Logik ab Zeile 24 verzweigt zu start_proc oder stop_proc, je nachdem ob wir einen Prozess starten oder stoppen wollen. Im Startfall übergibt Zeile 25 der Funktion start_proc nicht nur die Pfadangabe zur PID-Datei, sondern fügt auch noch den Namen des zu startenden Prozesses mit allen angegebenen Parametern bei. @ARGV[1..$#ARGV] ist ein sogenanntes Array-Slice, das alle Argumente aus @ARGV enthält, außer dem ersten, das auf start oder stop gesetzt ist und beim eigentlichen Starten des Prozesses nichts verloren hat.

start_proc ab Zeile 31 entpuffert zunächst die Standard-Ausgabe, so dass es die mit print geschriebenen Meldungen auch dann gleich anzeigt, wenn noch kein Newline-Zeichen angegeben wurde. $| = 1 tut genau dies, und um die Einstellung nicht global durchzuführen, wird es mit local ausgezeichnet -- lexikalischen Scope mit my kann man leider nur mit ``normalen'' Variaben, $| ist ein Spezialteil.

Stellt Zeile 38 fest, dass eine PID-Datei schon existiert, bricht pc den Vorgang mit der Meldung "Already running" ab, denn es geht davon aus, dass der Prozess schon gestartet wurde und noch läuft und erst mit pc stop gestoppt werden muss.

Zeile 43 erzeugt mit fork einen Sohnprozess. Der Vater, der normal weiterläuft erhält in $pid die Prozess-ID des Sohnes zugewiesen, während im Sohn-Universum $pid auf 0 gesetzt ist.

Der Vater schreibt dann in Zeile 46 schnell die Sohn-PID in die PID-Datei und kehrt nach dem if-Block mit einer Meldung zurück, falls pc nicht gerade mit der -q-Option zum Schweigen verdonnert wurde.

Der Sohn überlädt in Zeile 51 mit exec den gegenwärtigen Prozess mit einem externen Programm, dessen Name und Kommandozeilenparamter in @proc liegen. exec ist ein schwarzes Loch, aus dem niemand jemals lebend zurückkehrt -- außer es ging etwas grob schief, wie wenn etwa der Prozess zum Überladen nicht gefunden wurde. In diesem Fall springt Zeile 52 ein und bricht mit einer Fehlermeldung ab.

stop_proc ab Zeile 59 versucht, einen laufenden Prozess durch das Senden eines Signals zu beenden. Hierzu versucht Zeile 66, die Prozess-ID aus der PID-Datei zu extrahieren. Existiert diese nicht, liegt der Schluß nahe, dass der Prozess nie mit pc gestartet wurde und Zeile 68 bricht den Vorgang ab.

Andernfalls sendet Zeile 74 dem Prozess mit dem kill-Signal das Signal Nummer 0 , was auf den Prozess keine Auswirkungen hat, aber schiefgeht, falls der Prozess nicht existiert. In diesem Fall denkt pc, der Prozess wäre schon gestoppt, löscht die PID-Datei und bricht rasch mit einer Fehlermeldung ab.

Geht Zeile 74 gut, existiert der Prozess mit der angegebenen PID und das Konstrukt zwischen 80 und 89 versucht zehn Mal im Sekundentakt, den Prozess mit einem SIGTERM-Signal zum Beenden zu überreden. Falls pc mit der Option -s und einem Signalnamen wie KILL, HUP, INT etc. aufgerufen wurde, nimmt der kill-Befehl in Zeile 81 diesen stattdessen. Alle verfügbaren Signal definiert die include-Datei /usr/include/asm/signal.h).

Zeile 82 prüft hinterher immer, ob der Prozess immer noch herumhängt, und falls ja, geht's in die nächste Runde. Nach zehn ist endgültig Schluss und pc gibt auf.

    pc -s KILL stop myapp

holt eine vorher gestartete Applikation myapp aber garantiert auf den Boden der Tatsachen zurück, denn das SIGKILL-Signal mit der Nummer 9 bedeutet das unweigerliche Ende, ohne dass der Prozess auch noch eine Chance zum Aufräumen hätte. Stellt pc fest, dass der Prozess gekillt wurde, löscht es in Zeile 84 die PID-Datei und beendet sich.

Die usage-Funktion ab Zeile 95 extrahiert aus $0, das den Programmpfad enthält (z. B. ./myapp) den Programmnamen (myapp) und gibt die unterstützten Optionen aus.

Startet fleissig und stoppt selten! Viel Spass damit!

Listing pc

    001 #!/usr/bin/perl
    002 ##################################################
    003 # pc [-d dir] [-l logdir] [-s signal] [-q] 
    004 #    start|stop proc
    005 ##################################################
    006 
    007 use Getopt::Std;
    008 use File::Spec;
    009 
    010 sub usage($);
    011 
    012 getopts('d:l:s:q', \%opts);
    013 
    014 usage "Wrong argument count" if @ARGV < 2;
    015 usage "No action" unless $ARGV[0] eq "start" ||
    016                          $ARGV[0] eq "stop";
    017 
    018 chdir($opts{d}) or usage "Cannot chdir to $opts{d}" if 
    019     exists $opts{d};
    020 
    021 my $pidf = "$ARGV[1].pid";
    022 $pidf = File::Spec->catfile($opts{l}, 
    023                             $pidf) if $opts{l};
    024 if($ARGV[0] eq "start") {
    025     start_proc($pidf, @ARGV[1..$#ARGV]);
    026 } else {
    027     stop_proc($pidf);
    028 }
    029 
    030 ##################################################
    031 sub start_proc {
    032 ##################################################
    033     my ($pidf, @proc) = @_;
    034     local $| = 1;
    035 
    036     print "Starting @proc ... " unless $opts{q};
    037 
    038     if(-f $pidf) {
    039         print "Already running\n";
    040         exit 0;
    041     }
    042 
    043     defined(my $pid = fork()) or die "Fork failed";
    044 
    045     if($pid != 0) {                      # Father
    046         open LOG, ">$pidf" or 
    047             usage "Cannot open $pidf";
    048         print LOG "$pid\n";
    049         close LOG;
    050     } else {                             # Son
    051         exec @proc;
    052         usage "Cannot start \"$proc[0]\"";
    053     }
    054 
    055     print "started\n" unless $opts{q};
    056 }
    057 
    058 ##################################################
    059 sub stop_proc {
    060 ##################################################
    061     my $pidf = shift;
    062     local $| = 1;
    063     
    064     print "Stopping ... " unless $opts{q};
    065 
    066     if(! open LOG, "<$pidf") {
    067         print "No instance running\n";
    068         exit 0;
    069     }
    070 
    071     my $pid = <LOG>;
    072     close LOG;
    073 
    074     if(! kill 0, $pid) {
    075         print "Already Stopped\n";
    076         unlink $pidf or die "Cannot unlink $pidf";
    077         return;
    078     }
    079 
    080     foreach (1..10) {
    081         kill $opts{s} || "TERM", $pid;
    082         if(! kill 0, $pid) {
    083             print "stopped\n" unless $opts{q};
    084             unlink $pidf or 
    085                          die "Cannot unlink $pidf";
    086             return;
    087         }
    088         sleep(1);
    089     }
    090     
    091     print "Can't stop it - giving up.\n";
    092 }
    093 
    094 ##################################################
    095 sub usage($) {
    096 ##################################################
    097     (my $prog = $0) =~ s#.*/##g;
    098     print "$prog: $_[0].\n";
    099     print "usage: $prog [-d dir] [-l logdir] " .
    100           "start|stop proc\n";
    101     exit 1;
    102 }

Michael Schilli

arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com.