13 kill und signal

13.1 Signalbehandlung mit trap

13.1.1 Grundlagen der Signalverarbeitung

In Unix/Linux-Systemen sind Signale eine Form der Interprozesskommunikation. Sie werden verwendet, um Prozessen mitzuteilen, dass bestimmte Ereignisse eingetreten sind. Ein Signal kann von einem Prozess an einen anderen gesendet werden, vom Kernel an einen Prozess oder vom Benutzer durch Tastenkombinationen wie Ctrl+C (SIGINT) oder Ctrl+Z (SIGTSTP).

Wenn ein Skript oder ein Programm ein Signal empfängt, hat es standardmäßig eine vordefinierte Reaktion. Zum Beispiel führt das SIGTERM-Signal normalerweise zur Beendigung des Prozesses. Mit dem trap-Kommando können wir jedoch definieren, wie unser Shell-Skript auf verschiedene Signale reagieren soll.

13.1.2 Signale senden mit dem kill-Kommando

Das kill-Kommando ist der zentrale Mechanismus zum Senden von Signalen an Prozesse unter Unix/Linux. Entgegen seinem Namen dient kill nicht nur zum Beenden von Prozessen, sondern kann jedes beliebige Signal an einen Prozess senden. Die Grundsyntax lautet:

kill [-Signalname oder -Signalnummer] PID

Beispiele:

# Sendet SIGTERM (15) an Prozess 1234
kill 1234

# Sendet SIGKILL (9) an Prozess 1234
kill -9 1234
# oder
kill -KILL 1234

# Sendet SIGHUP (1) an Prozess 1234
kill -HUP 1234

Das kill-Kommando bietet verschiedene Optionen:

Eine Übersicht aller verfügbaren Signale erhalten Sie mit:

$ kill -l
 1) SIGHUP      2) SIGINT      3) SIGQUIT     4) SIGILL      5) SIGTRAP
 6) SIGABRT     7) SIGBUS      8) SIGFPE      9) SIGKILL    10) SIGUSR1
11) SIGSEGV    12) SIGUSR2    13) SIGPIPE    14) SIGALRM    15) SIGTERM
16) SIGSTKFLT  17) SIGCHLD    18) SIGCONT    19) SIGSTOP    20) SIGTSTP
21) SIGTTIN    22) SIGTTOU    23) SIGURG     24) SIGXCPU    25) SIGXFSZ
26) SIGVTALRM  27) SIGPROF    28) SIGWINCH   29) SIGIO      30) SIGPWR
31) SIGSYS     34) SIGRTMIN   35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX

Die am häufigsten verwendeten Signale beim Prozessmanagement sind:

13.1.3 Das trap-Kommando

Das trap-Kommando ermöglicht es, bestimmte Aktionen auszuführen, wenn ein Signal empfangen wird. Die Syntax ist wie folgt:

trap 'Befehle' Signale

Dabei sind Befehle die Shell-Befehle, die ausgeführt werden sollen, wenn eines der angegebenen Signale empfangen wird.

13.1.4 Häufig verwendete Signale

Hier sind einige der am häufigsten verwendeten Signale und ihre Bedeutung:

Signal Nummer Bedeutung Standardaktion
SIGHUP 1 Hangup, Terminal wurde geschlossen Beenden
SIGINT 2 Interrupt (Ctrl+C) Beenden
SIGQUIT 3 Quit (Ctrl+\) Beenden mit Core
SIGKILL 9 Kill Sofort beenden
SIGTERM 15 Terminate Beenden
SIGTSTP 18 Stop (Ctrl+Z) Anhalten

Hinweis: SIGKILL (9) und SIGSTOP (19) können nicht mit trap abgefangen werden.

13.1.5 Einfaches Beispiel: Aufräumen beim Beenden

Ein häufiger Anwendungsfall für trap ist das Aufräumen temporärer Dateien, wenn ein Skript beendet wird, sei es normal oder durch ein Signal:

#!/bin/bash

# Temporäre Datei erstellen
TEMP_FILE=$(mktemp /tmp/example.XXXXXX)

