Signale (<signal.h>)
Programme können durch ungültige Speicherzugriffe, Division durch Null oder Signale vom Betriebssystem unerwartet terminieren. C bietet mit der Signalbehandlung einen Mechanismus, um auf solche Ausnahmesituationen zu reagieren, bevor das Programm endet.

Signale und Handler registrieren (signal())
Ein Signal ist eine asynchrone Benachrichtigung an einen Prozess über ein besonderes Ereignis.
Ein Handler für ein Signal wird mit der Funktion signal() registriert
(Quelle: ISO/IEC 9899:2024, § 7.14.1.1 The signal function)
:
void (*signal(int sig, void (*handler)(int)))(int);
Der C-Standard definiert sechs obligatorische Signale, die als erstes Argument übergeben werden (Quelle: ISO/IEC 9899:2024, § 7.14 Signal handling <signal.h>) :
| Signal | Bedeutung |
|---|---|
SIGABRT |
Abnormale Programmbeendigung (z. B. durch Aufruf von abort()) |
SIGFPE |
Arithmetischer Fehler (z. B. Division durch Null) |
SIGILL |
Ungültige Prozessorinstruktion |
SIGINT |
Interrupt-Signal vom Terminal (typisch Strg+C) |
SIGSEGV |
Ungültiger Speicherzugriff |
SIGTERM |
Terminierungsanforderung an das Programm |
Das zweite Argument an signal() ist ein Zeiger auf eine Handler-Funktion (Callback): void handler(int sig);. Diese Funktion wird vom Laufzeitsystem aufgerufen, sobald das angegebene Signal empfangen wird. Der Parameter sig enthält die Signalnummer, die den Aufruf ausgelöst hat. Der Handler liefert keinen Rückgabewert (Typ void).
Der Rückgabewert von signal() ist ein Zeiger auf den zuvor registrierten Handler für dieses Signal oder SIG_ERR bei einem Fehler.
Das folgende Beispiel zeigt die Reaktion auf SIGINT (Strg+C):
#include <signal.h>
#include <stdio.h>
volatile sig_atomic_t got_int = 0;
static void sigint_handler(int sig) {
got_int = 1; // atomar und signal-sicher
}
int main() {
if (signal(SIGINT, sigint_handler) == SIG_ERR) {
perror("signal");
return 1;
}
puts("Programm läuft. Drücke Strg+C...");
for (int i = 0; i < 10 && !got_int; i++) {
printf("Iteration %d\n", i);
for (volatile long j = 0; j < 100000000L; j++) {}
}
if (got_int) {
puts("Signal SIGINT empfangen");
}
}
Im Beispiel wird die Variable volatile sig_atomic_t got_int als einziges Kommunikationsmittel zwischen Signal-Handler und Hauptprogramm verwendet. Es ist ein Status-Flag, das anzeigt, ob das Signal SIGINT (Strg+C) empfangen wurde. Zu Beginn steht es auf 0. Der Signal-Handler setzt es auf 1, sobald das Signal eintrifft. Der Handler macht sonst nichts und kehrt sofort zurück. Er führt keine Ausgaben aus und ruft keine Bibliotheksfunktionen auf.
Das Hauptprogramm prüft dieses Flag regelmäßig. Sobald got_int ungleich null ist, verlässt das Programm die Schleife und reagiert kontrolliert auf das Signal.
sig_atomic_t ist ein vom C-Standard definierter Ganzzahltyp, bei dem garantiert ist, dass Lesen und Schreiben atomar erfolgen.
(Quelle: ISO/IEC 9899:2024, § 7.14 Signal handling <signal.h>)
Der Zugriff kann also nicht mitten im Vorgang durch ein Signal unterbrochen werden. Genau das ist nötig, weil der Handler jederzeit in den laufenden Code springen kann. volatile verhindert, dass der Compiler Zugriffe auf got_int wegoptimiert oder zwischenspeichert.
Das Hauptprogramm muss den aktuellen Wert sehen, auch wenn er asynchron im Handler geändert wurde.
Best Practice im Umgang mit Signalen:
- Signal-Handler extrem kurz halten
- Nur
volatile sig_atomic_t-Variablen ändern- Im Handler keine Ein-/Ausgabe, keine Speicherverwaltung, keine komplexe Logik
- Die eigentliche Reaktion immer im normalen Programmablauf umsetzen
Der Handler meldet nur das Ereignis und das Hauptprogramm entscheidet, was daraus folgt.
Standardaktion für den Signal-Handler
Das zweite Argument der Funktion signal() ist formal ein Zeiger auf eine Handler-Funktion. Alternativ dürfen dort vordefinierte Sonderwerte übergeben werden, die keine Funktionen darstellen, sondern eine spezielle Behandlung durch das Laufzeitsystem anfordern:
SIG_DFL– Standardbehandlung des SignalsSIG_IGN– Signal ignorieren
Mit SIG_DFL wird die vom System vorgesehene Standardaktion für das jeweilige Signal aktiviert. Beispielaufruf:
signal(SIGINT, SIG_DFL);
Welche Aktion das ist, hängt vom Signal ab und führt in der Regel zur sofortigen Beendigung des Programms. Ein Signal befindet sich automatisch im Zustand SIG_DFL, solange kein eigener Handler registriert wurde.
Mit SIG_IGN wird das Signal vollständig verworfen.
Weder ein Handler wird aufgerufen noch die Standardaktion ausgeführt. Beispielaufruf:
signal(SIGINT, SIG_IGN);
Nicht alle Signale dürfen ignoriert werden. Insbesondere SIGKILL und SIGSTOP können weder abgefangen noch ignoriert werden.
Wie bereits erwähnt, gibt signal() den zuvor registrierten Handler zurück -- dabei können auch die Sonderwerte SIG_DFL oder SIG_IGN zurückgegeben werden (oder SIG_ERR bei einem Fehler). So lässt sich der vorherige Zustand eines Signals speichern und später wiederherstellen.
Beispiel: SIGINT ignorieren und später wieder aktivieren:
#include <signal.h>
#include <stdio.h>
int main() {
if (signal(SIGINT, SIG_IGN) == SIG_ERR) {
perror("signal");
return 1;
}
puts("SIGINT wird ignoriert – Strg+C hat keine Wirkung");
for (volatile long i = 0; i < 1000000000L; i++) {}
if (signal(SIGINT, SIG_DFL) == SIG_ERR) {
perror("signal");
return 1;
}
puts("SIGINT wird wieder normal behandelt");
for (;;) {} // jetzt beendet Strg+C das Programm
}
Synchrone vs. asynchrone Signale
Signale unterbrechen den normalen Programmablauf. Dabei ist wichtig, ob das auslösende Ereignis synchron oder asynchron zum laufenden Code auftritt.
-
Synchrone Ereignisse entstehen direkt durch die aktuell ausgeführte Instruktion. Ursache und Auslöser liegen im Programm selbst. Typische Beispiele sind Division durch 0 (
SIGFPE), Zugriff auf ungültigen Speicher (SIGSEGV) oder eine illegale Instruktion (SIGILL). Das Signal tritt genau an der Stelle auf, an der der Fehler passiert. Der Handler wird also als unmittelbare Folge des aktuellen Befehls aufgerufen. Falls eine Rückkehr aus dem Handler erfolgt, würde das Programm an derselben fehlerhaften Stelle fortsetzen, weshalb eine Weiterführung meist nicht sinnvoll ist. In solchen Fällen sollte der Handler das Programm direkt mit_Exit()oderabort()terminieren. -
Asynchrone Ereignisse entstehen unabhängig vom aktuellen Kontrollfluss. Sie können zu jedem Zeitpunkt eintreffen, auch zwischen zwei beliebigen Instruktionen. Beispiele sind
SIGINTdurch Strg+C oderSIGTERMdurch einen anderen Prozess. Das Programm ›springt‹ in diesem Moment in den Handler, egal was es gerade ausführt. Nach dem Handler läuft das Programm an der unterbrochenen Stelle weiter.
Das bedeutet: Asynchrone Signale können jeden Codepfad unterbrechen. Deshalb gelten dort besonders strenge Regeln. Synchrone Signale zeigen in der Regel einen Programmfehler an und dienen meist nur dazu, kontrolliert zu terminieren oder eine Diagnose zu ermöglichen.
Der C-Standard garantiert bei asynchronen Signalen nur den sicheren Zugriff auf Objekte vom Typ volatile sig_atomic_t sowie die Verwendung von _Exit().
Welche Funktionen in einem Signalhandler aufgerufen werden dürfen, ist im C-Standard nicht weiter festgelegt; eine entsprechende Liste existiert erst im POSIX-Standard. Funktionen wie printf(), malloc(), exit() oder die meisten anderen Bibliotheksfunktionen dürfen in Signalhandlern nicht verwendet werden, da ihr Verhalten undefiniert ist.
Signale explizit auslösen
Mit der Funktion raise() kann ein Programm gezielt selbst ein Signal auslösen
(Quelle: ISO/IEC 9899:2024, § 7.14.2.1 The raise function)
:
int raise(int sig);
Der Rückgabewert von raise() ist 0 bei Erfolg und ungleich 0 bei Fehler.
raise() wird verwendet, um die Signalbehandlung kontrolliert zu testen oder um interne Fehlerzustände über den gleichen Mechanismus zu behandeln wie externe Ereignisse. Die Funktion erzeugt das Signal im eigenen Prozess und führt den registrierten Handler unmittelbar im aktuellen Kontrollfluss aus.
Hinweis: C stellt mehrere Funktionen bereit, um Signale auszulösen. Sie stammen aus unterschiedlichen Standards und haben klar getrennte Aufgaben.
raise(int sig)ist Teil des C-Standards. Die Funktion sendet ein Signal an das eigene Programm und dient primär dazu, Signal-Handler gezielt auszulösen oder interne Fehler über die Signalbehandlung weiterzugeben.abort()gehört ebenfalls zum C-Standard. Die Funktion löst immerSIGABRTaus und beendet das Programm danach sofort. Falls ein installierter Handler zurückkehrt, wird das Programm anschließend dennoch beendet.kill(pid_t pid, int sig)stammt aus dem POSIX-Standard.Sie sendet ein Signal an einen beliebigen Prozess und wird für die externe Prozesssteuerung unter Unix-Systemen verwendet.
Grenzen von signal()/raise()
Die Standard-API (signal(), raise()) ist bewusst minimal definiert. Der C-Standard legt nur fest, dass ein Handler registriert wird, nicht wie er sich im Detail verhält. Das führt zu mehreren grundlegenden Problemen:
- Das Verhalten von
signal()ist implementierungsabhängig. Insbesondere ist nicht garantiert, dass ein Signal-Handler nach seiner Ausführung installiert bleibt. Auf manchen Systemen wird er nach dem ersten Signal automatisch auf die Standardaktion zurückgesetzt. - Ob unterbrochene Systemaufrufe automatisch neu gestartet werden oder mit einem Fehler abbrechen, ist ebenfalls nicht einheitlich definiert.
- Reihenfolge, Maskierung und Nebenwirkungen bei gleichzeitig eintreffenden Signalen sind nur unzureichend spezifiziert.
Damit ist Code, der sich auf signal() verlässt, nicht portabel und kann sich je nach Plattform unterschiedlich oder instabil verhalten.
Zusätzlich ist die API funktional stark eingeschränkt:
- Es gibt keine Möglichkeit, zusätzliche Kontextinformationen zu erhalten, etwa die Adresse eines Speicherfehlers.
- Stacktraces lassen sich nicht portabel erzeugen.
- Der C-Standard macht keine Aussagen zu Threads. In Multithread-Programmen ist Signalzustellung und -behandlung plattformspezifisch und deutlich komplexer.
Für robustes Verhalten sind daher plattformspezifische Schnittstellen nötig:
- Unix/POSIX:
sigaction(), optional mitsiginfo_t(), sowiebacktrace()unter glibc - Windows: Structured Exception Handling (SEH), Vectored Exception Handling
Eine wirklich portable Lösung für fortgeschrittene Crash-Diagnose existiert nicht.
Tipp: Während der Entwicklung sind Werkzeuge wie AddressSanitizer, Valgrind oder Debugger mit Core-Dump-Analyse deutlich zuverlässiger als eigene Signal-Handler.
Fehlerbehandlung ohne Signale
Signale eignen sich primär für asynchrone, externe Ereignisse. Sie sind kein Ersatz für saubere Fehlerbehandlung mit Rückgabewerten und Assertions, und sie bleiben ein Notfallmechanismus für Situationen, in denen der normale Kontrollfluss versagt.
Für vorhersehbare Fehler innerhalb des Programms sind Rückgabewerte und Fehlercodes die bevorzugte Methode in C, so wie es das folgende Beispiel zeigt:
#include <stdio.h>
#include <stdlib.h>
typedef enum {
OK,
ERR_NULL_POINTER,
ERR_DIVISION_BY_ZERO
} error_t;
static error_t divide(int a, int b, int *result) {
if (result == nullptr) return ERR_NULL_POINTER;
if (b == 0) return ERR_DIVISION_BY_ZERO;
*result = a / b;
return OK;
}
int main() {
int result;
error_t err = divide(10, 0, &result);
if (err != OK) {
fprintf(stderr, "Fehler: %d\n", err);
return EXIT_FAILURE;
}
printf("Resultat: %d\n", result);
}