Linux-Signale empfangen und verarbeiten

Unter Linux werden Programme mit Signalen gesteuert, wie macht man das eigentlich in Freepascal?

Ein Artikel von Michael Fuchs.
Getestet mit Lazarus 3.2 | FPC 3.2.2

Entwickler unter Linux kennen mit Sicherheit den Befehl

kill -9
Bash

Wer beim Testen seiner Software mal eine Endlosschleife produziert hat, kann sie hierüber bequem abschießen. Aber kill kann noch viel mehr. Es sendet ein Signal an ein laufendes Programm und ist damit eine einfache Form der Interprozesskommunikation (https://de.wikipedia.org/wiki/Kill_(Unix)).

Neben dem bekannten Signal 9 (SIGKILL) — mit dem das Beenden des Programms erzwungen wird — gibt es noch eine Reihe weiterer Signale. Ein Beispiel wäre die 15 (SIGTERM) — eine höflichere Variante von SIGKILL — mit der das Programm gebeten wird sich zu beenden. Im Gegensatz zu SIGKILL kann das Programm diese Bitte auch ignorieren.

Alle existierenden Signale eines Systems, kann man sich mit dem Befehl

kill -l

ausgeben lassen. In dieser Liste findet man dann auch die beiden Signale SIGUSR1 (30) und SIGUSR2 (31), die zur freien Verwendung bereitstehen.
Und freie Verwendung heißt, wir können unser eigenes Programm damit steuern.

Beispiel

Nehmen wir folgendes simple Beispielprogramm als Grundlage:

program killme;
{$MODE ObjFpc}
{$H+}

uses
  SysUtils;

var
  Output: String = 'Guten Morgen!';
  DoShutdown: Boolean = False;

begin
  repeat
    WriteLn(Output);
    Sleep(1000);
  until DoShutdown;
end.
Pascal

Nach dem Senden von SIGUSR1 wollen wir nun, dass die Ausgabe von „Guten Morgen!“ auf „Guten Tag!“ wechselt. Beim Senden von SIGTERM hingegen soll das Programm noch einmal „Auf Wiedersehen!“ ausgeben und sich anschließend beenden.

Signalverarbeitung

Zunächst benötigen wir eine Prozedur, die die Signalverarbeitung durchführt.

procedure DoKill(Signal: CInt); cdecl;
begin
  case Signal of
    SIGUSR1:
      Output := 'Guten Tag!';
    SIGTERM:
      DoShutdown := True;
  end;
end;
Pascal

Wir prüfen im case-Block, welches Signal übergeben wurde, und ändern entweder den Ausgabetext über die globale Variable Output oder setzen DoShutdown auf TRUE. Damit soll später die repeat…until-Schleife beendet werden. Alle anderen Signale werden ignoriert.
Um die Konstanten SIGTERM und SIGUSR1 zu nutzen, müssen wir die Unit BaseUnix in unseren uses-Bereich aufnehmen.

Handler installieren

Damit unser Programm die Signale verarbeiten kann, müssen wir zwei Signal-Handler installieren.

procedure InstallSigHandler(Signal: CInt);
var
  Action: PSigActionRec;
begin
  New(Action);
  Action^.sa_handler := SigActionHandler(@DoKill);
  FillChar(Action^.sa_mask, SizeOf(Action^.sa_mask), #0);
  Action^.sa_flags := 0;
  Action^.sa_restorer := nil;
  if (FPSigaction(Signal, Action, nil) <> 0) then begin
    WriteLn('Error: ', fpgeterrno);
    Halt(1);
  end;
  Dispose(Action);
end;
Pascal

Die Prozedur erzeugt einen neuen PSigActionRec und befüllt ihn. Relevant für uns ist nur das Feld sa_handler. Dies wird mit einem (auf SigActionHandler gecasteten) Zeiger auf unsere DoKill-Prozedur befüllt. Die restlichen Felder werden auf 0 bzw. nil gesetzt.

Durch den Aufruf von FPSigaction wird der Signal-Handler installiert. Der erste Parameter gibt dabei die Signal-Nummer an, der zweite ist der davor erzeugte PSigActionRec. Als dritter Parameter kann optional ein weitere PSigActionRec übergeben werden, dieser enthält nach dem Aufruf die Daten des ursprünglichen Signal-Handlers. Für unser Beispiel ist das aber nicht nötig und wir übergeben daher nil. Zu guter Letzt entfernen wir den Record wieder aus dem Arbeitsspeicher.

Programm

Nachdem nun alle Vorarbeiten erledigt sind, passen wir unser Hauptprogramm an.

begin
  WriteLn('Process ' + IntToStr(FpGetpid) + ' started...');
  InstallSigHandler(SIGUSR1);
  InstallSigHandler(SIGTERM);
  repeat
    WriteLn(Output);
    Sleep(1000);
  until DoShutdown;
  WriteLn('Auf Wiedersehen!');
end.
Pascal

Die beiden Aufrufe von InstallSigHandler installieren unsere DoKill-Prozedur als Handler für die beiden Signale. Die repeat..until-Schleife bleibt bestehen und nach ihrem Ende wird die Abschlussausgabe auf den Bildschirm geschrieben und das Programm ist beendet.

Um das Testen zu vereinfachen, geben wir mit der ersten Zeile des Programms noch die aktuelle Prozess-ID aus. Das erspart uns die Suche in der Prozessliste.

Test

Für den Test unseres Programms öffnen wir nebeneinander zwei Konsolenfenster.

$ ./killme 
Process 553673 started...
Guten Morgen!
Guten Morgen!
Guten Morgen!

Nun senden wir in der zweiten Konsole SIGUSR1 an den Prozess:

$ kill -SIGUSR1 553673

Wie erwartet ändert sich die fortlaufende Ausgabe von „Guten Morgen!“ zu „Guten Tag!“.

Guten Morgen!
Guten Tag!
Guten Tag!
Guten Tag!
Guten Tag!

Nun wird es Zeit den Prozess zu beenden.

$ kill -SIGTERM 553673

Auch hier gibt es bei der Ausgabe keine Überraschung. Der Prozess schreibt „Auf Wiedersehen!“ in die Ausgabe, das Programm endet und das Eingabeprompt erscheint wieder.

Guten Tag!
Guten Tag!
Auf Wiedersehen!
$

Fazit

Mit relativ überschaubaren Code kann man sich somit eine sehr simple Interprozess-Kommunikation programmieren. Zwar kann der Benutzer damit nur sehr wenige Daten an das Programm übertragen, aber eine Aufforderung Config-Datei neu einzulesen wäre beispielsweise problemlos möglich.

Beispielquellcode