14 Prozesse und Job-Kontrolle

14.1 Grundlegende Prozesskonzepte in Unix/Linux

In Unix/Linux-Systemen ist ein Prozess eine Instanz eines laufenden Programms. Jeder Prozess besteht aus Code, Daten, Ressourcen und einem Ausführungszustand. Das Verständnis des Prozessmodells ist für effektives Shell-Scripting unerlässlich, da es die Grundlage für Programmausführung, Automatisierung und Systemverwaltung bildet.

14.1.1 Prozess-Identifikation

Jeder Prozess in einem Unix/Linux-System wird durch eine eindeutige Prozess-ID (PID) identifiziert. Diese numerische ID dient als primärer Bezeichner für den Prozess und wird für die meisten prozessbezogenen Operationen verwendet.

# Anzeige der PID des aktuellen Shell-Prozesses
echo $$

# Anzeige der PID des zuletzt gestarteten Hintergrundprozesses
echo $!

14.1.2 Prozesshierarchie

Unix/Linux-Systeme organisieren Prozesse in einer hierarchischen Baumstruktur. Jeder Prozess hat einen Elternprozess, der ihn erzeugt hat (mit Ausnahme des init-Prozesses, der die PID 1 hat und vom Kernel beim Systemstart gestartet wird).

# Anzeige des Prozessbaums
pstree

# Alternative mit ps
ps -ef --forest

Wenn ein Prozess einen neuen Prozess startet, wird dieser als Kindprozess bezeichnet. Der startende Prozess wird zum Elternprozess. Diese Beziehung ist wichtig für das Verständnis der Prozesssteuerung und des Lebenszyklus von Prozessen.

14.1.3 Prozesszustände

Ein Prozess kann sich in verschiedenen Zuständen befinden:

# Anzeige von Prozesszuständen
ps aux | head -n 10

Der zweite Buchstabe in der STAT-Spalte gibt den Prozesszustand an.

14.1.4 Prozessinformationen anzeigen

Das wichtigste Werkzeug zur Anzeige von Prozessinformationen ist der ps-Befehl:

# Grundlegende Prozessinformationen für alle Prozesse
ps -ef

# Detaillierte Informationen mit Ressourcennutzung
ps aux

# Prozesse eines bestimmten Benutzers anzeigen
ps -u username

# Prozesse nach CPU-Nutzung sortieren
ps aux --sort=-%cpu

Alternativ bietet top oder htop eine dynamische Echtzeit-Ansicht der laufenden Prozesse:

# Dynamische Prozessüberwachung
top

# Benutzerfreundlichere Alternative (möglicherweise Installation erforderlich)
htop

14.1.5 Prozessattribute

Jeder Prozess verfügt über verschiedene Attribute:

  1. Eigentümer und Gruppe: Bestimmen die Zugriffsrechte des Prozesses
  2. Priorität und Nice-Wert: Beeinflussen die Scheduling-Priorität
  3. Arbeitsverzeichnis: Das aktuelle Verzeichnis des Prozesses
  4. Umgebungsvariablen: Konfigurationswerte für den Prozess
  5. Offene Dateien: Dateien und Sockets, die der Prozess geöffnet hat
  6. Signalmasken und -handler: Definieren, wie der Prozess auf Signale reagiert
# Offene Dateien eines Prozesses anzeigen
lsof -p PID

# Umgebungsvariablen eines Prozesses anzeigen
cat /proc/PID/environ | tr '\0' '\n'

# Arbeitsverzeichnis eines Prozesses anzeigen
readlink /proc/PID/cwd

14.1.6 Prozesserzeugung

In Shell-Skripten können Prozesse auf verschiedene Arten erzeugt werden:

  1. Direkte Ausführung: Befehle in der Shell starten automatisch neue Prozesse

    ls -la    # Startet einen neuen Prozess für ls
  2. Hintergrundprozesse: Mit dem &-Operator kann ein Prozess im Hintergrund gestartet werden

    long_running_command &   # Startet den Befehl im Hintergrund
  3. Prozesssubstitution: Ermöglicht die Verwendung der Ausgabe eines Prozesses als Datei

    diff <(ls dir1) <(ls dir2)   # Vergleicht die Ausgaben zweier ls-Befehle

14.1.7 Prozess-Umgebung

Die Umgebung eines Prozesses umfasst Variablen, die sein Verhalten beeinflussen können:

# Alle Umgebungsvariablen anzeigen
env

# Eine spezifische Umgebungsvariable für einen Prozess setzen
VARIABLE=Wert command

Wenn ein Prozess einen Kindprozess erzeugt, erbt dieser normalerweise die Umgebungsvariablen des Elternprozesses.

14.1.8 Prozess-Grenzen und Ressourcenbeschränkungen

Unix/Linux-Systeme erlauben die Begrenzung der Ressourcen, die ein Prozess verwenden kann:

# Aktuelle Ressourcenlimits anzeigen
ulimit -a

# Maximale Anzahl offener Dateien für den aktuellen Prozess setzen
ulimit -n 2048

Diese Limits können in Shell-Skripten gesetzt werden, um sicherzustellen, dass Prozesse nicht zu viele Systemressourcen verbrauchen.

14.2 Starten und Überwachen von Hintergrundprozessen

Die Fähigkeit, Prozesse im Hintergrund auszuführen und zu überwachen, ist ein zentraler Aspekt effektiver Shell-Skripte, besonders wenn es um langwierige Operationen geht. Hintergrundprozesse ermöglichen es, mehrere Aufgaben parallel auszuführen, ohne auf den Abschluss jeder einzelnen warten zu müssen.

14.2.1 Prozesse im Hintergrund starten

14.2.1.1 Der &-Operator

Der einfachste Weg, einen Prozess im Hintergrund zu starten, ist die Verwendung des &-Operators am Ende eines Befehls:

# Starten eines Prozesses im Hintergrund
sleep 300 &

# Die Shell gibt die PID des Hintergrundprozesses zurück
[1] 12345

Nach dem Starten eines Hintergrundprozesses zeigt die Shell die Job-Nummer (in eckigen Klammern) und die PID an. Die Shell kehrt sofort zur Eingabeaufforderung zurück, während der Prozess im Hintergrund weiterläuft.

14.2.1.2 Mehrere Hintergrundprozesse

Es können mehrere Prozesse gleichzeitig im Hintergrund ausgeführt werden:

# Mehrere Hintergrundprozesse starten
long_task1 &
long_task2 &
long_task3 &

Jeder dieser Prozesse erhält eine eigene Job-Nummer und PID.

14.2.1.3 Ausgabeumleitung bei Hintergrundprozessen

Standardmäßig schreiben Hintergrundprozesse ihre Ausgabe weiterhin auf das Terminal, was störend sein kann. Um dies zu vermeiden, können Ausgaben umgeleitet werden:

# Standardausgabe und Standardfehlerausgabe in eine Datei umleiten
long_task > output.log 2>&1 &

# Ausgaben verwerfen
long_task > /dev/null 2>&1 &

# Ausgaben in unterschiedliche Dateien umleiten
long_task > output.log 2> error.log &

14.2.2 Hintergrundprozesse überwachen

14.2.2.1 jobs-Befehl

Der jobs-Befehl zeigt alle Hintergrundprozesse an, die von der aktuellen Shell gestartet wurden:

# Alle Hintergrundprozesse anzeigen
jobs

# Ausgabe mit PIDs
jobs -l

# Nur laufende Jobs anzeigen
jobs -r

# Nur gestoppte Jobs anzeigen
jobs -s

Die Ausgabe von jobs enthält die Job-Nummer, den Status (Running, Stopped, etc.) und den Befehl.

14.2.2.2 Prozessstatus mit ps

Der ps-Befehl kann verwendet werden, um detailliertere Informationen über Prozesse zu erhalten:

# Alle eigenen Prozesse anzeigen
ps

# Prozess mit bestimmter PID anzeigen
ps -p 12345

# Alle Prozesse (auch die anderer Benutzer) anzeigen
ps aux

# Bestimmte Informationen filtern
ps -eo pid,ppid,cmd,%cpu,%mem --sort=-%cpu

14.2.2.3 Kontinuierliche Überwachung mit top/htop

Für eine Echtzeit-Überwachung von Prozessen sind top und htop besser geeignet:

# Standardüberwachung mit top
top

# Überwachung eines bestimmten Prozesses
top -p 12345

# Benutzerfreundlichere Alternative (möglicherweise Installation erforderlich)
htop

14.2.3 Prozesskontrolle mit fg, bg und kill

14.2.3.1 Vordergrund und Hintergrund wechseln

Prozesse können zwischen Vordergrund und Hintergrund verschoben werden:

# Einen Hintergrundprozess in den Vordergrund holen
fg %1    # % gefolgt von der Job-Nummer

# Einen gestoppten Prozess im Hintergrund fortsetzen
bg %2

Ein laufender Vordergrundprozess kann mit Ctrl+Z angehalten und dann mit bg im Hintergrund fortgesetzt werden:

# Beispiel-Workflow
sleep 300    # Startet einen Prozess im Vordergrund
# [Drücke Ctrl+Z] -> Stoppt den Prozess
[1]+  Stopped                 sleep 300
bg    # Setzt den gestoppten Prozess im Hintergrund fort

14.2.3.2 Prozesse beenden

Prozesse können mit dem kill-Befehl beendet werden:

# Prozess mit PID beenden
kill 12345

# Prozess mit Job-Nummer beenden
kill %1

# Verschiedene Signale verwenden
kill -TERM 12345    # Normales Beenden (gleichwertig mit kill 12345)
kill -KILL 12345    # Sofortiges Beenden, kann nicht abgefangen werden
kill -HUP 12345     # Sendet das Hangup-Signal, oft für Neukonfiguration verwendet

Um einen Prozess zu beenden, der nicht reagiert, kann das SIGKILL-Signal (-9) verwendet werden. Dies sollte jedoch als letzte Möglichkeit betrachtet werden:

kill -9 12345    # Erzwingt die Beendigung

14.2.4 Prozessüberwachung in Skripten

14.2.4.1 wait-Befehl

In Shell-Skripten kann der wait-Befehl verwendet werden, um zu warten, bis Hintergrundprozesse abgeschlossen sind:

#!/bin/bash

# Starten mehrerer Hintergrundprozesse
process1 &
PID1=$!

process2 &
PID2=$!

process3 &
PID3=$!

# Auf Abschluss eines bestimmten Prozesses warten
wait $PID1
echo "Prozess 1 abgeschlossen"

# Auf Abschluss aller Hintergrundprozesse warten
wait
echo "Alle Prozesse abgeschlossen"

14.2.4.2 Überwachung mit Zeitlimit

Manchmal möchte man einen Prozess nur für eine begrenzte Zeit laufen lassen:

#!/bin/bash

# Prozess im Hintergrund starten
long_process &
PID=$!

# Timeout setzen (z.B. 60 Sekunden)
timeout=60
count=0

while kill -0 $PID 2>/dev/null; do
    if [ $count -ge $timeout ]; then
        echo "Zeitüberschreitung - Prozess wird beendet"
        kill -9 $PID
        break
    fi
    sleep 1
    ((count++))
done

Der kill -0-Befehl prüft nur, ob der Prozess existiert, ohne ein Signal zu senden.

14.2.4.3 Exitstatus überwachen

Um den Exitstatus eines Hintergrundprozesses zu erfassen:

#!/bin/bash

# Prozess im Hintergrund starten
some_command &
PID=$!

# Auf Abschluss warten
wait $PID
EXIT_STATUS=$?

if [ $EXIT_STATUS -eq 0 ]; then
    echo "Prozess erfolgreich abgeschlossen"
else
    echo "Prozess fehlgeschlagen mit Exitstatus $EXIT_STATUS"
fi

14.2.5 Nützliche Hinweise für Hintergrundprozesse

14.2.5.1 nohup und disown

Um sicherzustellen, dass Hintergrundprozesse weiterlaufen, selbst wenn die Shell geschlossen wird:

