69. Systemprogrammierung IV: Tochterprozesse

Häufig möchte man ein externen Programm aufrufen. einfachste Möglichkeit: system(char *cmd) aus stdlib.h; führt cmd aus wie die Shell, gibt Rückgabewert des externen Programms zurück.

Zusätzlich stdin oder stdout des erzeugten Prozesses bedienen: FILE *popen(char *command, char *type), type="r" oder "w". Schließen des erhaltenen FILEs mit mit pclose(FILE *stream) schließen, Ergebnis ist der Rückgabewert des aufgerufenen Programms.

Ein Beispiel dafür könnte sein, dass wir ein Jpeg-Bild schreiben wollen, aber zu faul sind, den Jpeg-Encoder per Bibliothek zu betreiben. Bei der offenen Jpeg-Bibliothek ist hingegen ein Programm namens cjpeg dabei, das die Quelldatei im pbm-Format liest (bei debian ist das im Paket libjpeg-progs, andere Distributionen werden ähnliche Namen haben; auch der Quellcode der Bibliothek ist am Netz verfügbar). Ein pbm-Bild zu schreiben, ist trivial: Man gibt eine Signatur (“P6” steht für Farbdaten), dann Höhe und Breite des Bildes und den maximalen Wert, den jeder Farbkanal annehmen kann, jeweils in ASCII. Nach einem Zeilenvorschub kommen dann ganz einfach die Daten, hier je ein Byte für Rot, Blau und Grün.

Man könnte das über eine Zwischendatei und system verhandeln, aber Zwischendateien sind immer schwierig. Mit popen geht das relativ einfach:

#include <stdio.h>

int main(int argc, char **argv)
{
  FILE *output;
  int i, j, fail=0;

  if ((output=popen("cjpeg > colours.jpg", "w"))) {
    fprintf(output, "P6\n255 255 255\n");
    for (i=0; i<255; i++) {
      for (j=0; j<255; j++) {
        fprintf(output, "%c%c%c", i, j, 255);
    } }
    fail = pclose(output);
  } else { fail = 1;}
  if (fail) {
    perror(argv[0]);
  }
  return 0;
}

fork und exec

Sowohl system als auch popen sind relativ bequem, haben aber ein paar Probleme. Was ist z.B. wenn wir einen Koprozess wollen, bei dem sowohl stdin als auch stdout auf irgendwelche Deskriptoren von unserem Programm zeigen? Was ist, wenn wir den Tochterprozessen Signale schicken wollen? Erschwerend kommt hinzu, dass wir immer Sicherheitsprobleme haben, wenn eine Shell im Spiel ist – wenn unser Programm mehr Rechte hat als der Mensch, der sie aufruft (und das ist z.B. immer der Fall, wenn wir mit dem Netz reden), könnte er/sie durch gemeine Trickserei mit Umgebungsvariablen oder Parametern, die wir so übergeben, plötzlich Dinge mit den Rechten unseres Programms ausführen.

Kurz: Wer mehr will, muss auf die popen und system zugrunde liegenden Funktionen zurückgreifen: fork und exec.

Die Funktion pid_t fork(void) “verzweigt” die Ausführung eines Programms – es entstehen zwei identische Prozesse. Das Kind sieht als Ergebnis von fork 0, das Elter die PID des Kindes.

PID steht dabei für Program Identification; dies ist eine Zahl (sichtbar z.B. in der Ausgabe von ps), die für jeden Prozess, der zu einer gegebenen Zeit auf einer Maschine läuft, eindeutig ist.

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main(void)
{
  pid_t childPid;

  childPid = fork();
  if (childPid==0) { /* child */
    printf("Hier spricht das Kind.\n");
  } else {
    printf("Mein Kind heißt %d.\n", childPid);
  }
  return 0;
}

Um damit Dinge wie system machen zu können, braucht man noch Funktionen aus der exec-Familie und ggf. wait.

fork führt einfach das gleiche Programm zwei Mal aus. Das kann sinnvoll sein (z.B. ein Steuerprogramm, das immer weiter läuft und Arbeiterprogramme abspaltet, um ihre Ergebnisse in irgendeiner Weise einzusammeln oder auch nicht – apache z.B. arbeitet so). In der Regel möchte man aber andere Programme ausführen. Dazu kann man ein Prozessimage komplett durch ein anderes ersetzen, und zwar mit der Funktionen aus der exec-Familie – gelingt eine dieser Funktionen, so ist das “alte” Programm komplett vergessen, d.h. exec kehrt wenn überhaupt nur bei einem Fehler zurück.

Der aufrufende Prozess kann dass mit wait – ähnlich wie system das tut – auf das Ende des Tochterprozesses warten – oder auch weiterlaufen. Wenn ein Tochterprozess terminiert, bekommt der Mutterprozess ein SIGCHLD geschickt, das er tunlichst annehmen sollte. Wird das Signal nicht verarbeitet (was bei Verwendung von wait nicht passieren kann), entstehen Zombies, Prozesse, die eigentlich nicht arbeiten, aber auch nicht sterben können, weil Unix noch wartet, bis der Mutterprozess anerkennt, dass sie gestorben sind. Sinn dieser Regelung ist, dass der Mutterprozess noch an Daten des Tochterprozesses interessiert sein könnte und diese nicht mehr zur Verfügung stehen, wenn der Prozess aufgeräumt wurde, wohl aber, solange er als Zombie eine unheilige Existenz fristet.

Auch dabei können, wenn die beiden Prozesse miteinander reden, viele Concurrency-Probleme auftreten, etwa deadlocks bei Koprozessen. Wenn beispielsweise der Tochterprozess darauf wartet, von der Mutter etwas zu lesen, die Mutter aber selbst etwas von der Tochter lesen möchte, bevor sie selbst schreibt, geht natürlich nichts mehr weiter. Es ist einfacher als man glaubt, solche Zustände zu erzeugen.


Markus Demleitner

Copyright Notice