Parallele Vorgänge

Ich habe schon in den letzten Socket-Tipps versprochen irgendwann auf IPC und Prozesse einzugehen, aber werde es auch diesmal nicht bringen. Ich möchte hier nur kurz soweit an der Oberfläche kratzen, daß jeder in der Lage sein sollte die nötigen Informationen zu finden.

Unter parallelen Vorgängen verstehe ich z.B. einen Server, der gleichzeitig mehrere Clients verwaltet. Das kann einmal mit select() passieren, indem er sie sequentiell abarbeitet, oder aber quasi-parallel, indem der Server sich in mehrere Prozesse aufspaltet, und jeder Client einen "eigenen" Server hat.

Dieses Konzept ist nur unter UNIX wirklich sinnvoll, in der Windows-Welt wird man hier viel lieber zu Threads greifen, da diese "leichter" aus der Sicht des Systems sind, sie erzeugen weniger Verwaltungsaufwand. Das liegt auch daran, daß sie in der Windows-Welt schon immer eingeplant waren, während die POSIX-Threads (pthreads) unter UNIX ein wenig drangefummelt erscheinen, und vor meinen Augen bisher noch keine Gnade gefunden haben.

Unter UNIX verwenet man den Systemcall fork() um einen neuen Prozeß zu erzeugen. Dieser Call wird einmal gerufen, aber kehrt zweimal zurück, nämlich im Elternprozess und im Kindprozess. Der Rückgabewert teilt das mit: der Elternprozess erhält die PID des Kindes (weil er anders nicht drankäme), der Kindprozess erhält 0 (denn er kann mit getpid() und getppid() jederzeit die fehlenden Informationen erhalten).

Nach einem fork setzt das Programm genau dort fort, wo fork aufgerufen wurde. Also ist ein if oder switch nötig, um dem Kind die Aufgabe zu erteilen. Das praktische Vorgehen ist so, daß der Elternprozess auf dem Lausch-Socket wartet, und immer wenn ein Client kommt ein Kind erzeugt wird, das diesen dann exklusiv erhält.

Achtung: bei einem fork() wird alles kopiert, alle Variablen, alle Rechte und natürlich auch alle offenen Dateien - daher bleibt der Client "kleben" wenn der Server nicht in allen Prozessen den Socket zumacht. Der Code sieht also so in der Art aus:

for(;;)
{
    cli = accept(lausch, NULL, 0);
    pid = fork();
    if (pid == 0)
        exit(handle_client(cli));
    close(cli);
}
    

Der Elternprozess ist auch dafür verantwortlich Zombies zu verhindern. Bei Zombies handelt es sich um tote Kinder, deren Eltern sich nicht für die Umstände des Todes interessieren. :-)

Wenn ein Prozeß ablebt, dann bleiben ein paar Informationen bestehen (Exit-Status, CPU-Usage etc.), die der Kernel solange bereithält, bis der Elternprozess sie abholt. Das verschwendet Ressourcen und sieht bei einem ps-Aufruf hässlich aus.

Das richtige Vorgehen: der Elternprozess bekommt SIGCHLD gesendet (immer wenn ein Kind ablegt), und sollte dann wait() aufrufen. Signalbehandlung ist etwas trickreich und aus einem der Beispielcodes ersichtlich, die Funktion dazu heißt sigaction() (bitte nicht mehr signal() verwenden, es wäre schön wenn diese Krücke aussterben würde).

Ein Nachteil, den man bei Prozessen hat, nicht aber bei Threads, ist die Kommunikation zwischen den einzelnen Teilen. Prozesse haben komplett eigene Speicherbereiche und können mit anderen Prozessen nur über Sockets, Signale, Pipes, gemeinsame Dateien oder andere "Tricks" kommunizieren. Die sauberste Art ist das sogenannte Shared Memory (Manpages zu shmctl(), shmget() usw.). Hierbei wird ein gemeinsamer Speicherbereich ausgehandelt, auf den alle Beteiligten zugreifen können.

Ich bevorzuge bei Problemen, die keine Kommunikation der einzelnen Clients erfordern (z.B. Webserver) Prozesse, und bei solchen, bei denen die Clients miteinander zu tun haben (z.B. Chatserver) einen einzelnen Prozeß mit select() und sequentieller Abarbeitung.