# Prozess starten, der das SIGHUP-Signal ignoriert
nohup long_process &

# Alternativ: Bestehenden Hintergrundprozess von der Shell "lösen"
long_process &
disown %1

14.2.5.2 Ändern der Prozesspriorität

Die Priorität von Prozessen kann mit nice und renice gesteuert werden:

# Prozess mit niedrigerer Priorität starten (höherer Nice-Wert)
nice -n 10 resource_intensive_command &

# Priorität eines laufenden Prozesses ändern
renice +10 -p 12345

Nice-Werte reichen von -20 (höchste Priorität, nur von root nutzbar) bis 19 (niedrigste Priorität).

14.2.5.3 parallelisierte Ausführung mit xargs

Für die Ausführung vieler paralleler Prozesse ist xargs mit der -P-Option nützlich:

# 4 parallele Instanzen ausführen
cat task_list.txt | xargs -P 4 -I {} ./process_task.sh {}

14.2.6 Umgang mit Zombie-Prozessen

Wenn ein Kindprozess beendet wird, aber der Elternprozess seinen Status nicht abfragt, bleibt er als “Zombie” im System:

# Zombie-Prozesse identifizieren
ps aux | grep 'Z'

In gut geschriebenen Shell-Skripten sollten Zombie-Prozesse durch korrektes Warten auf Kindprozesse vermieden werden:

#!/bin/bash

# Korrekte Behandlung von Kindprozessen
child_process &
wait $!  # Verhindert Zombies

# Alternativ: Signal-Handler für SIGCHLD
trap 'wait $!' SIGCHLD

14.3 Prozess-Signale und ihre Handhabung

Signale sind ein grundlegender Mechanismus zur Interprozess-Kommunikation in Unix/Linux-Systemen. Sie ermöglichen es, Prozessen Ereignisse oder Anfragen zu übermitteln und darauf zu reagieren. Im Shell-Scripting spielen Signale eine wichtige Rolle bei der Prozesssteuerung, Fehlerbehandlung und der Implementierung ordnungsgemäßer Aufräumroutinen.

14.3.1 Grundlagen der Prozess-Signale

Ein Signal ist im Wesentlichen eine Software-Unterbrechung, die an einen Prozess gesendet wird, um ein bestimmtes Ereignis anzuzeigen. Wenn ein Prozess ein Signal empfängt, unterbricht das Betriebssystem dessen normale Ausführung und führt eine vorbestimmte Aktion aus, die vom Signaltyp abhängt.

14.3.1.1 Gemeinsame Signaltypen

Hier sind die am häufigsten verwendeten Signale:

Signal Nummer Beschreibung Standardaktion
SIGHUP 1 Hangup (Terminal geschlossen) Beenden
SIGINT 2 Interrupt (meist Ctrl+C) Beenden
SIGQUIT 3 Quit (meist Ctrl+\) Beenden mit Core-Dump
SIGKILL 9 Kill (nicht abfangbar) Beenden (erzwungen)
SIGTERM 15 Terminate (normal beenden) Beenden
SIGSTOP 19 Stop (nicht abfangbar) Anhalten
SIGTSTP 20 Terminal stop (meist Ctrl+Z) Anhalten
SIGCONT 18 Continue (nach Stop) Fortsetzen
SIGUSR1 10 Benutzerdefiniert 1 Beenden
SIGUSR2 12 Benutzerdefiniert 2 Beenden
# Liste aller verfügbaren Signale anzeigen
kill -l

14.3.2 Signale senden

Der kill-Befehl ist das Hauptwerkzeug zum Senden von Signalen an Prozesse:

# SIGTERM (Standard) an einen Prozess senden
kill 1234

# Bestimmtes Signal an einen Prozess senden
kill -SIGINT 1234   # Oder: kill -2 1234

# Signal an alle Prozesse einer Prozessgruppe senden
kill -SIGTERM -1234  # Negative PID bezieht sich auf die Prozessgruppe

# Signal an alle Prozesse eines Benutzers senden
pkill -SIGTERM -u username

# Signal an Prozesse senden, die einem Muster entsprechen
pkill -SIGTERM firefox

14.3.3 Signale in Shell-Skripten abfangen

Mit dem trap-Befehl können Shell-Skripte auf Signale reagieren und eigene Handler definieren:

#!/bin/bash

# Funktion zum Aufräumen bei Beendigung
cleanup() {
    echo "Aufräumen und Beenden..."
    rm -f /tmp/tempfile_$$
    exit 0
}

# Signal-Handler einrichten
trap cleanup SIGINT SIGTERM EXIT

# Temporäre Datei erstellen
touch /tmp/tempfile_$$

# Langwierige Operation simulieren
echo "Prozess läuft. PID: $$"
echo "Drücke Ctrl+C um zu beenden."
while true; do
    sleep 1
done

In diesem Beispiel: - Die cleanup-Funktion wird ausgeführt, wenn das Skript SIGINT (Ctrl+C), SIGTERM oder beim normalen Beenden (EXIT) empfängt - Das Skript erstellt eine temporäre Datei und stellt sicher, dass diese aufgeräumt wird, unabhängig davon, wie das Skript beendet wird - Die Variable $$ enthält die PID des aktuellen Shell-Prozesses

14.3.3.1 Syntax des trap-Befehls

trap [Aktion] [Signalliste]

Die Aktion kann sein: - Ein Befehl oder eine Funktion - Ein leerer String '' (Signal ignorieren) - - (Standardaktion wiederherstellen)

# Signal ignorieren
trap '' SIGINT

# Standardaktion wiederherstellen
trap - SIGINT

# Mehrere Befehle als Handler
trap 'echo "Abbruch!"; rm -f /tmp/tempfile_$$; exit 1' SIGINT

14.3.4 Gebräuchliche Signal-Handhabungsmuster

14.3.4.1 Aufräumen temporärer Ressourcen

#!/bin/bash

TEMPFILES=()
TEMPFILES+=("$(mktemp)")
TEMPFILES+=("$(mktemp)")

cleanup() {
    echo "Temporäre Dateien entfernen..."
    for file in "${TEMPFILES[@]}"; do
        rm -f "$file"
    done
}

trap cleanup EXIT

14.3.4.2 Graceful Shutdown eines Daemon-Skripts

#!/bin/bash

PID_FILE="/var/run/mydaemon.pid"

shutdown() {
    echo "Graceful Shutdown wird durchgeführt..."
    # Kindprozesse beenden
    pkill -P $$
    # PID-Datei entfernen
    rm -f "$PID_FILE"
    exit 0
}

# PID speichern
echo $$ > "$PID_FILE"

# Signal-Handler einrichten
trap shutdown SIGINT SIGTERM

# Daemon-Loop
while true; do
    # Hauptlogik hier
    sleep 10
done

14.3.4.3 Timeout für Operationen implementieren

#!/bin/bash

timeout_handler() {
    echo "Zeitlimit überschritten!"
    kill -9 $CHILD_PID 2>/dev/null
    exit 1
}

# SIGALRM-Handler einrichten
trap timeout_handler SIGALRM

# Timeout in Sekunden
TIMEOUT=30

# Alarm setzen
(sleep $TIMEOUT && kill -SIGALRM $$) &
ALARM_PID=$!

# Langwierige Operation starten
long_running_command &
CHILD_PID=$!

# Auf Abschluss warten
wait $CHILD_PID
RESULT=$?

# Alarm-Prozess beenden
kill $ALARM_PID 2>/dev/null

exit $RESULT

14.3.5 Besonderheiten und Einschränkungen

14.3.5.1 Nicht abfangbare Signale

SIGKILL (9) und SIGSTOP (19) können nicht abgefangen, blockiert oder ignoriert werden. Sie werden immer vom Betriebssystem durchgesetzt:

# Diese Signal-Handler werden nie ausgeführt
trap 'echo "Dies wird nie angezeigt"' SIGKILL SIGSTOP

14.3.5.2 Subshells und Signalvererbung

Signal-Handler werden nicht automatisch an Subshells vererbt:

#!/bin/bash

trap 'echo "Hauptskript fängt Signal ab"' SIGINT

# Dieser Subshell-Prozess erbt den Handler nicht
(
    # Eigenen Handler definieren
    trap 'echo "Subshell fängt Signal ab"; exit 1' SIGINT
    echo "Subshell läuft. Drücke Ctrl+C."
    sleep 30
)

# Warten auf Abschluss der Subshell
wait

14.3.5.3 Prozessgruppen und Signale

Befehle, die über eine Pipeline verbunden sind, sind Teil derselben Prozessgruppe. Signale werden standardmäßig an die gesamte Prozessgruppe gesendet:

#!/bin/bash

# Handler für das Hauptskript
trap 'echo "Hauptskript fängt Signal ab"' SIGINT

# Pipeline starten (alle Prozesse in einer Prozessgruppe)
cat /dev/urandom | head -c 100000 | sort | uniq | wc -l &

# PID der Prozessgruppe erhalten
pgid=$!

echo "Prozessgruppe läuft mit PGID: $pgid"
echo "Drücke Ctrl+C oder warte 10 Sekunden."

sleep 10

# Alternative: Signal nur an einen bestimmten Prozess in der Gruppe senden
# kill -SIGINT $(pgrep -P $pgid | head -1)

14.3.6 Fortgeschrittene Signaltechniken

14.3.6.1 Signale zur Interprozesskommunikation

Benutzerdefinierte Signale (SIGUSR1, SIGUSR2) eignen sich gut für die Kommunikation zwischen Skripten:

#!/bin/bash

# Prozess A: Signal-Empfänger
receiver() {
    echo "Empfänger gestartet. PID: $$"
    
    handle_usr1() {
        echo "Konfiguration neu laden..."
    }
    
    handle_usr2() {
        echo "Statusbericht generieren..."
    }
    
    trap handle_usr1 SIGUSR1
    trap handle_usr2 SIGUSR2
    
    echo "Warte auf Signale. Beende mit Ctrl+C."
    while true; do
        sleep 1
    done
}

# Prozess B: Signal-Sender
sender() {
    echo "Sender gestartet."
    echo "1) SIGUSR1 senden"
    echo "2) SIGUSR2 senden"
    echo "q) Beenden"
    
    while true; do
        read -p "Befehl: " cmd
        case $cmd in
            1) kill -SIGUSR1 $RECEIVER_PID ;;
            2) kill -SIGUSR2 $RECEIVER_PID ;;
            q) exit 0 ;;
            *) echo "Unbekannter Befehl" ;;
        esac
    done
}

# Empfänger im Hintergrund starten
receiver &
RECEIVER_PID=$!

# Sender im Vordergrund starten
sender

# Aufräumen
kill $RECEIVER_PID

14.3.6.2 Asynchrone Ereignisbehandlung

Signale ermöglichen asynchrone Ereignisbehandlung in Shell-Skripten:

#!/bin/bash

# Globaler Status
STATUS="Inaktiv"

# Signal-Handler
handle_event() {
    case $1 in
        start)
            STATUS="Aktiv"
            echo "Status: $STATUS"
            ;;
        stop)
            STATUS="Inaktiv"
            echo "Status: $STATUS"
            ;;
        status)
            echo "Status: $STATUS"
            ;;
    esac
}

# Signal-Handler zuweisen
trap 'handle_event start' SIGUSR1
trap 'handle_event stop' SIGUSR2
trap 'handle_event status' SIGHUP

echo "Event-Handler gestartet. PID: $$"
echo "Sende Signale mit:"
echo "  kill -SIGUSR1 $$ # Start"
echo "  kill -SIGUSR2 $$ # Stop"
echo "  kill -SIGHUP $$ # Status"

# Hauptloop
while true; do
    sleep 1
done

