68. Systemprogrammierung III: Signale

Signale teilen einem Programm mit, dass

  • Ein Fehler aufgetreten ist (SIGSEGV, SIGFPE, )
  • Ein externes Ereignis aufgetreten ist (SIGHUP, SIGINT, )
  • Ein Prozess gerne etwas von uns hätte (SIGTERM, SIGABRT, SIGCHLD, )

Signale werden in der Regel duch ihre symbolischen Namen bezeichnet. Welche es da gibt, steht samt Erklärung bei signal(7). Die oben erwähnten bedeuten in etwa folgendes:

  • SIGSEGV: “Segmentation Violation”, das Programm hat versucht, auf Speicher zuzugreifen, der ihm nicht gehört.
  • SIGFPE: “Floating Point Exception”, bei einer mathematischen Operation (nicht notwendig Fließkomma) ist ein Fehler aufgetreten (z.B. Division durch Null)
  • SIGHUP: “Hangup”, das Terminal, von dem aus der Prozess kontrolliert wird, hat “aufgelegt”, existiert also nicht mehr (Dämonen, die ohnehin kein kontrollierendes Terminal haben, benutzen das Signal gerne als “Konfiguration neu laden”)
  • SIGINT: “Interrupt”, jemand hat Control-C (oder den Interrupt Key) gedrückt.
  • SIGTERM: “Terminate”, jemand will, dass das Programm möglichst schnell den Geist aufgibt
  • SIGABRT: “Abort”, jemand will, dass das Programm abbricht, wird meistens vom Programm selbst in Verbindung mit assert geworfen
  • SIGCHLD: Ein Kindprozess ist fertig geworden.

Die Bibliothek richtet für jedes Signal einen Handler ein, der das Signal ignoriert, den Prozess beendet, einen core dumpt usf.

Es ist aber auch möglich, eigene Handler einzurichten. Dazu dient die Funktion

void *signal(int sigNum, sighandler_t action)