# trap-Befehl, um die temporäre Datei zu löschen wenn das Skript beendet wird
trap 'echo "Räume auf..."; rm -f "$TEMP_FILE"; echo "Temporäre Datei gelöscht."' EXIT

# Etwas in die temporäre Datei schreiben
echo "Dies ist ein Test" > "$TEMP_FILE"

# Inhalt anzeigen
echo "Inhalt der temporären Datei:"
cat "$TEMP_FILE"

echo "Skript wird beendet..."

In diesem Beispiel wird die temporäre Datei automatisch gelöscht, wenn das Skript auf natürliche Weise endet oder mit exit beendet wird. Das EXIT-Signal ist ein spezielles Pseudo-Signal, das ausgelöst wird, wenn das Skript beendet wird, unabhängig davon, wie es beendet wird.

13.1.6 Abfangen mehrerer Signale

Sie können mehrere Signale mit derselben Trap-Aktion versehen:

#!/bin/bash

# Die gleiche Aktion für mehrere Signale definieren
trap 'echo "Abbruch durch Signal erkannt. Beende..."' SIGINT SIGTERM

echo "Skript gestartet. PID: $$"
echo "Drücke Ctrl+C, um zu testen."

# Endlosschleife
while true; do
    sleep 1
done

13.1.7 Zurücksetzen einer Trap

Um eine Trap zurückzusetzen, sodass das Signal seine Standardaktion ausführt, verwenden Sie das Schlüsselwort -:

trap - SIGINT

13.1.8 Anzeigen aktiver Trap-Kommandos

Um zu sehen, welche Trap-Befehle aktuell definiert sind, führen Sie trap ohne Argumente aus:

trap

13.1.9 Fortgeschrittenes Beispiel: Graceful Shutdown

Hier ist ein Beispiel für ein Skript, das eine “graceful shutdown” implementiert:

#!/bin/bash

# Funktion für das Aufräumen
cleanup() {
    echo "Aufräumen..."
    # Hier Aufräumaktionen einfügen, z.B. temporäre Dateien löschen
    rm -f /tmp/myapp_*.tmp
    echo "Aufräumen abgeschlossen."
}

# Funktion für die Beendigung
terminate() {
    echo "Signal zum Beenden empfangen."
    cleanup
    echo "Skript wird beendet."
    exit 0
}

# Trap für verschiedene Beendigungssignale
trap terminate SIGINT SIGTERM

# Trap für EXIT (wird aufgerufen, wenn das Skript normal beendet wird)
trap cleanup EXIT

echo "Skript gestartet. PID: $$"
echo "Zum Beenden Ctrl+C drücken oder 'kill $$' von einem anderen Terminal aus senden."

# Simulierte Arbeit
count=0
while true; do
    echo "Arbeit läuft... (Zähler: $count)"
    sleep 2
    ((count++))
done

13.1.10 Besondere Anwendungsfälle

13.1.10.1 DEBUG-Trap

Die DEBUG-Trap wird vor jeder Befehlsausführung ausgelöst und ist nützlich für Debugging:

#!/bin/bash

trap 'echo "DEBUG: Gerade ausgeführter Befehl: $BASH_COMMAND"' DEBUG

echo "Dies ist ein Test."
a=5
b=10
c=$((a + b))
echo "Summe: $c"

13.1.10.2 ERR-Trap

Die ERR-Trap wird ausgelöst, wenn ein Befehl mit einem Nicht-Null-Status fehlschlägt:

#!/bin/bash

trap 'echo "FEHLER: Der Befehl \"$BASH_COMMAND\" schlug fehl mit Exitcode $?"' ERR

# Diesen Befehl versuchen
cat nicht_existierende_datei.txt

# Weiterführen des Skripts
echo "Das Skript läuft trotz des Fehlers weiter."

13.1.11 Praktisches Beispiel: Hintergrundprozesse verwalten

In diesem Beispiel starten wir mehrere Hintergrundprozesse und verwenden trap, um sicherzustellen, dass alle beendet werden, wenn das Hauptskript beendet wird:

#!/bin/bash

# Array für Prozess-IDs
pids=()

