In Teil 1 der Reihe „Linux-Signale“ haben wir die grundlegenden Konzepte hinter Linux-Signalen kennengelernt.
Aufbauend auf dem vorherigen Teil lernen wir in diesem Artikel, wie man Signale in einem Prozess abfängt. Wir werden die praktische Seite des Signalhandlings anhand von C-Programmcodeschnipseln darstellen.
Ein Signal empfangen
Wie bereits im vorherigen Artikel besprochen, muss der Prozess, wenn ein Prozess bestimmte Signale verarbeiten möchte, im Code eine Signalverarbeitungsfunktion beim Kernel registrieren.
Das Folgende ist der Prototyp einer Signalverarbeitungsfunktion:
void <signal handler func name> (int sig)
Die Signal-Handler-Funktion hat den Rückgabetyp void und akzeptiert eine Signalnummer, die dem zu verarbeitenden Signal entspricht.
Um die Signal-Handler-Funktion im Kernel zu registrieren, wird der Signal-Handler-Funktionszeiger als zweites Argument an die „Signal“-Funktion übergeben. Der Prototyp der Signalfunktion ist :
void (*signal(int signo, void (*func )(int)))(int);
Dies scheint eine komplizierte Erklärung zu sein. Wenn wir versuchen, es zu entschlüsseln:
- Die Funktion erfordert zwei Argumente.
- Das erste Argument ist eine Ganzzahl (signo), die die Signalnummer oder den Signalwert darstellt.
- Das zweite Argument ist ein Zeiger auf die Signal-Handler-Funktion, die eine Ganzzahl als Argument akzeptiert und nichts zurückgibt (void).
- Während die 'Signal'-Funktion selbst einen Funktionszeiger zurückgibt, dessen Rückgabetyp ungültig ist.
Nun, um die Dinge einfacher zu machen, verwenden wir typedef :
typedef void sigfunc(int)
Hier haben wir also einen neuen Typ ‚sigfunc‘ erstellt. Verwenden Sie nun diese Typdefinition, wenn wir den Prototyp des Signalhandlers neu entwerfen:
sigfunc *signal(int, sigfunc*);
Jetzt sehen wir, dass es einfacher zu verstehen ist, dass die Signal-Handler-Funktion eine ganze Zahl und einen Funktionszeiger vom Typ sigfunc akzeptiert, während sie einen Funktionszeiger vom Typ sigfunc zurückgibt.
Beispiel C Programm zum Empfangen eines Signals
Die meisten Linux-Benutzer verwenden die Tastenkombination Strg+C, um Prozesse unter Linux zu beenden.
Haben Sie schon einmal darüber nachgedacht, was sich dahinter verbirgt? Nun, immer wenn Strg+C gedrückt wird, wird ein Signal SIGINT an den Prozess gesendet. Die Standardaktion dieses Signals besteht darin, den Prozess zu beenden. Aber auch dieses Signal kann verarbeitet werden. Der folgende Code demonstriert dies:
#include<stdio.h> #include<signal.h> #include<unistd.h> void sig_handler(int signo) { if (signo == SIGINT) printf("received SIGINT\n"); } int main(void) { if (signal(SIGINT, sig_handler) == SIG_ERR) printf("\ncan't catch SIGINT\n"); // A long long wait so that we can easily issue a signal to this process while(1) sleep(1); return 0; }
Im obigen Code haben wir einen lang andauernden Prozess mit einer unendlichen While-Schleife simuliert.
Eine Funktion sig_handler wird als Signalhandler verwendet. Diese Funktion wird beim Kernel registriert, indem sie als zweites Argument des Systemaufrufs „signal“ in der main()-Funktion übergeben wird. Das erste Argument der Funktion „signal“ ist das Signal, das der Signal-Handler handhaben soll, in diesem Fall SIGINT.
Nebenbei bemerkt, die Verwendung der Funktion sleep(1) hat einen Grund. Diese Funktion wurde in der While-Schleife verwendet, damit die While-Schleife nach einiger Zeit ausgeführt wird (dh in diesem Fall eine Sekunde). Dies ist wichtig, da ansonsten eine unendliche While-Schleife, die wild läuft, den größten Teil der CPU verbrauchen und den Computer sehr, sehr langsam machen könnte.
Wie auch immer, kommen wir zurück, wenn der Prozess ausgeführt wird und wir versuchen, den Prozess mit Strg+C:
zu beenden$ ./sigfunc ^Creceived SIGINT ^Creceived SIGINT ^Creceived SIGINT ^Creceived SIGINT ^Creceived SIGINT ^Creceived SIGINT ^Creceived SIGINT
Wir sehen in der obigen Ausgabe, dass wir die Tastenkombination Strg + C mehrmals versucht haben, aber jedes Mal wurde der Prozess nicht beendet. Dies liegt daran, dass das Signal im Code behandelt wurde und dies durch den Druck bestätigt wurde, den wir auf jeder Zeile erhielten.
SIGKILL, SIGSTOP und benutzerdefinierte Signale
Abgesehen von der Behandlung der verfügbaren Standardsignale (wie INT, TERM usw.). Wir können auch benutzerdefinierte Signale haben, die gesendet und verarbeitet werden können. Es folgt der Code, der ein benutzerdefiniertes Signal USR1 behandelt:
#include<stdio.h> #include<signal.h> #include<unistd.h> void sig_handler(int signo) { if (signo == SIGUSR1) printf("received SIGUSR1\n"); else if (signo == SIGKILL) printf("received SIGKILL\n"); else if (signo == SIGSTOP) printf("received SIGSTOP\n"); } int main(void) { if (signal(SIGUSR1, sig_handler) == SIG_ERR) printf("\ncan't catch SIGUSR1\n"); if (signal(SIGKILL, sig_handler) == SIG_ERR) printf("\ncan't catch SIGKILL\n"); if (signal(SIGSTOP, sig_handler) == SIG_ERR) printf("\ncan't catch SIGSTOP\n"); // A long long wait so that we can easily issue a signal to this process while(1) sleep(1); return 0; }
Wir sehen, dass wir im obigen Code versucht haben, ein benutzerdefiniertes Signal USR1 zu verarbeiten. Außerdem wissen wir, dass zwei Signale KILL und STOP nicht behandelt werden können. Daher haben wir auch versucht, diese beiden Signale zu handhaben, um zu sehen, wie der Systemaufruf „Signal“ in diesem Fall reagiert.
Wenn wir den obigen Code ausführen:
$ ./sigfunc can't catch SIGKILL can't catch SIGSTOP
Die obige Ausgabe macht also deutlich, dass, sobald der Systemaufruf „signal“ versucht, einen Handler für KILL- und STOP-Signale zu registrieren, die Signalfunktion fehlschlägt, was darauf hinweist, dass diese beiden Signale nicht abgefangen werden können.
Nun versuchen wir mit dem kill-Befehl das Signal USR1 an diesen Prozess zu übergeben:
$ kill -USR1 2678
und auf dem Terminal, auf dem das obige Programm läuft, sehen wir :
$ ./sigfunc can't catch SIGKILL can't catch SIGSTOP received SIGUSR1
Wir sehen also, dass das benutzerdefinierte Signal USR1 im Prozess empfangen und ordnungsgemäß verarbeitet wurde.