Empfangen von Signalen

Der Empfänger hat keine Möglichkeit, den Sender ausfindig zu machen. Das Signal ist beim Empfänger ein asynchrones Ereignis; es kann zu irgendeinem beliebigen Zeitpunkt in Form eines Interrupts auftreten. Der Empfänger hat keinerlei Einfluß auf den Zeitpunkt. Das heißt, daß der gerade ablaufende Maschinencode unterbrochen wird, vom Betriebssystem der gesamte Prozeßstatus (Stack, Register, etc.) irgendwohin gerettet wird, und dann das Signal vom Empfängerprozess verarbeitet werden kann. Danach wird der Prozeß an der unterbrochenen Stelle wieder aufgesetzt, und läuft weiter. Es ist sogar möglich, daß ein Interrupt auftritt, während der Empfänger auf die Rückkehr einer aufgerufenen Betriebssystemfunktion wartet (z.B. read() aus einer Datei oder von einem Socket). Gerade an diesem Punkt unterscheidet sich das Verhalten verschiedener Unixversionen.

Signale werden nicht gepuffert, und es können daher auch Signale verlorengehen. Wenn ein bestimmtes Signal zum zweitenmal gesendet wird, bevor die Reaktion auf das erste beendet ist, dann geht das zweite verloren. Denn der Kernel hat je Prozeß und Signaltyp nur jeweils ein Bit, in dem vermerkt wird, ob für diesen Prozeß ein bestimmtes Signal anliegt, oder nicht. Man kann Signale also nicht zum Zählen von Ereignissen nehmen (zumindest dann nicht, wenn es genau gehen soll)!

Ein Prozeß kann für jedes der möglichen Signale (außer SIGKILL und SIGSTOP) steuern, wie er darauf reagieren möchte: entweder ignorieren, oder gezielt darauf reagieren. Für jedes Signal gibt es ein Standardverhalten (default signal handler). Das ist systemweit für jedes Signal entweder auf ignorieren oder Beenden des Programms gesetzt. Für jeden Prozeß existiert eine individuelle Tabelle, wie auf jedes Signal reagiert werden soll. Diese Tabelle ist anfangs mit dem Standardverhalten für jedes Signal vorbelegt, und kann zur Laufzeit modifziert werden.

Um auf ein Signal reagieren zu können, muß man vorher (!) in C einen signal handler installieren. Das ist keine große Sache; man ruft nur die Funktion signal() auf, und übergibt ihr die betreffende Signalnummer (beispielsweise SIGUSR1) und die Adresse einer Funktion, die beim Eintreffen eines Signals aufgerufen werden soll. Diese Funktion ist der signal handler. Sie erhält einen Parameter, nämlich den Typ des aufgetretenen Signals. So kann man eine Funktion für verschiedene Signale verwenden wenn man will. Siehe man 2 signal.

Ein signal handler sollte nicht lange laufen, weil er weitere Signale blockieren kann. Wenn auf ein Signal hin etwas aufwendiges passieren soll (z.B. Öffnen und Lesen von Dateien), dann sollte man im signal handler nur vermerken, daß das Signal empfangen wurde (beispielsweise in einer globalen Variablen), und zu einem späteren Zeitpunkt anhand dieser Variablen entsprechend reagieren (beispielsweise in einer Hauptschleife des Programms, oder bei ohnehin regelmäßig auftretenden Timerereignissen).

Beispiel 30   Installation der Funktion signal_f() als signal handler:
    static int KonfigurationMussNeuGelesenWerden = 0;
    ...
    /* Bei einem SIGIO soll das Programm beendet werden,
     * bei SIGPIPE soll der socket geschlossen werden,
     * bei SIGHUP soll in (KonfigurationMussNeuGelesenWerden)
     * vermerkt werden, daß die Konfigurationsdatei neu gelesen
     * werden soll.
     */
    void signal_f( int s )
    {
      switch( s )
        {
        case SIGPIPE:
          {
            schliesseosocket();
          }
          break;
    
        case SIGIO:
          {
            schliesseosocket();
            exit( 2 );
          }
          break;
    
        case SIGHUP:
          KonfigurationMussNeuGelesenWerden++;
          break;
    
        default :
    
          break;
        } /* Ende switch */
    } /* Ende signal_f() */
    ...
    /* Signalhandler installieren (damit bei einem
     * SIGIO nicht das Programm beendet wird, sondern
     * nur in signal_f() der socket geschlossen wird, etc.):
     */
    signal( SIGPIPE, signal_f );
    signal( SIGIO, signal_f );
    signal( SIGHUP, signal_f );
    ...
    /* Hauptschleife des Programms (je nach Systemumgebung, hier nur
     * fiktiv):
     */
    while( ( E=HoleEreignis() )!=EreignisAbbruch )
    {
      switch( E )
        {
        case EreignisTastegedrueckt:
          VerarbeiteTastenDruck();
          break;

        case EreignisMausgedrueckt:
          MausQuietscht();
          break;

        case EreignisTimer:
          if( KonfigurationMussNeuGelesenWerden!=0 )
            {
              /* es ist ein Signal SIGHUP aufgetreten:
               */
              KonfigurationMussNeuGelesenWerden = 0;
              LeseKonfiguration();
            }
          break;
          
        default :
          break;
        }
    }