14.3.7 Bewährte Praktiken für die Signalhandhabung

  1. Immer EXIT abfangen: Um eine konsistente Aufräumung bei allen Beendigungsarten zu gewährleisten

    trap cleanup EXIT
  2. Idempotente Handler: Signal-Handler sollten mehrfach ausführbar sein ohne Schaden anzurichten

    cleanup() {
        # Variable verhindern mehrfache Ausführung
        if [ "$CLEANUP_DONE" = "1" ]; then
            return
        fi
        CLEANUP_DONE=1
    
        # Aufräumen hier
        rm -f "$TEMP_FILE"
    }
  3. Minimale Handler: Signal-Handler sollten kurz und zuverlässig sein

    # Gut: Nur Flags setzen im Handler
    trap 'TERMINATE=1' SIGTERM
    
    while [ "$TERMINATE" != "1" ]; do
        # Hauptlogik hier
        sleep 1
    done
    
    # Aufräumen nach der Schleife
  4. Signalblockierung in kritischen Abschnitten:

    # Signale temporär blockieren
    trap '' SIGINT SIGTERM
    
    # Kritischer Abschnitt hier
    critical_operation
    
    # Standardverhalten wiederherstellen
    trap - SIGINT SIGTERM

14.4 Job-Kontrolle in der Shell

Die Job-Kontrolle ist ein leistungsstarkes Feature moderner Shells, das es Benutzern ermöglicht, mehrere Prozesse effizient zu verwalten, zwischen Vordergrund- und Hintergrundausführung zu wechseln und die Ausführung von Prozessen zu pausieren oder fortzusetzen. Diese Funktionen sind besonders nützlich für Shell-Skripte, die komplexe Aufgaben mit mehreren parallel laufenden Komponenten ausführen müssen.

14.4.1 Grundkonzepte der Job-Kontrolle

In der Shell-Terminologie ist ein “Job” eine Gruppe von Prozessen, die von einem einzelnen Befehl oder einer Pipeline gestartet wurden. Die Shell weist jedem Job eine Jobnummer zu, die für die Steuerung dieses Jobs verwendet werden kann.

14.4.1.1 Vordergrund vs. Hintergrund

Jobs können in zwei Modi ausgeführt werden:

# Vordergrundausführung
sleep 60

# Hintergrundausführung
sleep 60 &

14.4.2 Grundlegende Job-Kontrolle-Befehle

14.4.2.1 jobs - Anzeigen aktiver Jobs

Der jobs-Befehl listet alle aktiven Jobs auf, die von der aktuellen Shell-Sitzung verwaltet werden:

# Alle Jobs anzeigen
jobs

# Ausgabeformat:
# [1]  + Running                 sleep 100 &
# [2]  - Stopped                 nano testfile.txt

# Mit zusätzlichen Optionen
jobs -l    # Mit Process-IDs anzeigen
jobs -p    # Nur Process-IDs anzeigen
jobs -s    # Nur angehaltene Jobs anzeigen
jobs -r    # Nur laufende Jobs anzeigen

In der Ausgabe: - Die Zahl in eckigen Klammern ist die Job-Nummer - Das Plus-Zeichen (+) kennzeichnet den aktuellen Job (wird standardmäßig von den Befehlen fg und bg verwendet) - Das Minus-Zeichen (-) kennzeichnet den Job, der zum aktuellen Job wird, wenn der aktuelle Job beendet wird - Der Status zeigt an, ob der Job läuft, angehalten oder beendet ist

14.4.2.2 fg - Job in den Vordergrund bringen

Der fg-Befehl bringt einen Hintergrund- oder angehaltenen Job in den Vordergrund:

# Aktuellen Job in den Vordergrund bringen
fg

# Bestimmten Job in den Vordergrund bringen
fg %1     # Job mit Nummer 1
fg %vim   # Job, dessen Befehlsname mit "vim" beginnt
fg %%     # Aktueller Job (äquivalent zu fg)
fg %-     # Vorheriger Job

14.4.2.3 bg - Job im Hintergrund fortsetzen

Der bg-Befehl setzt einen angehaltenen Job im Hintergrund fort:

# Aktuellen angehaltenen Job im Hintergrund fortsetzen
bg

# Bestimmten angehaltenen Job im Hintergrund fortsetzen
bg %2

14.4.2.4 Ctrl+Z - Job anhalten

Die Tastenkombination Ctrl+Z sendet das SIGTSTP-Signal an den aktuellen Vordergrundjob, wodurch dieser angehalten wird:

sleep 100  # Starte einen Vordergrundjob
# [Drücke Ctrl+Z]
# [1]+ Stopped                 sleep 100
bg         # Setze den angehaltenen Job im Hintergrund fort

14.4.2.5 Ctrl+C - Job beenden

Die Tastenkombination Ctrl+C sendet das SIGINT-Signal an den aktuellen Vordergrundjob, wodurch dieser in der Regel beendet wird:

sleep 100  # Starte einen Vordergrundjob
# [Drücke Ctrl+C]
# ^C

14.4.3 Jobreferenzen

Jobs können auf verschiedene Arten referenziert werden:

# Beispiele für Jobreferenzen
kill %1        # Sendet SIGTERM an Job 1
bg %?find      # Setzt Job fort, der "find" in der Befehlszeile enthält
fg %+          # Bringt den aktuellen Job in den Vordergrund

14.4.4 Job-Kontrolle in Shell-Skripten

14.4.4.1 Aktivieren der Job-Kontrolle in Skripten

In Shell-Skripten ist die Job-Kontrolle standardmäßig deaktiviert. Sie kann mit dem Befehl set -m oder set -o monitor aktiviert werden:

#!/bin/bash
# Job-Kontrolle aktivieren
set -m

# Jobs starten
process1 &
process2 &

# Jobs auflisten
jobs

# Auf einen bestimmten Job warten
fg %1

14.4.4.2 Automatische Job-Wiederherstellung

Ein nützliches Muster für die Wiederherstellung angehaltener Jobs:

#!/bin/bash
set -m

# Funktion zur Wiederherstellung angehaltener Jobs
resume_stopped_jobs() {
    local stopped_jobs
    stopped_jobs=$(jobs -sp)
    
    if [ -n "$stopped_jobs" ]; then
        echo "Wiederherstellung angehaltener Jobs..."
        while read -r pid; do
            kill -CONT "$pid" 2>/dev/null
        done <<< "$stopped_jobs"
    fi
}

# Signal-Handler einrichten
trap resume_stopped_jobs EXIT

# Hauptlogik des Skripts
# ...

14.4.4.3 Job-Gruppen erstellen und verwalten

Für komplexere Anwendungen kann es nützlich sein, Jobs in Gruppen zu organisieren:

#!/bin/bash
set -m

# Neue Prozessgruppe starten
start_job_group() {
    # Subshell mit eigener Prozessgruppe
    (
        # Mehrere Prozesse starten
        process1 &
        process2 &
        process3 &
        
        # Auf alle warten
        wait
    ) &
    
    # PID der Prozessgruppe (= PID der Subshell)
    echo $!
}

# Job-Gruppe starten
group_pid=$(start_job_group)
echo "Job-Gruppe gestartet mit PID: $group_pid"

# Später: Ganze Gruppe beenden
kill -TERM -"$group_pid"  # Negative PID bezieht sich auf die Prozessgruppe

14.4.5 Fortgeschrittene Job-Kontrolle-Techniken

14.4.5.1 Co-Prozesse

Bash unterstützt Co-Prozesse, die eine bidirektionale Kommunikation mit einem Hintergrundjob ermöglichen:

#!/bin/bash

# Co-Prozess starten
coproc SORTER {
    sort
}

# Daten an den Co-Prozess senden
echo -e "banana\napple\ncherry" >&${SORTER[1]}
# Eingabekanal schließen
exec {SORTER[1]}>&-

# Daten vom Co-Prozess lesen
sorted_output=$(cat <&${SORTER[0]})
echo "Sortierte Liste:"
echo "$sorted_output"

# Ausgabekanal schließen
exec {SORTER[0]}>&-

# Auf Beendigung des Co-Prozesses warten
wait $SORTER_PID

14.4.5.2 Dezente Handhabung mehrerer Jobs

Für komplexere Anwendungen, die mehrere Jobs verwalten müssen:

#!/bin/bash
set -m

# Assoziatives Array für Job-Tracking
declare -A jobs_map

# Funktion zum Starten eines Hintergrundjobs
start_job() {
    local job_name=$1
    local command=$2
    
    $command &
    local pid=$!
    jobs_map["$job_name"]=$pid
    echo "Job '$job_name' gestartet mit PID $pid"
}

# Funktion zum Beenden eines Jobs
stop_job() {
    local job_name=$1
    local pid=${jobs_map["$job_name"]}
    
    if [ -n "$pid" ] && kill -0 $pid 2>/dev/null; then
        echo "Beende Job '$job_name' (PID $pid)..."
        kill -TERM $pid
        wait $pid 2>/dev/null
        echo "Job '$job_name' beendet."
    else
        echo "Job '$job_name' existiert nicht oder läuft nicht."
    fi
}

# Funktion zum Auflisten aller Jobs
list_jobs() {
    echo "Aktive Jobs:"
    for job_name in "${!jobs_map[@]}"; do
        local pid=${jobs_map["$job_name"]}
        if kill -0 $pid 2>/dev/null; then
            echo "  $job_name (PID $pid): Aktiv"
        else
            echo "  $job_name (PID $pid): Beendet"
            unset jobs_map["$job_name"]
        fi
    done
}

# Beispielverwendung
start_job "logger" "tail -f /var/log/syslog"
start_job "counter" "for i in {1..100}; do echo \$i; sleep 1; done"

sleep 5
list_jobs

stop_job "counter"
list_jobs

# Vor dem Beenden alle Jobs stoppen
trap 'for job in "${!jobs_map[@]}"; do stop_job "$job"; done' EXIT

14.4.5.3 Dynamisches Scheduling von Jobs

Für Anwendungen, die Jobs basierend auf Systemauslastung oder anderen Kriterien planen müssen:

#!/bin/bash
set -m

# Maximal zulässige Anzahl gleichzeitiger Jobs
MAX_CONCURRENT_JOBS=4

# Warteschlange für ausstehende Aufgaben
task_queue=()

# Funktion zum Hinzufügen einer Aufgabe zur Warteschlange
add_task() {
    task_queue+=("$1")
    echo "Aufgabe zur Warteschlange hinzugefügt: $1"
    schedule_tasks
}