Sie nimmt in sigNum eine Signalnummer (eines der SIGxxxx-Symbole, in action entweder einen Signalhandler oder eines der Symbole SIG_DFL (Default wiederherstellen) oder SIG_IGN (Signal ignorieren). Von letzterem solltet ihr vorläufig die Finger lassen.

Die Funktion gibt das, was vorher als action gesetzt war, zurück. Auf diese Weise kann man den vorherigen Handler wiederherstellen, wenn das Signal keine spezielle Behandlung mehr braucht.

Signale können grundsätzlich asynchron kommen, d.h. zu einem beliebigen Zeitpunkt im Programmablauf. Damit ist man eigentlich schon im Bereich des concurrent programming, in dem allerlei Probleme auftreten, die mit dem gleichzeitigen Zugriff verschiedener Programme auf gemeinsame Ressourcen zusammenhängen – Schlagworte sind hier Reentranz, Race Conditions oder Locking. Generell sollten Laien nur die Signale SIGHUP, SIGINT, SIGTERM und evtl. SIGQUIT behandeln und in Signalhandlern nur einen Flag setzen (aber Vorsicht, vgl. unten), der dann im Hauptprogramm ausgewertet wird.

Wer mehr machen will, muss mindestens den Abschnitt über Signale in der glibc-Dokumentation lesen, besser noch den Stevens und ein Buch über Betriebssysteme.

Bei der Anwendung von signal kommt erschwerend hinzu, dass verschiedene Unices verschiedene Dinge tun, wenn ein Signalhandler aufgerufen wurde, das Programm aber weiterläuft. Manche richten danach wieder den Default-Handler ein, andere nicht. Bei rezeptgemäßer Anwendung muss euch dieser Unterschied nicht wesentlich kümmern – der schlimmstmögliche Fall ist, dass euer Signalhandler ein möglicherweise erneut auftretendes Signal nicht mehr fängt und dann nicht aufgeräumt wird. Im Zweifelsfall besser, aber auch komplizierter, ist die Anwendung von sigaction.

Häufige Verwendung: Aufräumen nach externem Programmabbruch, etwa nach folgendem Muster.

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

volatile sig_atomic_t done=0;

void handleTerm(int sig)
{
  done = 1;
}

int main(void)
{
  signal(SIGINT, handleTerm);
  while(!done) {
    printf("Still going\n");
    sleep(1);
  }
  printf("Cleanup.\n");
  return 0;
}

Wichtig hierbei vor allem die Definition von done. Erstens ist es volatile. Das ist nötig, weil der Compiler sonst in der Main-Schleife diagnostizieren könnte, dass done im Schleifenkörper nie verändert wird und die ganze Abfrage herausoptimieren. Generell: Was asynchron (d.h. außerhalb des normalen Programmablaufs) verändert werden kann, muss auch volatile sein.

Zweitens hat es den Typ sig_atomic_t. Im Groben ist dadurch garantiert, dass Zuweisungen zu Variablen dieses Typs nicht von Signalen unterbrochen werden können, was für andere Typen nicht sicher ist. Wenn aber eine Zuweisung unterbrochen wird und der Signalhandler selbst zuweist, ist das Ergebnis meistens Mist – stellt euch folgendes Szenario vor:

strcpy(someStr, "abcdefg") wird von einem Signal unterbrochen, gerade, wenn es das c geschrieben hat. Der Signalhandler führt strcpy(someStr, "0000000") aus und kehrt zurück. Das ursprüngliche strcpy macht beim d weiter, das Ergebnis ist, dass someStr den Wert "000defg" hat – weder das, was vom Hauptprogramm aus drinstehen sollte, noch das, was der Signalhandler reingeschrieben hat. Dies ist ein Spezialfall einer so genannten race condition.

Im oben vorgestellten Muster ist das aber auch ohne atomare Zugriffe kein Problem, weil nur der Signalhandler schreibend auf done zugreift.

Eine weitere für Laien “erlaubte” Anwendung ist alarm, etwa für Timeouts.

Hierbei wird es aber schon etwas kitzlig. Relativ einfach ist es noch, wenn man das Programm einfach abbrechen will:

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

#define NAME_LEN 80

void handleAlarm(int sig)
{
  fprintf(stderr, "\nYou're sleeping.\n");
  exit(1);
}

int main(void)
{
  char name[NAME_LEN];
  void *oldAlrmHandler;

  oldAlrmHandler = signal(SIGALRM, handleAlarm);
  alarm(2);
  printf("Name: ");
  fgets(name, NAME_LEN, stdin);
  alarm(0);
  signal(SIGALRM, oldAlrmHandler);
  printf("Name: ");
  printf("\nHi, %s\n", name);
  return 0;
}

Hier wollen wir den alten Signalhandler restaurieren, merken uns also, was uns signal zurückgibt. Dann sagen wir, dass wir in zwei Sekunden geweckt werden wollen und warten auf eine Eingabe der Benutzerin. Wenn diese rechtzeitig kommt, löschen wir die Alarmanforderung (das tut das Argument Null) und restaurieren den Signalhandler, sonst kommt unser Handler ins Spiel.

Entgegen der Aussagen oben verwende ich hier durchaus Bibliotheksfunktionen. Das darf ich hier, weil ich sicher weiß, dass weder ein fprintf auf stderr noch ein exit aktiv sind, wenn der Handler aufgerufen wird – in gewissem Sinn kommt das Signal hier nicht asynchron, sondern irgendwie doch programmgesteuert.

Wenn das Programm nachher weiterlaufen soll, wirds komplizierter. Näheres dazu in der entsprechenden man- oder info-Seite.

In Python ist letzteres übrigens erheblich einfacher, weil man Exceptions hat:

import signal, sys

class WakeUp(Exception):
  pass

def raiseWakeUp(signo, stackFrame):
  raise WakeUp, "Ring Ring"

signal.signal(signal.SIGALRM, raiseWakeUp)
signal.alarm(2)
try:
  print "Name: ",
  name = sys.stdin.readline()
  print "Hi, %s"%name
except WakeUp:
  print "\nYou're sleeping!"
  sys.exit(1)

Man sieht, dass der Signalmechanismus in Python weitgehend ähnlich funktioniert wie der in C, nur der Signalhandler bekommt ein zusätzliches Argument. Durch die Exception können wir aber aus dem Signalhandler Nachrichten zurück an das Hauptprogramm schicken, ohne uns verrenken zu müssen.

Außerdem ist jede Python-Instruktion atomar, was einerseits gut ist, weil auf diese Weise viele der richtig saftigen Concurrency-Probleme gar nicht auftreten, andererseits aber auch blöd, weil Signale so lange warten, bis ein Block C-Code ausgeführt ist und der Interpreter wieder die Kontrolle hat. Es kann also manchmal sein, dass Signale in Python doch nicht ganz das tun, was wir von ihnen hätten.

SIGINT übrigens löst in Python einfach eine KeyboardInterrupt-Exception aus (wenn man keinen Signalhandler für INT installiert hat).


Markus Demleitner

Copyright Notice