Wenn dann später dieses Signal auftritt, wird der installierte signal handler, also die eigene Funktion aufgerufen (hier signal_f()).

Um einen solchen signal handler in der Shell zu definieren, gibt es das (zwangsläufig interne) Kommando (nein, nicht signal, sondern) trap (siehe dazu info bash, darin Bourne Shell Builtins).

trap erhält ein Kommando mit den zugehörigen Argumenten (als String), das beim späteren Eintreffen eines Signals ausgeführt werden soll, und die betreffende Signalnummer.

Mit der Option -p listet trap die gesetzten signal handler.

Beispiel 31   Das folgende Beispiel setzt eine eigene Behandlung für die Signale SIGINT, SIGKILL, SIGUSR1, SIGUSR2. Das Signal SIGKILL kann man nicht mit einer eigenen Behandlung übersteuern; deshalb bewirkt trap ... SIGKILL nichts.

Dann gibt das Skript mit trap -p aus, welche Signalbehandlungen gesetzt sind, und geht in eine Endlosschleife. Damit nicht die ganze CPU-Zeit verballert wird, habe ich noch ein sleep eingebaut. Normalerweise könnte man die Endlosschleife durch Drücken von ^C knacken (das bewirkt das Senden von SIGINT), aber dieses Skript fängt SIGINT ab, und gibt dafür nur aus:
ich habe hier ein Signal SIGINT

Beispielausgabe:

klaus@aw33:~/s2 > ../t
ursprünglich abgefangene Signale:
jetzt abgefangene Signale:
trap -- 'machwaswennSignal SIGINT' SIGINT
trap -- 'machwaswennSignal SIGKILL' SIGKILL
trap -- 'machwaswennSignal SIGUSR1' SIGUSR1
trap -- 'machwaswennSignal SIGUSR2' SIGUSR2
schnarch... (Prozeß 9411)
schnarch... (Prozeß 9411)
schnarch... (Prozeß 9411)
ich habe hier ein Signal SIGINT
schnarch... (Prozeß 9411)
schnarch... (Prozeß 9411)
schnarch... (Prozeß 9411)
...

Von einem anderen Terminal aus kann man andere Signale schicken, beispielsweise kill -s SIGUSR1 9411 würde den Text ich habe hier ein Signal SIGUSR1 produzieren. Beenden kann man diesen Amokläufer nur noch durch kill -s SIGKILL 9411.

Hier das Skript des Amokläufers:

#!/bin/sh

# Diese Funktion soll aufgerufen werden, wenn ein Signal kommt:
machwaswennSignal()
{
  echo ich habe hier ein Signal $1
}

# bisher gesetzte Signale ausgeben:
echo ursprünglich abgefangene Signale:
trap -p

# eigene Signalbehandlung setzen.
# Das Setzen von SIGKILL ist überflüssig,
# weil dieses Signal nicht angefangen werden kann.
trap  'machwaswennSignal SIGINT' SIGINT
trap  'machwaswennSignal SIGKILL' SIGKILL
trap  'machwaswennSignal SIGUSR1' SIGUSR1
trap  'machwaswennSignal SIGUSR2' SIGUSR2

echo jetzt abgefangene Signale:
trap -p

# Endlosschleife: Ausgabe der eigenen Process id,
# und dann nochmal kurz schlafen legen:
while true
do
    echo "schnarch... (Prozeß $$)"
    sleep 1
done

AnyWare@Wachtler.de