# Funktion zur Beendigung aller Kindprozesse
cleanup() {
    echo "Beende alle Kindprozesse..."
    for pid in "${pids[@]}"; do
        if kill -0 "$pid" 2>/dev/null; then
            echo "Beende Prozess $pid"
            kill "$pid"
        fi
    done
    wait
    echo "Alle Prozesse beendet."
}

# Trap für Aufräumarbeiten
trap cleanup EXIT SIGINT SIGTERM

# Starte einige Hintergrundprozesse
for i in {1..3}; do
    (
        echo "Prozess $i gestartet (PID: $$)"
        while true; do
            echo "Prozess $i noch aktiv"
            sleep 5
        done
    ) &
    # Speichere PID des Hintergrundprozesses
    pids+=($!)
    echo "Hintergrundprozess $i gestartet mit PID ${pids[-1]}"
done

echo "Alle Hintergrundprozesse gestartet. Hauptskript-PID: $$"
echo "Drücke Ctrl+C zum Beenden"

# Warte auf Benutzerinteraktion
while true; do
    sleep 1
done

13.1.12 Einschränkungen von trap

Es ist wichtig zu beachten, dass einige Signale nicht mit trap abgefangen werden können:

  1. SIGKILL (9) kann nicht abgefangen oder ignoriert werden.
  2. SIGSTOP (19) kann nicht abgefangen oder ignoriert werden.

Weiterhin funktioniert trap nur innerhalb der Shell, in der es definiert wurde. Wenn ein Skript andere Programme oder Shell-Skripte aufruft, müssen diese ihre eigenen trap-Befehle haben.

13.1.13 Bewährte Praktiken

  1. Immer aufräumen: Verwenden Sie trap mit EXIT, um sicherzustellen, dass temporäre Ressourcen freigegeben werden, unabhängig davon, wie das Skript beendet wird.

  2. Vorsicht mit komplexen Aktionen: Die in einer trap definierten Aktionen sollten robust sein und keine Fehler verursachen, da dies zu unerwünschten Ergebnissen führen kann.

  3. Debuggen von Trap-Aktionen: Fügen Sie Logging-Anweisungen in Ihre trap-Aktionen ein, um zu verfolgen, wann und wie sie ausgelöst werden.

  4. Ressourcenverwaltung: Verwenden Sie trap zum Verwalten von Ressourcen wie temporären Dateien, Hintergrundprozessen und Sperrdateien.

  5. Graceful Shutdown implementieren: Erlauben Sie Ihren Skripten, laufende Operationen ordnungsgemäß abzuschließen, bevor sie beendet werden.

13.1.14 Zusammenspiel von kill und trap

Das Zusammenspiel von kill und trap ist entscheidend für robuste Shell-Skripte:

#!/bin/bash

# PID in einer Datei speichern, damit andere Prozesse Signale senden können
echo $ > /tmp/mein_skript.pid

# Trap für verschiedene Signale
trap 'echo "SIGHUP empfangen, Konfiguration wird neu geladen..."; source config.sh' SIGHUP
trap 'echo "SIGTERM empfangen, beende ordnungsgemäß..."; cleanup; exit 0' SIGTERM
trap 'echo "SIGINT empfangen, beende..."; exit 1' SIGINT

cleanup() {
    echo "Aufräumen..."
    rm -f /tmp/mein_skript.pid
}

# Hauptfunktion
main() {
    echo "Skript läuft mit PID $"
    echo "Sende Signale mit:"
    echo "  kill -HUP $(cat /tmp/mein_skript.pid)  # Konfiguration neu laden"
    echo "  kill -TERM $(cat /tmp/mein_skript.pid) # Ordnungsgemäß beenden"
    echo "  kill -INT $(cat /tmp/mein_skript.pid)  # Sofort beenden"
    
    while true; do
        echo "Arbeit wird ausgeführt..."
        sleep 10
    done
}

main

Mit diesem Muster können Sie: 1. Ein laufendes Skript von außen steuern 2. Unterschiedliche Reaktionen auf verschiedene Signale definieren 3. Prozesse ordnungsgemäß herunterfahren

Ein typisches Anwendungsbeispiel ist ein Daemon-Skript, das auf SIGHUP reagiert, um seine Konfiguration neu zu laden, ohne neu gestartet werden zu müssen.

