24.3 Prozesse und Signale
So wie der Prozessor Interrupts als Benachrichtigungen für bestimmte Ereignisse (wie den Ablauf eines Timers oder die Verfügbarkeit aus dem Speicher angeforderter Daten) behandelt, kann ein Prozess über Signale die verschiedensten Ereignisse abfangen.
24.3.1 Das Syscall-Interface
Das eigentliche Versenden und Empfangen von Signalen läuft über den Kernel. Die entsprechenden Schnittstellen sind dabei als Syscalls realisiert:
- int kill(pid_t pid, int signum);
- Mit dem kill-Syscall kann man Signale versenden. Das Signal selbst wird dabei intern nur über eine Nummer referenziert, wobei dem Programmierer beziehungsweise dem Benutzer in der Shell auch Signalnamen zur Verfügung stehen. Mit pid wird die PID des Prozesses bezeichnet, der das Signal empfangen soll. Wird hier jedoch 0 angegeben, so wird das Signal an alle Prozesse der eigenen Prozessgruppe gesendet. Bei –1 wird es an alle Prozesse außer init geschickt, und der Wert -PID bezeichnet die Prozessgruppe des Prozesses mit der entsprechenden PID. Ein Beispiel:
#include <sys/types.h> #include <signal.h> int main(int argc, char* argv[]) { kill(1234, SIGTERM); return 0; }Listing 24.14 kill
- Nach der Einbindung der entsprechenden Headerdateien wird dem Prozess mit der PID 1234 hier das SIGTERM-Signal geschickt.
- sighandler_t signal(int signum, sighandler_t handler);
- Diese Funktion dient nun dazu, eine Funktion – einen sogenannten Handler (auch Callback) -- festzulegen, die beim Empfang des entsprechenden Signals vom Kernel aufgerufen werden soll. Allerdings gibt es auch Signale, die aufgrund ihrer Semantik nicht abgefangen werden können, sondern die direkt vom Kernel bearbeitet werden.
Doch für den Anwender ist statt der Frage nach den einzelnen Syscalls die Frage nach den unterschiedlichen Signalen in der Regel interessanter. Dabei kennen Sie aus dem ersten Kapitel bereits verschiedene, mehr oder weniger gnadenlos zum Prozessende führende Signale: SIGKILL und SIGTERM. Doch zunächst soll kurz besprochen werden, wie man eigentlich Signale von der Kommandozeile senden kann.
24.3.2 Signale von der Kommandozeile senden: kill
Dem Benutzer steht mit dem Kommando kill die Möglichkeit zur Verfügung, Signale zu versenden.
Hierbei werden wie beim gleichnamigen Syscall der Signaltyp und die Prozess-ID des Zielprozesses beziehungsweise dessen Jobnummer angegeben:
$ kill 499 $ kill –9 500 $ kill -SIGKILL 501
Listing 24.15 Beispielaufruf des kill-Kommandos
Wird kill ohne einen Signalparameter und lediglich mit einer Prozess-ID aufgerufen, so wird das Signal SIGTERM an den Prozess gesendet, das ihn zur Beendigung auffordert, aber nicht zwingend dessen Beendigung erwirkt – denn das Signal kann abgefangen werden.
24.3.3 Welche Signale gibt es?
Es gibt also zwei Gruppen von Signalen: Eine Gruppe kann vom Prozess ignoriert beziehungsweise abgefangen werden, die andere nicht. Der Adressat dieser Signale ist viel eher der Kernel, der mit einer bestimmten Aktion gegenüber dem Empfängerprozess reagieren soll. Dies verdeutlichen die folgenden Beispiele:
- Signal 9, »SIGKILL« oder »KILL«
- Dieses Signal beendet einen Prozess zwingend durch den Kernel.
- Signal 19, »SIGSTOP« oder »STOP«
- Dieses Signal unterbricht die Verarbeitung eines Prozesses, bis er fortgesetzt wird.
- Signal 18, »SIGCONT« oder »CONT«
- Dieses Signal setzt einen gestoppten Prozess fort.
Im Folgenden sollen nun noch abfangbare Signale sowie ihre Bedeutung erklärt werden. Die Liste ist natürlich nicht vollständig, da es sehr viel mehr als nur die hier genannten Signale gibt. Die wichtigsten Signale können dabei wie folgt zusammengefasst werden:
- Signal 1, »SIGHUP« oder »HUP«
- Der Prozess soll sich selbst beenden und neu starten. Dieses Signal wird oftmals benutzt, um Dämonprozesse neu zu starten, damit diese ihre Konfigurationsdaten neu einlesen.
- Signal 14, »SIGALRM« oder »ALARM«
- Dieses Signal meldet den Ablauf eines Timers, den ein Programmierer mit dem Syscall alarm() starten kann.
- Signal 15, »SIGTERM« oder »TERM«
- Dieses Signal soll den Prozess dazu bewegen, sich freiwillig zu beenden. Wenn der Computer heruntergefahren wird, sendet der Kernel allen Prozessen solch ein Signal. Daraufhin haben die Prozesse einige Sekunden Zeit, sich zu beenden und beispielsweise Konfigurationsdaten zu speichern, bevor letztendlich das SIGKILL-Signal an alle Prozesse gesendet wird. <Hierbei sollten Sie beachten, dass nicht alle Prozesse auf das SIGTERM-Signal reagieren. Es liegt im Ermessen des Softwareentwicklers, ob eine entsprechende Signalbehandlungsroutine im Quellcode implementiert wird.>
Aus den Shell-Kapiteln wissen Sie bereits, dass einige Shells (z. B. die bash) ihre eigenen Implementierungen des kill-Kommandos als Builtin mitbringen. Diese Implementierungen bieten vereinzelt weitere Signaltypen. Die bash zum Beispiel unterstützt über 60 verschiedene Signale.
Eine Liste der von Ihrem kill unterstützten Signale bekommen Sie durch den Aufruf von kill -l. Das Linux-kill-Kommando kennt darüber hinaus den -L-Parameter für eine tabellarische Ausgabe.
24.3.4 Die Rechte
Natürlich darf nicht jeder Benutzer fremden Prozessen einfach durch Signale mehr oder weniger durch die Blume mitteilen, dass sie doch bitte die wertvolle Rechenzeit freigeben und sich lieber beenden sollen.
Dazu muss schon wenigstens die reale oder effektive Benutzer-ID des sendenden Prozesses mit der realen oder gespeicherten Benutzer-ID des Zielprozesses übereinstimmen.
Somit wird gewährleistet, dass ein Benutzer jeweils nur eigene Prozesse abschießen kann – außer natürlich, es handelt sich um root, der ja bekanntlich alles darf.
24.3.5 In der Praxis: Signale empfangen
Im Folgenden sollen noch einmal alle Fakten zu einem abschließenden Beispiel zusammengefügt werden. Dazu betrachten wir den folgenden Code, der ein Callback »handler()« zur Behandlung eines Signals über den Syscall signal() beim Kernel registriert:
#include <signal.h>
#include <stdio.h>
static int x = 0;
void handler(int i)
{
printf("Signal empfangen: %i", i);
x = 1;
return;
}
int main(int argc, char* argv[])
{
typedef void (*sighandler_t)(int);
signal(SIGALRM, &handler);
while(x == 0) {};
return 0;
}
Listing 24.16 Ein Callback für SIGALRM
Dem Syscall signal() übergibt man also das abzufangende Signal sowie die Adresse der Funktion, die das Signal behandeln soll. Diese Funktion darf nichts zurückgeben – sie ist vom Typ »void« – und bekommt als Argument die Nummer des empfangenen Signals übergeben. Das ist insofern sinnvoll, als man mit diesem Argument bei einem Handler für mehrere Signale recht einfach überprüfen kann, was man denn da gerade empfangen hat.
Trifft nun ein Signal ein, wird der Prozess vom Kernel nicht mehr an der alten Stelle – in diesem Fall in der leeren Schleife – fortgesetzt. Stattdessen wird die Funktion handler() aufgerufen, die die Variable x auf 1 setzt. Nach dem Ende der Funktion wird das Programm an der vorherigen Stelle fortgesetzt. Da das Programm in der Schleife unterbrochen wurde, wird es auch dort fortgesetzt – allerdings ist die Abbruchbedingung jetzt erfüllt, und der ganze Prozess kann beendet werden. Die Funktionalität kann man wie folgt testen:
$ gcc -o test test.c $ ./test & [1] 9172 $ kill -SIGALRM %1 $ Signal empfangen: 14 [1]+ Exit 1 ./test
Listing 24.17 Das Beispiel ausprobieren
Dabei wird der Sourcecode zuerst kompiliert und anschließend mit dem Ampersand (&) als Hintergrundprozess gestartet.
In diesem Augenblick durchläuft das Programm immer wieder die leere Schleife. Erst, nachdem wir diesem Job das SIGALRM-Signal geschickt haben, gibt es die Meldung samt der Signalnummer auf die Konsole aus und beendet sich dann, da die Variable x auf 1 gesetzt wurde und somit das Abbruchkriterium für die Schleife erfüllt ist.