# Funktion zum Planen und Ausführen von Aufgaben
schedule_tasks() {
    # Anzahl aktiver Jobs ermitteln
    local active_jobs=$(jobs -r | wc -l)
    
    # Solange Platz für mehr Jobs und Aufgaben in der Warteschlange sind
    while [ $active_jobs -lt $MAX_CONCURRENT_JOBS ] && [ ${#task_queue[@]} -gt 0 ]; do
        # Nächste Aufgabe aus der Warteschlange nehmen
        local next_task=${task_queue[0]}
        task_queue=("${task_queue[@]:1}")  # Erste Aufgabe entfernen
        
        # Aufgabe im Hintergrund starten
        echo "Starte Aufgabe: $next_task"
        eval "$next_task" &
        
        # Anzahl aktiver Jobs aktualisieren
        active_jobs=$(jobs -r | wc -l)
    done
}

# Signal-Handler für beendete Hintergrundjobs
handle_job_completion() {
    schedule_tasks
}
trap handle_job_completion CHLD

# Beispielverwendung
for i in {1..10}; do
    add_task "echo 'Aufgabe $i gestartet'; sleep $((RANDOM % 5 + 1)); echo 'Aufgabe $i beendet'"
done

# Warten, bis alle Aufgaben abgearbeitet sind
wait
echo "Alle Aufgaben abgeschlossen."

14.4.6 Best Practices für die Job-Kontrolle

  1. Aufräumen hinterlassener Jobs:

    # Vor dem Beenden alle Hintergrundjobs beenden
    trap 'jobs -p | xargs -r kill' EXIT
  2. Überwachung langlebiger Jobs:

    # Prozessüberwachung mit timeout
    monitor_job() {
        local pid=$1
        local max_runtime=$2
    
        (
            sleep $max_runtime
            if kill -0 $pid 2>/dev/null; then
                echo "Job $pid überschreitet maximale Laufzeit"
                kill -TERM $pid
            fi
        ) &
    }
    
    long_process &
    job_pid=$!
    monitor_job $job_pid 3600  # 1 Stunde Timeout
  3. Job-Status protokollieren:

    # Job mit Statusprotokollierung starten
    log_file="job_$(date +%Y%m%d_%H%M%S).log"
    {
        echo "Job gestartet: $(date)"
        time my_command
        echo "Job beendet: $(date) mit Status: $?"
    } > "$log_file" 2>&1 &
  4. Ressourcenkontrolle für Jobs:

    # Limitierte Ressourcen für einen Job
    (
        # CPU-Priorität reduzieren
        renice +10 -p $$ > /dev/null
    
        # I/O-Priorität reduzieren (benötigt ionice)
        ionice -c 3 -p $$
    
        # Speicherlimit setzen
        ulimit -v 1000000  # 1 GB virtueller Speicher
    
        # CPU-Zeit begrenzen
        ulimit -t 3600     # 1 Stunde CPU-Zeit
    
        # Job ausführen
        intensive_process
    ) &

14.5 Parallelisierung von Aufgaben

In der heutigen Welt der Mehrkernprozessoren und verteilten Systeme ist die Fähigkeit, Aufgaben zu parallelisieren, zu einer grundlegenden Anforderung für effiziente Skripte geworden. Die Parallelisierung in Shell-Skripten ermöglicht es, die verfügbaren Systemressourcen besser zu nutzen und die Ausführungszeit von Aufgaben erheblich zu reduzieren. Dieser Abschnitt behandelt verschiedene Techniken zur Parallelisierung von Aufgaben in Shell-Skripten.

14.5.1 Grundlegende Parallelisierung mit dem &-Operator

Die einfachste Form der Parallelisierung in Shell-Skripten ist die Verwendung des &-Operators, um Prozesse im Hintergrund zu starten:

#!/bin/bash

echo "Starte parallele Aufgaben..."

# Mehrere Befehle parallel ausführen
task1 &
task2 &
task3 &

# Auf Abschluss aller Hintergrundprozesse warten
wait

echo "Alle Aufgaben abgeschlossen."

Diese grundlegende Technik hat jedoch einige Einschränkungen: - Keine einfache Möglichkeit, die Anzahl gleichzeitiger Prozesse zu begrenzen - Schwierigkeiten bei der Erfassung und Verarbeitung von Rückgabewerten - Keine integrierte Fehlerbehandlung

14.5.2 Parallelisierung mit xargs

Der xargs-Befehl mit der Option -P ist ein leistungsstarkes Werkzeug zur kontrollierten Parallelisierung:

#!/bin/bash

# Liste von Aufgaben
echo "Datei1 Datei2 Datei3 Datei4 Datei5 Datei6 Datei7 Datei8" | \
# Maximal 4 parallele Prozesse
xargs -P 4 -n 1 ./verarbeite_datei.sh

Der Parameter -P gibt die maximale Anzahl gleichzeitiger Prozesse an, während -n die Anzahl der Argumente pro Befehlsaufruf festlegt.

Ein praktisches Beispiel für die Bildkonvertierung:

#!/bin/bash

# Alle JPG-Dateien finden und in PNG konvertieren, mit maximal 8 parallelen Prozessen
find . -name "*.jpg" -type f | \
xargs -P 8 -I {} convert {} {}.png

14.5.3 Parallelisierung mit GNU Parallel

GNU Parallel ist ein spezialisiertes Werkzeug für die Parallelisierung, das mehr Funktionen als xargs bietet:

#!/bin/bash

# Sicherstellen, dass GNU Parallel installiert ist
if ! command -v parallel &> /dev/null; then
    echo "GNU Parallel ist nicht installiert. Bitte installieren Sie es mit:"
    echo "  apt-get install parallel  # Debian/Ubuntu"
    echo "  yum install parallel      # CentOS/RHEL"
    exit 1
fi

# Grundlegende Verwendung: Liste von Dateien verarbeiten
find . -name "*.log" | \
parallel --max-procs=8 ./analyze_log.sh {}

# Mit Jobnamen und Fortschrittsanzeige
find . -name "*.log" | \
parallel --max-procs=8 --joblog parallel.log --progress \
    './analyze_log.sh {} > {}.report'

GNU Parallel bietet zahlreiche fortgeschrittene Funktionen:

# Automatische CPU-Kern-Erkennung
parallel --jobs 100% ./task.sh ::: input1 input2 input3

# Unterschiedliche Eingabequellen kombinieren
parallel ./process.sh {1} {2} ::: A B C ::: 1 2 3

# Ausgabe in der gleichen Reihenfolge wie die Eingabe
find . -name "*.txt" | parallel --keep-order 'wc -l {}'

# Ergebnisse in einer CSV-Datei speichern
parallel --results results.csv './test.sh {}' ::: input1 input2 input3

14.5.4 Begrenzung und Steuerung paralleler Prozesse

14.5.4.1 Implementierung eines einfachen Semaphors

Für Umgebungen ohne xargs -P oder GNU Parallel kann ein einfacher Semaphor implementiert werden:

#!/bin/bash

# Maximale Anzahl gleichzeitiger Prozesse
MAX_PROCS=4

# Funktion zum Starten eines Prozesses mit Semaphor
run_with_semaphore() {
    # Aktuelle Anzahl laufender Hintergrundprozesse
    local running=$(jobs -r | wc -l)
    
    # Warten, bis ein Platz frei ist
    while [ $running -ge $MAX_PROCS ]; do
        sleep 0.5
        running=$(jobs -r | wc -l)
    done
    
    # Prozess starten
    "$@" &
}

# Beispielverwendung
for file in *.txt; do
    run_with_semaphore ./process_file.sh "$file"
done

# Auf Abschluss aller Hintergrundprozesse warten
wait

14.5.4.2 Fortgeschrittener Semaphor mit FIFOs

Für eine präzisere Kontrolle kann ein Semaphor mit Named Pipes (FIFOs) implementiert werden:

#!/bin/bash

# Maximale Anzahl gleichzeitiger Prozesse
MAX_PROCS=4
FIFO="/tmp/semaphore_$$"

# Aufräumen beim Beenden
cleanup() {
    rm -f "$FIFO"
}
trap cleanup EXIT

# FIFO erstellen
mkfifo "$FIFO"

# Semaphor initialisieren
for ((i=0; i<MAX_PROCS; i++)); do
    echo $i > "$FIFO" &
done

# Funktion zum Ausführen eines Befehls unter Semaphorkontrolle
run_with_semaphore() {
    local slot
    
    # Slot aus dem Semaphor lesen (blockiert, wenn keiner verfügbar)
    read slot < "$FIFO"
    
    # Befehl ausführen und Slot zurückgeben, wenn fertig
    ( "$@"; echo $slot > "$FIFO" ) &
}

# Beispielverwendung
for file in *.dat; do
    run_with_semaphore ./analyze.sh "$file"
done

# Auf Abschluss aller Prozesse warten
wait

14.5.5 Prozess-Pools und Worker-Modelle

Für komplexere Anwendungen kann ein Worker-Pool-Modell implementiert werden:

#!/bin/bash

# Konfiguration
NUM_WORKERS=4
TASK_QUEUE="/tmp/task_queue_$$"
RESULT_QUEUE="/tmp/result_queue_$$"

# Aufräumen beim Beenden
cleanup() {
    rm -f "$TASK_QUEUE" "$RESULT_QUEUE"
    # Alle Worker-Prozesse beenden
    jobs -p | xargs -r kill
}
trap cleanup EXIT

# FIFOs erstellen
mkfifo "$TASK_QUEUE" "$RESULT_QUEUE"

# Worker-Funktion
worker() {
    local worker_id=$1
    local task
    
    echo "Worker $worker_id gestartet"
    
    # Auf Aufgaben aus der Warteschlange warten
    while read task; do
        # Beenden, wenn Terminierungssignal empfangen
        if [ "$task" = "TERMINATE" ]; then
            break
        fi
        
        echo "Worker $worker_id verarbeitet Aufgabe: $task"
        
        # Aufgabe ausführen und Ergebnis speichern
        local result=$(eval "$task" 2>&1)
        local status=$?
        
        # Ergebnis zurückmelden
        echo "$worker_id:$task:$status:$result" > "$RESULT_QUEUE"
    done < "$TASK_QUEUE"
    
    echo "Worker $worker_id beendet"
}

# Worker starten
for ((i=1; i<=NUM_WORKERS; i++)); do
    worker $i &
done

# Funktion zum Hinzufügen einer Aufgabe zur Warteschlange
add_task() {
    echo "$1" > "$TASK_QUEUE"
}

# Funktion zum Sammeln von Ergebnissen
collect_results() {
    local expected=$1
    local count=0
    local results=()
    
    while [ $count -lt $expected ]; do
        # Auf ein Ergebnis warten
        if read result < "$RESULT_QUEUE"; then
            results+=("$result")
            ((count++))
        fi
    done
    
    # Ergebnisse zurückgeben
    for result in "${results[@]}"; do
        echo "$result"
    done
}

# Beispielverwendung: Aufgaben zur Warteschlange hinzufügen
TASKS=(
    "sleep 2 && echo 'Aufgabe 1 abgeschlossen'"
    "sleep 1 && echo 'Aufgabe 2 abgeschlossen'"
    "sleep 3 && echo 'Aufgabe 3 abgeschlossen'"
    "sleep 1 && echo 'Aufgabe 4 abgeschlossen'"
    "sleep 2 && echo 'Aufgabe 5 abgeschlossen'"
    "sleep 1 && echo 'Aufgabe 6 abgeschlossen'"
)

# Aufgaben senden
for task in "${TASKS[@]}"; do
    add_task "$task"
done

# Ergebnisse sammeln
echo "Ergebnisse:"
collect_results ${#TASKS[@]} | sort

# Worker beenden
for ((i=1; i<=NUM_WORKERS; i++)); do
    add_task "TERMINATE"
done

# Warten, bis alle Worker beendet sind
wait
echo "Alle Worker beendet"

14.5.6 Parallelisierung mit Fork-Join-Modell

Das Fork-Join-Muster ist ein gängiges Paradigma für parallele Verarbeitung:

#!/bin/bash

# Funktion zur parallelen Verarbeitung von Teilen einer Aufgabe
fork_join() {
    local num_parts=$1
    local processor=$2
    local input=$3
    local output=$4
    
    # Verzeichnis für temporäre Dateien
    local temp_dir=$(mktemp -d)
    
    # Aufgabe in Teile aufteilen (Fork)
    split -n l/$num_parts "$input" "$temp_dir/part_"
    
    # Teile parallel verarbeiten
    ls "$temp_dir/part_"* | xargs -P "$num_parts" -I {} \
        bash -c "$processor {} {}.out"
    
    # Ergebnisse zusammenführen (Join)
    cat "$temp_dir"/part_*.out > "$output"
    
    # Aufräumen
    rm -rf "$temp_dir"
}

# Beispielverwendung für Textverarbeitung
processor="grep 'ERROR' | sort -u"
fork_join 4 "$processor" "große_logdatei.log" "fehler_zusammenfassung.txt"

14.5.7 Verteilte Parallelisierung über mehrere Systeme

Für rechenintensive Aufgaben können Prozesse auf mehrere Maschinen verteilt werden:

#!/bin/bash

# Liste der verfügbaren Server
SERVERS=("server1" "server2" "server3" "server4")

# Funktion zum Ausführen eines Befehls auf einem Remote-Server
run_on_server() {
    local server=$1
    local command=$2
    
    echo "Führe aus auf $server: $command"
    ssh "$server" "$command"
}

# Funktion zum Verteilen von Aufgaben auf Server
distribute_tasks() {
    local tasks=("$@")
    local num_tasks=${#tasks[@]}
    local num_servers=${#SERVERS[@]}
    local pids=()
    
    # Aufgaben auf Server verteilen
    for ((i=0; i<num_tasks; i++)); do
        local server_idx=$((i % num_servers))
        local server=${SERVERS[$server_idx]}
        local task=${tasks[$i]}
        
        # Aufgabe im Hintergrund auf Server ausführen
        run_on_server "$server" "$task" &
        pids+=($!)
    done
    
    # Auf Abschluss aller Aufgaben warten
    for pid in "${pids[@]}"; do
        wait $pid
    done
}

# Beispielverwendung
TASKS=(
    "find /var/log -name '*.log' -type f -mtime -1 | xargs grep 'ERROR'"
    "find /var/log -name '*.log' -type f -mtime -1 | xargs grep 'WARNING'"
    "find /var/log -name '*.log' -type f -mtime -1 | xargs grep 'CRITICAL'"
    "find /var/log -name '*.log' -type f -mtime -1 | xargs grep 'FATAL'"
)

distribute_tasks "${TASKS[@]}"

Mit GNU Parallel kann dies noch einfacher gestaltet werden:

parallel --sshlogin server1,server2,server3,server4 ./process.sh ::: input1 input2 input3 input4

14.5.8 Asynchrone Parallelisierung

Für I/O-lastige Aufgaben kann ein asynchrones Modell effizienter sein:

#!/bin/bash

# Maximale Anzahl gleichzeitiger Aufgaben
MAX_CONCURRENT=10

# Zähler für laufende Aufgaben
declare -i running=0

# Funktion für asynchrone Ausführung
async_run() {
    local command=$1
    
    # Aufgabe starten
    {
        eval "$command"
        # Signal senden, wenn Aufgabe beendet ist
        kill -USR1 $$
    } &
    
    ((running++))
    
    # Warten, wenn maximale Anzahl erreicht ist
    if [ $running -ge $MAX_CONCURRENT ]; then
        wait -n  # Warten auf Beendigung eines beliebigen Prozesses
        ((running--))
    fi
}

# Signal-Handler für beendete Aufgaben
handle_task_completion() {
    ((running > 0)) && ((running--))
}
trap handle_task_completion USR1

# Beispielverwendung mit URL-Anfragen
URLS=(
    "https://example.com/api/resource1"
    "https://example.com/api/resource2"
    "https://example.com/api/resource3"
    # ...weitere URLs...
)

for url in "${URLS[@]}"; do
    async_run "curl -s '$url' > '$(basename "$url").json'"
done

# Auf Abschluss aller verbleibenden Aufgaben warten
wait

14.5.9 Steuerung der Parallelität basierend auf Systemressourcen

Für adaptive Parallelität basierend auf verfügbaren Ressourcen:

#!/bin/bash

# Funktion zur Bestimmung der optimalen Parallelität
get_optimal_parallelism() {
    # CPU-Kerne ermitteln
    local cpu_cores=$(nproc)
    
    # Verfügbaren Arbeitsspeicher in MB ermitteln
    local mem_available=$(free -m | awk '/^Mem:/ {print $7}')
    
    # Geschätzter Speicherbedarf pro Prozess in MB
    local mem_per_process=200
    
    # Maximale Prozesse basierend auf CPU und Speicher berechnen
    local max_by_cpu=$cpu_cores
    local max_by_mem=$((mem_available / mem_per_process))
    
    # Minimum wählen, aber mindestens 1
    local optimal=$(( max_by_cpu < max_by_mem ? max_by_cpu : max_by_mem ))
    echo $((optimal > 0 ? optimal : 1))
}

# Optimale Parallelität ermitteln
PARALLEL_JOBS=$(get_optimal_parallelism)
echo "Starte $PARALLEL_JOBS parallele Jobs basierend auf Systemressourcen"

# Mit xargs verwenden
find . -name "*.csv" | xargs -P $PARALLEL_JOBS -I {} ./process_file.sh {}

14.5.10 Best Practices für parallele Shell-Skripte

  1. Ressourcenkontrolle berücksichtigen:

    # Limitierte Ressourcen pro Prozess
    parallel 'ulimit -v 500000; ./memory_intensive_task {}' ::: inputs/*
  2. Zuverlässiges Logging implementieren:

    # Jeder parallele Prozess schreibt in eine eigene Logdatei
    parallel --joblog parallel.log './task.sh {} > logs/{}.log 2>&1' ::: inputs/*
  3. Wiederaufnahme abgebrochener Jobs:

    # GNU Parallel kann unterbrochene Jobs wieder aufnehmen
    parallel --resume --joblog joblog.txt './task.sh {}' ::: inputs/*
  4. Kommunikation zwischen parallelen Prozessen:

    Für Prozesse, die miteinander kommunizieren müssen, können Named Pipes verwendet werden:

    mkfifo /tmp/pipe1 /tmp/pipe2
    
    # Prozess 1
    {
        while read line < /tmp/pipe1; do
            echo "Prozess 1 empfing: $line"
            echo "Antwort von Prozess 1" > /tmp/pipe2
        done
    } &
    
    # Prozess 2
    {
        echo "Nachricht von Prozess 2" > /tmp/pipe1
        read response < /tmp/pipe2
        echo "Prozess 2 empfing: $response"
    } &
    
    wait
    rm -f /tmp/pipe1 /tmp/pipe2
  5. Fehlerbehandlung und Robustheit:

    # Parallele Aufgaben mit Fehlerbehandlung
    parallel --halt soon,fail=1 './task.sh {}' ::: inputs/*

14.6 Synchronisation und Kommunikation zwischen Prozessen

Die effektive Zusammenarbeit mehrerer gleichzeitig laufender Prozesse erfordert Synchronisations- und Kommunikationsmechanismen. In der Shell-Programmierung stehen verschiedene Techniken zur Verfügung, um diese Herausforderungen zu bewältigen. Dieser Abschnitt behandelt die wichtigsten Methoden, mit denen Prozesse koordiniert werden und Daten austauschen können.

14.6.1 Grundlegende Synchronisationsmechanismen

14.6.1.1 Warten auf Prozessabschluss mit wait

Der wait-Befehl ist der einfachste Mechanismus zur Prozesssynchronisation:

#!/bin/bash

# Prozesse im Hintergrund starten
process1 &
PID1=$!

process2 &
PID2=$!

# Auf einen bestimmten Prozess warten
wait $PID1
echo "Prozess 1 wurde abgeschlossen"

# Auf alle Hintergrundprozesse warten
wait
echo "Alle Prozesse wurden abgeschlossen"

Der wait-Befehl unterstützt auch das Abfragen des Exit-Status:

process_data &
wait $!
if [ $? -eq 0 ]; then
    echo "Datenverarbeitung erfolgreich"
else
    echo "Fehler bei der Datenverarbeitung"
fi

14.6.1.2 Timeout für wait implementieren

Mit wait -n (verfügbar in neueren Bash-Versionen) kann auf die Beendigung eines beliebigen Child-Prozesses gewartet werden:

#!/bin/bash

# Timeout-Funktion
wait_with_timeout() {
    local pid=$1
    local timeout=$2
    
    # Timeout-Prozess starten
    (
        sleep $timeout
        kill -0 $pid 2>/dev/null && kill -TERM $pid
    ) &
    local timeout_pid=$!
    
    # Auf den Hauptprozess warten
    wait $pid
    local exit_status=$?
    
    # Timeout-Prozess beenden
    kill $timeout_pid 2>/dev/null
    
    return $exit_status
}

# Beispielverwendung
long_running_process &
process_pid=$!

if wait_with_timeout $process_pid 60; then
    echo "Prozess erfolgreich abgeschlossen"
else
    echo "Prozess wurde nach Timeout beendet oder schlug fehl"
fi

14.6.2 Dateibasierte Synchronisation

14.6.2.1 Lockfiles

Lockfiles sind eine einfache Methode zur Synchronisation zwischen Prozessen:

#!/bin/bash

LOCKFILE="/tmp/myprocess.lock"

# Funktion zum Erwerben des Locks
acquire_lock() {
    # Atomares Erstellen der Lockdatei mit Shell-Umleitung
    if (set -o noclobber; echo "$$" > "$LOCKFILE") 2>/dev/null; then
        trap 'rm -f "$LOCKFILE"; exit $?' INT TERM EXIT
        return 0
    else
        return 1
    fi
}

# Funktion zum Freigeben des Locks
release_lock() {
    rm -f "$LOCKFILE"
    trap - INT TERM EXIT
}

# Versuchen, den Lock zu erwerben
if ! acquire_lock; then
    echo "Ein anderer Prozess läuft bereits (PID: $(cat "$LOCKFILE"))"
    exit 1
fi

echo "Lock erworben, kritischer Abschnitt beginnt"

# Kritischer Abschnitt
sleep 10
echo "Kritische Operation wird ausgeführt"

# Lock freigeben
release_lock
echo "Lock freigegeben, kritischer Abschnitt beendet"

14.6.2.2 Lockfiles mit flock

Das flock-Programm bietet eine zuverlässigere Methode für Dateilocks:

#!/bin/bash

LOCKFILE="/tmp/myprocess.lock"

# Kritischen Abschnitt mit flock schützen
(
    # Versuchen, einen exklusiven Lock zu erwerben, ohne zu blockieren
    if ! flock -n 200; then
        echo "Lock konnte nicht erworben werden. Ein anderer Prozess läuft bereits."
        exit 1
    fi
    
    echo "Lock erworben, kritischer Abschnitt beginnt"
    sleep 10
    echo "Kritische Operation wird ausgeführt"
    echo "Lock wird freigegeben"
    
    # Lock wird automatisch beim Schließen des Filedeskriptors freigegeben
) 200>"$LOCKFILE"

Der flock-Befehl kann auch verwendet werden, um parallele Ausführungen zu begrenzen:

#!/bin/bash

# Maximal 3 gleichzeitige Instanzen erlauben
MAX_INSTANCES=3

for i in {1..10}; do
    (
        # Versuchen, einen Lock auf Filedeskriptor 200 zu erwerben
        # -w 0: nicht warten, wenn kein Lock verfügbar
        flock -w 0 200 || { echo "Instance $i: Konnte keinen Lock erhalten"; exit 1; }
        
        echo "Instance $i: Lock erworben, Verarbeitung beginnt"
        sleep $((RANDOM % 5 + 1))
        echo "Instance $i: Verarbeitung abgeschlossen"
    ) 200>/tmp/limited_instances.lock.$((i % MAX_INSTANCES)) &
done

# Auf Abschluss aller Hintergrundprozesse warten
wait

14.6.3 Interprozesskommunikation (IPC)

14.6.3.1 Pipes (Anonyme Pipes)

Pipes sind der grundlegendste Mechanismus zur Kommunikation zwischen Prozessen:

#!/bin/bash

# Standard-Pipeline
ls -la | grep "^d" | sort -k 5 -n

# Pipeline in einer Variablen speichern
directories=$(ls -la | grep "^d" | sort -k 5 -n)
echo "Gefundene Verzeichnisse: $directories"

# Process Substitution für bidirektionale Kommunikation
diff <(ls -la dir1) <(ls -la dir2)

Ein praktisches Beispiel für die Verwendung von Pipes in Skripten:

#!/bin/bash

# Producer-Consumer-Muster mit Pipes
producer() {
    for i in {1..10}; do
        echo "Daten $i"
        sleep 1
    done
}

consumer() {
    while read line; do
        echo "Verarbeite: $line"
        # Datenverarbeitung hier
    done
}

# Producer und Consumer über eine Pipe verbinden
producer | consumer

14.6.3.2 Named Pipes (FIFOs)

Named Pipes ermöglichen die Kommunikation zwischen nicht verwandten Prozessen:

#!/bin/bash

FIFO_PATH="/tmp/myfifo"

# FIFO erstellen, falls sie nicht existiert
if [ ! -p "$FIFO_PATH" ]; then
    mkfifo "$FIFO_PATH"
fi

# Aufräumen beim Beenden
trap 'rm -f "$FIFO_PATH"' EXIT

# Beispiel: Einfacher Server mit Named Pipe
echo "Server gestartet. Senden Sie Nachrichten an $FIFO_PATH"
echo "Beenden mit Ctrl+C"

while true; do
    if read -r message < "$FIFO_PATH"; then
        echo "Empfangen: $message"
        
        # Echo-Antwort (muss in separater Shell erfolgen, um Blockierung zu vermeiden)
        (echo "ECHO: $message" > "$FIFO_PATH") &
    fi
done

Ein Client kann mit dem Server kommunizieren:

#!/bin/bash

FIFO_PATH="/tmp/myfifo"

# Prüfen, ob FIFO existiert
if [ ! -p "$FIFO_PATH" ]; then
    echo "Server nicht aktiv (FIFO nicht gefunden)"
    exit 1
fi

# Nachricht senden
echo "Nachricht vom Client: $(date)" > "$FIFO_PATH"

# Auf Antwort warten (mit Timeout)
if read -t 5 response < "$FIFO_PATH"; then
    echo "Antwort vom Server: $response"
else
    echo "Timeout beim Warten auf Antwort"
fi

14.6.3.3 Bidirektionale Kommunikation mit Named Pipes

Für eine vollständige bidirektionale Kommunikation werden zwei FIFOs benötigt:

#!/bin/bash

# Server-Skript
SERVER_TO_CLIENT="/tmp/server_to_client"
CLIENT_TO_SERVER="/tmp/client_to_server"

# FIFOs erstellen
mkfifo "$SERVER_TO_CLIENT" "$CLIENT_TO_SERVER"

# Aufräumen beim Beenden
cleanup() {
    echo "Server wird beendet"
    rm -f "$SERVER_TO_CLIENT" "$CLIENT_TO_SERVER"
    exit 0
}
trap cleanup EXIT INT TERM

echo "Server gestartet"

# Hauptschleife für Anfragen
while true; do
    if read request < "$CLIENT_TO_SERVER"; then
        echo "Anfrage empfangen: $request"
        
        # Antwort verarbeiten und zurücksenden
        response="Server hat '$request' verarbeitet um $(date)"
        echo "$response" > "$SERVER_TO_CLIENT"
    fi
done

Der entsprechende Client:

#!/bin/bash

# Client-Skript
SERVER_TO_CLIENT="/tmp/server_to_client"
CLIENT_TO_SERVER="/tmp/client_to_server"

# Prüfen, ob FIFOs existieren
if [ ! -p "$SERVER_TO_CLIENT" ] || [ ! -p "$CLIENT_TO_SERVER" ]; then
    echo "Server nicht aktiv (FIFOs nicht gefunden)"
    exit 1
fi

# Funktion zum Senden einer Anfrage und Empfangen der Antwort
send_request() {
    local request=$1
    
    # Anfrage senden
    echo "$request" > "$CLIENT_TO_SERVER"
    
    # Auf Antwort warten (mit Timeout)
    if read -t 5 response < "$SERVER_TO_CLIENT"; then
        echo "Antwort: $response"
    else
        echo "Timeout beim Warten auf Antwort"
    fi
}

# Beispielanfragen senden
send_request "Hallo Server"
send_request "Wie spät ist es?"

14.6.4 Temporäre Dateien für IPC

Temporäre Dateien können für einfache Kommunikation verwendet werden:

#!/bin/bash

# Temporäre Datei für die Kommunikation erstellen
TEMP_FILE=$(mktemp)

# Aufräumen beim Beenden
trap 'rm -f "$TEMP_FILE"' EXIT

# Prozess 1 schreibt in die temporäre Datei
{
    echo "Daten von Prozess 1" > "$TEMP_FILE"
    echo "Weitere Daten" >> "$TEMP_FILE"
    echo "FERTIG" >> "$TEMP_FILE"
} &

# Prozess 2 liest aus der temporären Datei
{
    # Warten, bis Daten verfügbar sind
    while [ ! -s "$TEMP_FILE" ]; do
        sleep 0.1
    done
    
    # Auf das Ende-Signal warten
    until grep -q "FERTIG" "$TEMP_FILE"; do
        sleep 0.1
    done
    
    # Daten verarbeiten (ohne die FERTIG-Zeile)
    grep -v "FERTIG" "$TEMP_FILE"
} &

# Auf beide Prozesse warten
wait

14.6.5 Signale für die Prozesssynchronisation

Signale eignen sich gut für einfache Synchronisationsanforderungen:

#!/bin/bash

# Signal-Handler für verschiedene Ereignisse
synchronize() {
    case $1 in
        start)
            echo "Start-Signal empfangen, beginne Verarbeitung"
            ;;
        pause)
            echo "Pause-Signal empfangen, pausiere Verarbeitung"
            ;;
        resume)
            echo "Resume-Signal empfangen, setze Verarbeitung fort"
            ;;
        stop)
            echo "Stop-Signal empfangen, beende Verarbeitung"
            exit 0
            ;;
    esac
}

# Signal-Handler einrichten
trap 'synchronize start' USR1
trap 'synchronize pause' USR2
trap 'synchronize resume' HUP
trap 'synchronize stop' TERM

echo "Prozess gestartet mit PID: $$"
echo "Steuern Sie diesen Prozess mit:"
echo "  kill -USR1 $$ # Start"
echo "  kill -USR2 $$ # Pause"
echo "  kill -HUP $$ # Resume"
echo "  kill -TERM $$ # Stop"

# Hauptschleife
while true; do
    sleep 1
done

14.6.5.1 Barrier-Synchronisation mit Signalen

Implementierung einer Barrier, an der mehrere Prozesse warten müssen:

#!/bin/bash

# Konfiguration
NUM_PROCESSES=4
BARRIER_FILE="/tmp/barrier_$$"

# Initialisierung der Barrier
echo "0" > "$BARRIER_FILE"

# Aufräumen beim Beenden
trap 'rm -f "$BARRIER_FILE"' EXIT

# Funktion für die Barrier-Synchronisation
barrier_wait() {
    local id=$1
    
    echo "Prozess $id erreicht die Barrier"
    
    # Atomare Inkrementierung des Zählers (mit flock für Thread-Sicherheit)
    (
        flock -x 200
        count=$(<"$BARRIER_FILE")
        count=$((count + 1))
        echo $count > "$BARRIER_FILE"
        
        if [ $count -eq $NUM_PROCESSES ]; then
            # Letzter Prozess erreicht die Barrier, Signal an alle senden
            echo "Alle Prozesse haben die Barrier erreicht"
            # Zähler zurücksetzen
            echo "0" > "$BARRIER_FILE"
            # Signal an die Prozessgruppe senden
            kill -USR1 -$$
        fi
    ) 200>"$BARRIER_FILE.lock"
    
    # Auf das Signal warten, sofern es nicht der letzte Prozess war
    if [ $count -lt $NUM_PROCESSES ]; then
        echo "Prozess $id wartet an der Barrier"
        # Warten auf das Signal
        trap 'echo "Prozess $id überschreitet die Barrier"; trap - USR1' USR1
        while true; do
            sleep 0.1
        done
    fi
}

# Beispielprozesse starten
for i in $(seq 1 $NUM_PROCESSES); do
    (
        # Phase 1 der Verarbeitung
        sleep $((RANDOM % 5 + 1))
        echo "Prozess $i hat Phase 1 abgeschlossen"
        
        # Auf alle Prozesse warten
        barrier_wait $i
        
        # Phase 2 der Verarbeitung
        sleep $((RANDOM % 3 + 1))
        echo "Prozess $i hat Phase 2 abgeschlossen"
    ) &
done

# Auf Abschluss aller Prozesse warten
wait

14.6.6 Sockets für die Netzwerkkommunikation

Mit netcat (nc) können Shell-Skripte über Netzwerk-Sockets kommunizieren:

#!/bin/bash

# Server-Skript
server() {
    local port=8888
    
    echo "Server startet auf Port $port"
    
    # Einfacher Echo-Server mit netcat
    while true; do
        nc -l $port | while read line; do
            echo "Empfangen: $line"
            echo "ECHO: $line" | nc -l $port
        done
    done
}

# Client-Skript
client() {
    local host="localhost"
    local port=8888
    local message="Hallo vom Client: $(date)"
    
    echo "Sende Nachricht an $host:$port: $message"
    
    # Nachricht senden und Antwort empfangen
    response=$(echo "$message" | nc $host $port)
    
    echo "Antwort erhalten: $response"
}

# Verwendung:
# ./script.sh server  # Startet den Server
# ./script.sh client  # Sendet eine Nachricht als Client

case "$1" in
    server)
        server
        ;;
    client)
        client
        ;;
    *)
        echo "Verwendung: $0 {server|client}"
        exit 1
        ;;
esac

14.6.7 DBus für die Systemkommunikation

DBus ermöglicht die Kommunikation mit Systemdiensten und anderen Anwendungen:

#!/bin/bash

# Beispiel: Benachrichtigung über DBus senden
send_notification() {
    local title="$1"
    local message="$2"
    
    gdbus call --session \
        --dest org.freedesktop.Notifications \
        --object-path /org/freedesktop/Notifications \
        --method org.freedesktop.Notifications.Notify \
        "Shell-Skript" 0 "dialog-information" \
        "$title" "$message" "[]" "{}" 5000
}

# Beispiel: System herunterfahren über DBus
shutdown_system() {
    gdbus call --system \
        --dest org.freedesktop.login1 \
        --object-path /org/freedesktop/login1 \
        --method org.freedesktop.login1.Manager.PowerOff \
        true
}

# Beispielverwendung
send_notification "Prozessabschluss" "Der langwierige Prozess wurde erfolgreich abgeschlossen."

# Shutdown-Beispiel (auskommentiert)
# shutdown_system

14.6.8 Gemeinsam genutzter Speicher mit /dev/shm

Der POSIX Shared Memory kann für schnelle IPC genutzt werden:

#!/bin/bash

# Gemeinsam genutzter Speicherbereich
SHM_FILE="/dev/shm/myshm_$$"

# Initialisierung
echo "0" > "$SHM_FILE"
chmod 600 "$SHM_FILE"

# Aufräumen beim Beenden
trap 'rm -f "$SHM_FILE"' EXIT

# Prozess 1: Inkrementiert den Zähler
(
    for i in {1..5}; do
        # Atomare Aktualisierung mit flock
        (
            flock -x 200
            current=$(cat "$SHM_FILE")
            echo "Prozess 1: Aktueller Wert: $current"
            echo $((current + 1)) > "$SHM_FILE"
        ) 200>"$SHM_FILE.lock"
        
        sleep 1
    done
) &

# Prozess 2: Liest und meldet den Zähler
(
    for i in {1..7}; do
        # Atomares Lesen mit flock
        (
            flock -s 200
            current=$(cat "$SHM_FILE")
            echo "Prozess 2: Lese Wert: $current"
        ) 200>"$SHM_FILE.lock"
        
        sleep 0.7
    done
) &

# Auf beide Prozesse warten
wait

14.6.9 Semaphore für die Zugriffskontrolle

Ein einfacher Semaphor für die Zugriffskontrolle auf gemeinsame Ressourcen:

#!/bin/bash

# Semaphor-Datei
SEM_FILE="/tmp/semaphore_$$"

# Semaphor initialisieren (Wert = Anzahl erlaubter gleichzeitiger Zugriffe)
echo "3" > "$SEM_FILE"
chmod 600 "$SEM_FILE"

# Aufräumen beim Beenden
trap 'rm -f "$SEM_FILE"' EXIT

# Funktion zum Erwerben des Semaphors
sem_acquire() {
    local timeout=${1:-30}  # Standardtimeout: 30 Sekunden
    local start_time=$(date +%s)
    
    while true; do
        # Atomarer Zugriff auf den Semaphor
        (
            flock -x 200
            
            # Aktuellen Wert lesen
            local count=$(cat "$SEM_FILE")
            
            if [ $count -gt 0 ]; then
                # Semaphor erfolgreich erworben
                echo $((count - 1)) > "$SEM_FILE"
                return 0
            fi
        ) 200>"$SEM_FILE.lock"
        
        # Prüfen, ob das Timeout erreicht wurde
        if [ $(($(date +%s) - start_time)) -ge $timeout ]; then
            return 1  # Timeout
        fi
        
        # Kurz warten, bevor erneut versucht wird
        sleep 0.1
    done
}

# Funktion zum Freigeben des Semaphors
sem_release() {
    # Atomare Inkrementierung
    (
        flock -x 200
        local count=$(cat "$SEM_FILE")
        echo $((count + 1)) > "$SEM_FILE"
    ) 200>"$SEM_FILE.lock"
}

# Beispiel: Mehrere Prozesse, die auf eine begrenzte Ressource zugreifen
for i in {1..10}; do
    (
        echo "Prozess $i versucht, den Semaphor zu erwerben"
        
        if sem_acquire 5; then
            echo "Prozess $i hat den Semaphor erworben"
            
            # Kritischer Abschnitt
            sleep $((RANDOM % 3 + 1))
            echo "Prozess $i arbeitet im kritischen Abschnitt"
            
            # Semaphor freigeben
            sem_release
            echo "Prozess $i hat den Semaphor freigegeben"
        else
            echo "Prozess $i: Timeout beim Erwerben des Semaphors"
        fi
    ) &
done

# Auf alle Prozesse warten
wait

14.7 Resource-Management für laufende Prozesse

Die effektive Verwaltung von Systemressourcen ist ein entscheidender Aspekt professioneller Shell-Skripte, insbesondere bei rechenintensiven oder langlebigen Prozessen. Dieser Abschnitt behandelt Techniken und Werkzeuge zum Überwachen, Steuern und Optimieren der Ressourcennutzung von Prozessen in Shell-Skripten.

14.7.1 Grundlegende Ressourcenlimits mit ulimit

Der ulimit-Befehl ermöglicht die Kontrolle der Ressourcen, die einem Prozess und seinen Kindprozessen zur Verfügung stehen:

#!/bin/bash

# Aktuelle Ressourcenlimits anzeigen
echo "Aktuelle Ressourcenlimits:"
ulimit -a

# Maximale Anzahl offener Dateien erhöhen
echo "Erhöhe das Limit für offene Dateien auf 4096"
ulimit -n 4096

# Maximale Dateigröße begrenzen (in Kilobytes)
echo "Begrenze die maximale Dateigröße auf 100 MB"
ulimit -f 102400

# Limitierte Core-Dump-Größe für besseres Debugging
echo "Setze Core-Dump-Größe auf 0"
ulimit -c 0

# Speicherbegrenzung (in Kilobytes)
echo "Begrenze virtuellen Speicher auf 2 GB"
ulimit -v 2097152

# CPU-Zeit begrenzen (in Sekunden)
echo "Begrenze CPU-Zeit auf 60 Sekunden"
ulimit -t 60

# Prozess mit diesen Limits starten
echo "Starte Prozess mit den konfigurierten Limits"
resource_intensive_command

Wichtige ulimit-Optionen:

Option Beschreibung
-t Maximale CPU-Zeit in Sekunden
-f Maximale Dateigröße in Kilobytes
-d Maximaler Prozess-Datensegment
-s Maximale Stack-Größe
-c Maximale Core-Datei-Größe
-v Maximaler virtueller Speicher
-n Maximale Anzahl offener Dateien
-u Maximale Anzahl von Benutzerprozessen
-m Maximale Speichernutzung

14.7.2 Prozess-Priorisierung mit nice und renice

Die Befehle nice und renice ermöglichen die Steuerung der Prozesspriorität:

#!/bin/bash

# Prozess mit niedriger Priorität starten
echo "Starte rechenintensiven Prozess mit niedriger Priorität"
nice -n 19 ./intensive_calculation.sh &
PID_LOW=$!

# Prozess mit normaler Priorität starten
echo "Starte rechenintensiven Prozess mit normaler Priorität"
./intensive_calculation.sh &
PID_NORMAL=$!

# Prozess mit hoher Priorität starten (erfordert root-Rechte)
echo "Starte rechenintensiven Prozess mit hoher Priorität"
if [ $(id -u) -eq 0 ]; then
    nice -n -10 ./intensive_calculation.sh &
    PID_HIGH=$!
else
    echo "Erhöhte Priorität erfordert root-Rechte"
fi

# Priorität eines laufenden Prozesses ändern
echo "Ändere Priorität eines laufenden Prozesses"
renice +5 -p $PID_NORMAL

Die nice-Werte reichen von -20 (höchste Priorität) bis 19 (niedrigste Priorität). Nur Benutzer mit entsprechenden Rechten können Prozesse mit negativem Nice-Wert ausführen.

14.7.3 I/O-Priorisierung mit ionice

Der ionice-Befehl steuert die I/O-Scheduling-Klasse und -Priorität:

#!/bin/bash

# Prüfen, ob ionice verfügbar ist
if ! command -v ionice &> /dev/null; then
    echo "ionice ist nicht installiert"
    exit 1
fi

# Prozess mit niedriger I/O-Priorität starten
echo "Starte I/O-intensiven Prozess mit niedriger Priorität"
ionice -c 3 ./io_intensive_task.sh &
PID_LOW=$!

# Prozess mit normaler I/O-Priorität (Best-Effort)
echo "Starte I/O-intensiven Prozess mit normaler Priorität"
ionice -c 2 -n 4 ./io_intensive_task.sh &
PID_NORMAL=$!

# Prozess mit hoher I/O-Priorität (erfordert root-Rechte)
echo "Starte I/O-intensiven Prozess mit hoher Priorität"
if [ $(id -u) -eq 0 ]; then
    ionice -c 1 -n 0 ./io_intensive_task.sh &
    PID_HIGH=$!
else
    echo "Echtzeitklasse erfordert root-Rechte"
fi

Die I/O-Scheduling-Klassen sind: - 1: Echtzeit (höchste Priorität, nur root) - 2: Best-Effort (Standard, mit Prioritäten 0-7) - 3: Idle (niedrigste Priorität, nur wenn keine andere I/O-Aktivität vorhanden ist)

14.7.4 Ressourcenbegrenzung mit cgroups

Control Groups (cgroups) bieten eine leistungsstarke Methode zur Begrenzung, Zuweisung und Überwachung von Systemressourcen:

#!/bin/bash

# Prüfen, ob cgcreate verfügbar ist
if ! command -v cgcreate &> /dev/null; then
    echo "cgcreate ist nicht verfügbar. Installieren Sie libcgroup-tools."
    exit 1
fi

# Name der Kontrollgruppe
CGROUP_NAME="mygroup"

# Kontrollgruppe erstellen (erfordert root-Rechte)
if [ $(id -u) -eq 0 ]; then
    # CPU-Kontrolle
    cgcreate -g cpu:/$CGROUP_NAME
    # Begrenze auf 50% der CPU-Zeit
    echo 50000 > /sys/fs/cgroup/cpu/$CGROUP_NAME/cpu.cfs_quota_us
    echo 100000 > /sys/fs/cgroup/cpu/$CGROUP_NAME/cpu.cfs_period_us
    
    # Speicherkontrolle
    cgcreate -g memory:/$CGROUP_NAME
    # Begrenze auf 1 GB Speicher
    echo 1073741824 > /sys/fs/cgroup/memory/$CGROUP_NAME/memory.limit_in_bytes
    
    # Prozess in der Kontrollgruppe ausführen
    cgexec -g cpu,memory:/$CGROUP_NAME ./resource_intensive_process.sh
    
    # Kontrollgruppe aufräumen
    cgdelete -g cpu,memory:/$CGROUP_NAME
else
    echo "cgroup-Manipulation erfordert root-Rechte"
fi

Für systemd-basierte Systeme kann systemd-run verwendet werden:

#!/bin/bash

# Temporäre Unit mit Ressourcenbegrenzungen erstellen und Prozess starten
if command -v systemd-run &> /dev/null; then
    echo "Starte Prozess mit systemd-run und Ressourcenbegrenzungen"
    systemd-run --user --scope \
        --property=CPUQuota=50% \
        --property=MemoryLimit=1G \
        --property=IOWeight=100 \
        ./resource_intensive_process.sh
else
    echo "systemd-run ist nicht verfügbar"
fi

14.7.5 Überwachung der Ressourcennutzung

14.7.5.1 top und htop für interaktive Überwachung

#!/bin/bash

# Prozess im Hintergrund starten
./intensive_process.sh &
PROCESS_PID=$!

# Prozess mit top überwachen
echo "Überwache Prozess mit PID $PROCESS_PID"
top -p $PROCESS_PID

# Alternativ mit htop (falls installiert)
if command -v htop &> /dev/null; then
    htop -p $PROCESS_PID
fi

14.7.5.2 Programmgesteuerte Ressourcenüberwachung mit ps

#!/bin/bash

# Funktion zum Überwachen der Ressourcennutzung eines Prozesses
monitor_process() {
    local pid=$1
    local interval=${2:-5}  # Standardintervall: 5 Sekunden
    local log_file=${3:-"process_stats.log"}
    
    # Header für die Logdatei
    echo "Timestamp,PID,CPU%,MEM%,VSZ,RSS,TIME" > "$log_file"
    
    echo "Überwache Prozess $pid, Intervall: ${interval}s, Log: $log_file"
    
    # Überwachungsschleife
    while kill -0 $pid 2>/dev/null; do
        # Prozessstatistiken sammeln
        local stats=$(ps -p $pid -o pid,pcpu,pmem,vsz,rss,time --no-headers)
        
        if [ -n "$stats" ]; then
            # Zeitstempel hinzufügen und in die Logdatei schreiben
            local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
            echo "$timestamp,$(echo $stats | tr -s ' ' ',')" >> "$log_file"
        fi
        
        sleep $interval
    done
    
    echo "Prozess $pid existiert nicht mehr. Überwachung beendet."
}

# Beispielverwendung
./intensive_process.sh &
PROCESS_PID=$!

# Prozess überwachen (alle 2 Sekunden)
monitor_process $PROCESS_PID 2 "process_stats.log" &
MONITOR_PID=$!

# Main-Prozess beenden, wenn der zu überwachende Prozess beendet ist
wait $PROCESS_PID
kill $MONITOR_PID

14.7.5.3 Speichernutzung überwachen

#!/bin/bash

# Speichernutzung eines Prozesses überwachen
monitor_memory() {
    local pid=$1
    local threshold_mb=$2
    local interval=${3:-5}
    
    echo "Überwache Speichernutzung für PID $pid (Schwellenwert: $threshold_mb MB)"
    
    while kill -0 $pid 2>/dev/null; do
        # Speichernutzung in KB
        local mem_kb=$(ps -p $pid -o rss= 2>/dev/null)
        
        if [ -n "$mem_kb" ]; then
            local mem_mb=$((mem_kb / 1024))
            echo "PID $pid: Speichernutzung: $mem_mb MB"
            
            # Schwellenwert prüfen
            if [ $mem_mb -gt $threshold_mb ]; then
                echo "WARNUNG: Speichernutzung überschreitet Schwellenwert ($threshold_mb MB)"
                # Optional: Maßnahmen ergreifen, z.B.
                # kill -TERM $pid
            fi
        fi
        
        sleep $interval
    done
}

# Beispielverwendung
./memory_intensive_process.sh &
PROCESS_PID=$!

# Speichernutzung überwachen mit 500 MB Schwellenwert
monitor_memory $PROCESS_PID 500 2

14.7.6 Adaptive Ressourcenzuweisung

Ein fortgeschrittenes Skript zur adaptiven Ressourcenzuweisung basierend auf der Systemlast:

#!/bin/bash

# Konfiguration
LOAD_THRESHOLD_HIGH=4.0
LOAD_THRESHOLD_LOW=2.0
CHECK_INTERVAL=30  # Sekunden

# Funktionen zur Prozesssteuerung
throttle_process() {
    local pid=$1
    echo "System unter hoher Last. Drossele Prozess $pid"
    renice +10 -p $pid
    # Für I/O-Drosselung (falls verfügbar)
    command -v ionice &>/dev/null && ionice -c 3 -p $pid
}

unthrottle_process() {
    local pid=$1
    echo "System unter niedriger Last. Normalisiere Prozess $pid"
    renice 0 -p $pid
    # Für I/O-Normalisierung (falls verfügbar)
    command -v ionice &>/dev/null && ionice -c 2 -n 4 -p $pid
}

# Hauptprozess starten
echo "Starte Hauptprozess"
./main_process.sh &
MAIN_PID=$!

# Niedrigere Prioritätsprozesse starten
echo "Starte Hintergrundprozesse"
for i in {1..3}; do
    ./background_process.sh &
    BG_PIDS+=($!)
done

# Überwachungsschleife
echo "Starte adaptive Ressourcenüberwachung"
throttled=false

while kill -0 $MAIN_PID 2>/dev/null; do
    # Aktuelle Systemlast ermitteln (1-Minuten-Load)
    load=$(cat /proc/loadavg | cut -d ' ' -f 1)
    echo "Aktuelle Systemlast: $load"
    
    # Load-Schwellenwerte prüfen und Maßnahmen ergreifen
    if (( $(echo "$load > $LOAD_THRESHOLD_HIGH" | bc -l) )); then
        if [ "$throttled" = false ]; then
            echo "Hohe Systemlast erkannt. Drossele Hintergrundprozesse"
            for pid in "${BG_PIDS[@]}"; do
                if kill -0 $pid 2>/dev/null; then
                    throttle_process $pid
                fi
            done
            throttled=true
        fi
    elif (( $(echo "$load < $LOAD_THRESHOLD_LOW" | bc -l) )); then
        if [ "$throttled" = true ]; then
            echo "Niedrige Systemlast erkannt. Normalisiere Hintergrundprozesse"
            for pid in "${BG_PIDS[@]}"; do
                if kill -0 $pid 2>/dev/null; then
                    unthrottle_process $pid
                fi
            done
            throttled=false
        fi
    fi
    
    sleep $CHECK_INTERVAL
done

# Aufräumen
echo "Hauptprozess beendet. Beende Hintergrundprozesse"
for pid in "${BG_PIDS[@]}"; do
    kill $pid 2>/dev/null
done

14.7.7 Ressourcenbegrenzung mit cpulimit

Das cpulimit-Tool begrenzt die CPU-Nutzung eines Prozesses auf einen bestimmten Prozentsatz:

#!/bin/bash

# Prüfen, ob cpulimit verfügbar ist
if ! command -v cpulimit &> /dev/null; then
    echo "cpulimit ist nicht installiert"
    echo "Installieren Sie es mit: apt-get install cpulimit (Debian/Ubuntu)"
    exit 1
fi

# Prozess starten und seine CPU-Nutzung begrenzen
echo "Starte CPU-intensiven Prozess mit Begrenzung auf 30% CPU"
./cpu_intensive_process.sh &
PROCESS_PID=$!

# CPU-Nutzung auf 30% begrenzen
cpulimit -p $PROCESS_PID -l 30 &
CPULIMIT_PID=$!

# Auf Abschluss des Hauptprozesses warten
wait $PROCESS_PID

# cpulimit beenden
kill $CPULIMIT_PID 2>/dev/null

14.7.8 Ressourcenüberwachung mit sar

Das sar-Tool (Teil des sysstat-Pakets) bietet detaillierte Systemressourcenstatistiken:

#!/bin/bash

# Prüfen, ob sar verfügbar ist
if ! command -v sar &> /dev/null; then
    echo "sar ist nicht installiert"
    echo "Installieren Sie es mit: apt-get install sysstat (Debian/Ubuntu)"
    exit 1
fi

# Systemressourcen vor dem Prozessstart erfassen
echo "Erfasse Baseline-Systemressourcen"
sar -o before.sar 1 5 >/dev/null

# Prozess starten
echo "Starte den zu überwachenden Prozess"
./resource_intensive_process.sh &
PROCESS_PID=$!

# Ressourcen während der Prozessausführung überwachen
echo "Überwache Systemressourcen während der Prozessausführung"
sar -o during.sar 1 30 >/dev/null &
SAR_PID=$!

# Auf Prozessabschluss warten
wait $PROCESS_PID

# sar-Überwachung beenden
kill $SAR_PID 2>/dev/null
sleep 2

# Systemressourcen nach dem Prozessende erfassen
echo "Erfasse Systemressourcen nach Prozessende"
sar -o after.sar 1 5 >/dev/null

# Zusammenfassung erstellen
echo "Erstelle Ressourcennutzungsbericht"
echo "=== CPU-Nutzung ===" > resource_report.txt
echo "Vor der Ausführung:" >> resource_report.txt
sar -u -f before.sar | tail -n 6 >> resource_report.txt
echo "Während der Ausführung:" >> resource_report.txt
sar -u -f during.sar | tail -n 6 >> resource_report.txt
echo "Nach der Ausführung:" >> resource_report.txt
sar -u -f after.sar | tail -n 6 >> resource_report.txt

echo "=== Speichernutzung ===" >> resource_report.txt
echo "Vor der Ausführung:" >> resource_report.txt
sar -r -f before.sar | tail -n 6 >> resource_report.txt
echo "Während der Ausführung:" >> resource_report.txt
sar -r -f during.sar | tail -n 6 >> resource_report.txt
echo "Nach der Ausführung:" >> resource_report.txt
sar -r -f after.sar | tail -n 6 >> resource_report.txt

echo "Bericht wurde in resource_report.txt geschrieben"

14.7.9 Prozess-Containerisierung mit unshare

Der unshare-Befehl kann zur einfachen Prozessisolierung verwendet werden:

#!/bin/bash

# Prüfen, ob unshare verfügbar ist
if ! command -v unshare &> /dev/null; then
    echo "unshare ist nicht verfügbar"
    exit 1
fi

# Prozess mit eigenem Prozess- und Netzwerk-Namespace ausführen (erfordert root)
if [ $(id -u) -eq 0 ]; then
    echo "Starte Prozess in isolierter Umgebung"
    unshare --fork --pid --net --mount-proc /bin/bash -c './isolated_process.sh'
else
    echo "Namespace-Isolation erfordert root-Rechte"
fi

14.7.10 Ressourcenbegrenzung für Gruppen von Prozessen

Mehrere zusammengehörige Prozesse können als Prozessgruppe verwaltet werden:

#!/bin/bash

# Funktionen zur Prozessgruppenverwaltung
create_process_group() {
    # Neue Sitzung und Prozessgruppe erstellen
    setsid bash -c "
        # PID der Prozessgruppe speichern
        echo \$$ > $1
        
        # Prozesse in dieser Gruppe starten
        $2 &
        $3 &
        $4 &
        
        # Warten, bis alle Prozesse beendet sind
        wait
    " &
    
    # Kurz warten, damit die PGID-Datei geschrieben werden kann
    sleep 1
}

set_group_priority() {
    local pgid_file=$1
    local nice_value=$2
    
    if [ -f "$pgid_file" ]; then
        local pgid=$(cat "$pgid_file")
        echo "Setze Priorität für Prozessgruppe $pgid auf $nice_value"
        renice $nice_value -g $pgid
    else
        echo "PGID-Datei $pgid_file nicht gefunden"
    fi
}

# Beispielverwendung
PGID_FILE="/tmp/mygroup.pgid"

echo "Erstelle Prozessgruppe mit mehreren Prozessen"
create_process_group "$PGID_FILE" \
    "./process1.sh" \
    "./process2.sh" \
    "./process3.sh"

# Priorität der gesamten Gruppe ändern
sleep 5
set_group_priority "$PGID_FILE" 10

# Auf Benutzereingabe warten, um Prozessgruppe zu beenden
read -p "Drücken Sie Enter, um die Prozessgruppe zu beenden"

if [ -f "$PGID_FILE" ]; then
    kill -TERM -$(cat "$PGID_FILE")
    rm -f "$PGID_FILE"
fi

14.7.11 Best Practices für Ressourcenmanagement

  1. Prozess-Hierarchie beachten:

    # Ressourcenlimits für Hauptprozess und Kindprozesse setzen
    ulimit -v 2097152  # Virtuellen Speicher auf 2 GB begrenzen
    ./parent_process.sh
  2. Feedback-basierte Ressourcenzuweisung:

    # Prozessgeschwindigkeit basierend auf Systemlast anpassen
    while true; do
        load=$(cat /proc/loadavg | cut -d ' ' -f 1)
        if (( $(echo "$load > 3.0" | bc -l) )); then
            # System ist stark ausgelastet, Verarbeitungsrate reduzieren
            export BATCH_SIZE=10
        else
            # System ist nicht stark ausgelastet, Verarbeitungsrate erhöhen
            export BATCH_SIZE=100
        fi
    
        ./process_batch.sh $BATCH_SIZE
        sleep 5
    done
  3. Kritische vs. Hintergrundprozesse:

    # Kritische Prozesse mit höherer Priorität
    nice -n -10 ./critical_process.sh &
    
    # Hintergrundprozesse mit niedrigerer Priorität
    nice -n 19 ./background_process.sh &
  4. Überwachung mit Alarmierung:

    #!/bin/bash
    
    # Funktion zur Benachrichtigung bei Ressourcenproblemen
    notify_admin() {
        local subject="$1"
        local message="$2"
        echo "$message" | mail -s "$subject" admin@example.com
    }
    
    # Prozess überwachen
    while kill -0 $PROCESS_PID 2>/dev/null; do
        # CPU-Nutzung prüfen
        cpu_usage=$(ps -p $PROCESS_PID -o pcpu= | tr -d ' ')
    
        # Speichernutzung prüfen (RSS in KB)
        mem_usage=$(ps -p $PROCESS_PID -o rss= | tr -d ' ')
        mem_usage_mb=$((mem_usage / 1024))
    
        if (( $(echo "$cpu_usage > 90.0" | bc -l) )); then
            notify_admin "Hohe CPU-Nutzung" "Prozess $PROCESS_PID verwendet $cpu_usage% CPU"
        fi
    
        if [ $mem_usage_mb -gt 1000 ]; then
            notify_admin "Hohe Speichernutzung" "Prozess $PROCESS_PID verwendet $mem_usage_mb MB Speicher"
        fi
    
        sleep 60
    done
  5. Graceful Degradation:

    #!/bin/bash
    
    # Funktionsweise basierend auf verfügbaren Ressourcen anpassen
    check_resources() {
        # Verfügbarer Speicher in MB
        local mem_available=$(free -m | awk '/^Mem:/ {print $7}')
    
        if [ $mem_available -lt 200 ]; then
            echo "CRITICAL: Weniger als 200 MB Speicher verfügbar"
            export MODE="minimal"
        elif [ $mem_available -lt 500 ]; then
            echo "WARNING: Weniger als 500 MB Speicher verfügbar"
            export MODE="reduced"
        else
            echo "INFO: Ausreichend Speicher verfügbar"
            export MODE="full"
        fi
    }
    
    # Verarbeitungsfunktion mit verschiedenen Modi
    process_data() {
        case $MODE in
            minimal)
                echo "Ausführung im minimalen Modus"
                ./process_minimal.sh
                ;;
            reduced)
                echo "Ausführung im reduzierten Modus"
                ./process_reduced.sh
                ;;
            full|*)
                echo "Ausführung im vollen Modus"
                ./process_full.sh
                ;;
        esac
    }
    
    # Hauptschleife
    while true; do
        check_resources
        process_data
        sleep 10
    done