13.1.15 Praktisches Beispiel: Dienst-Management-Skript

#!/bin/bash

# Konfiguration
SERVICE_NAME="mein_dienst"
PID_FILE="/var/run/$SERVICE_NAME.pid"
LOG_FILE="/var/log/$SERVICE_NAME.log"

# Hilfsfunktionen
log() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE"
    echo "$1"
}

start_service() {
    if [ -f "$PID_FILE" ] && kill -0 $(cat "$PID_FILE") 2>/dev/null; then
        log "Dienst läuft bereits mit PID $(cat "$PID_FILE")"
        return 1
    fi
    
    # Dienst starten
    log "Starte Dienst..."
    
    # Hier würde der eigentliche Dienst gestartet werden
    (
        # trap innerhalb des Dienstes definieren
        trap 'log "Signal empfangen, beende..."; exit 0' SIGINT SIGTERM
        trap 'log "SIGHUP empfangen, lade Konfiguration neu..."' SIGHUP
        
        # PID speichern
        echo $ > "$PID_FILE"
        
        log "Dienst gestartet mit PID $"
        
        # Hauptschleife des Dienstes
        while true; do
            # Simulierte Arbeit
            sleep 5
        done
    ) &
    
    # Kurz warten und prüfen, ob der Dienst noch läuft
    sleep 1
    if [ -f "$PID_FILE" ] && kill -0 $(cat "$PID_FILE") 2>/dev/null; then
        log "Dienst erfolgreich gestartet mit PID $(cat "$PID_FILE")"
        return 0
    else
        log "Fehler beim Starten des Dienstes"
        return 1
    fi
}

stop_service() {
    if [ ! -f "$PID_FILE" ]; then
        log "PID-Datei nicht gefunden. Dienst läuft nicht."
        return 0
    fi
    
    PID=$(cat "$PID_FILE")
    if ! kill -0 "$PID" 2>/dev/null; then
        log "Prozess $PID existiert nicht. Entferne veraltete PID-Datei."
        rm -f "$PID_FILE"
        return 0
    fi
    
    log "Sende SIGTERM an Prozess $PID..."
    kill -TERM "$PID"
    
    # Warten, bis der Prozess beendet ist
    for i in {1..10}; do
        if ! kill -0 "$PID" 2>/dev/null; then
            break
        fi
        log "Warte auf Beendigung des Prozesses..."
        sleep 1
    done
    
    # Falls der Prozess nicht reagiert, SIGKILL senden
    if kill -0 "$PID" 2>/dev/null; then
        log "Prozess reagiert nicht. Sende SIGKILL..."
        kill -KILL "$PID"
    fi
    
    rm -f "$PID_FILE"
    log "Dienst gestoppt."
    return 0
}

reload_service() {
    if [ ! -f "$PID_FILE" ]; then
        log "PID-Datei nicht gefunden. Dienst läuft nicht."
        return 1
    fi
    
    PID=$(cat "$PID_FILE")
    if ! kill -0 "$PID" 2>/dev/null; then
        log "Prozess $PID existiert nicht. Entferne veraltete PID-Datei."
        rm -f "$PID_FILE"
        return 1
    fi
    
    log "Sende SIGHUP an Prozess $PID, um Konfiguration neu zu laden..."
    kill -HUP "$PID"
    return 0
}

# Hauptprogramm
case "$1" in
    start)
        start_service
        ;;
    stop)
        stop_service
        ;;
    restart)
        stop_service
        start_service
        ;;
    reload)
        reload_service
        ;;
    status)
        if [ -f "$PID_FILE" ] && kill -0 $(cat "$PID_FILE") 2>/dev/null; then
            log "Dienst läuft mit PID $(cat "$PID_FILE")"
        else
            log "Dienst läuft nicht"
        fi
        ;;
    *)
        echo "Verwendung: $0 {start|stop|restart|reload|status}"
        exit 1
        ;;
esac

exit 0

Dieses Beispiel demonstriert ein vollständiges Dienst-Management-Skript, das kill und trap zusammen verwendet, um einen Daemon-Prozess zu steuern, zu überwachen und zu verwalten.