26.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.
26.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 das Signal an alle Prozesse außer init geschickt, und der Wert -PID bezeichnet die Prozessgruppe des Prozesses mit der entsprechenden PID. Ein Beispiel: - sighandler_t signal(int signum, sighandler_t handler);
Diese Funktion dient 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.
Listing 26.14 Der Syscall kill
#include <sys/types.h>
#include <signal.h>
int main(int argc, char* argv[])
{
kill(1234, SIGTERM);
return 0;
}
Nach der Einbindung der entsprechenden Headerdateien wird dem Prozess mit der PID 1234 hier das SIGTERM-Signal geschickt.
Für den Anwender interessanter als die Frage nach den einzelnen Syscalls ist in den meisten Fällen die Frage nach den unterschiedlichen Signalen. Aus dem ersten Kapitel kennen Sie bereits verschiedene, mehr oder weniger gnadenlos zum Prozess- ende führende Signale: SIGKILL und SIGTERM. Doch zunächst soll kurz besprochen werden, wie man eigentlich Signale von der Kommandozeile senden kann.
26.3.2 Signale von der Kommandozeile senden: kill
Der Benutzer kann mit dem Kommando kill Signale an Prozesse versenden. Hierbei werden wie beim gleichnamigen Syscall der Signaltyp und die Prozess-ID des Zielprozesses beziehungsweise dessen Jobnummer angegeben:
Listing 26.15 Beispielaufruf des kill-Kommandos
$ kill 499
$ kill –9 500
$ kill -SIGKILL 501
[»]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, diese aber nicht zwingend
erwirkt – denn das Signal kann abgefangen werden.
26.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 durch SIGCONT fortgesetzt wird. - Signal 18, »SIGCONT« oder »CONT«
Dieses Signal setzt einen gestoppten Prozess fort.
Im Folgenden sollen abfangbare Signale erläutert werden. Die Liste ist nicht vollständig; es gibt sehr viel mehr als nur die hier genannten Signale. Die wichtigsten Signale können jedoch 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. [Fn. 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 Kapiteln zur Shell 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-Kommando unterstützten Signale können Sie durch den Aufruf von kill -l anzeigen lassen. Das Linux-kill-Kommando kennt darüber hinaus den -L-Parameter für eine tabellarische Ausgabe.
26.3.4 Rechte
Natürlich darf nicht jeder Benutzer fremden Prozessen einfach durch Signale mehr oder weniger unverblümt 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 – mit Ausnahme von root, der ja bekanntlich alles darf.
26.3.5 In der Praxis: Signale empfangen
Im Folgenden sollen noch einmal alle Fakten zu einem abschließenden Beispiel kombiniert werden. Dazu betrachten wir den folgenden Code, der ein Callback handler() zur Behandlung eines Signals über den Syscall signal() beim Kernel registriert:
Listing 26.16 Ein Callback für SIGALRM
#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;
}
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 dass man mit diesem Argument bei einem Handler für mehrere Signale recht einfach überprüfen kann, was genau man da gerade empfangen hat.
Trifft nun ein Signal ein, so 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:
Listing 26.17 Das Beispiel ausprobieren
$ gcc -o test test.c
$ ./test &
[1] 9172
$ kill -SIGALRM %1
$ Signal empfangen: 14
[1]+ Exit 1 ./test
Dabei wird der Sourcecode zuerst kompiliert und anschließend mit dem kaufmännischen »Und« (&) 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 das Programm die Meldung samt der Signalnummer auf der 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.
Ihr Kommentar
Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.