Christian: Multithreaded Server - $SIG{CHLD} = sub { wait } beendet Server

Hallo zusammen,

ich versuche gerade die Prinzipien eines multithreaded Servers zu verstehen und habe dafür natürlich ein kleines Beispiel:

  
#!/usr/bin/perl -w  
  
use strict;  
use IO::Socket;  
  
# Port waehlen  
use constant MYPORT => 1234;  
  
my $client = '';  
  
my $sock = new IO::Socket::INET(LocalPort => MYPORT,  
                       Reuse     => 1,  
                       Listen    => 5)  
      or die "can't create local socket: $@\n";  
  
$SIG{'CHLD'} = 'IGNORE';  
#$SIG{'CHLD'} = sub { my $c = wait(); print "Child $c terminated.\n" };    # Zombies verhindern  
  
print "Accepting connections on Port ", MYPORT, "...\n";  
while ($client = $sock->accept()) {  
  # Verbindung ist aufgebaut  
  print "Accepted connection from ",  
          $client->peerhost(), ":", $client->peerport(), "\n";  
  
  # Erzeugen eines Kindprozesses und Uebergabe an $client.  
  if (fork() == 0) { # Kindprozess  
  
    $sock->close; # brauch ich hier nicht mehr  
  
    # Eingabe wird untersucht und in Großbuchstaben ausgegeben  
    do {  
      my $input = <$client>;  
      chomp($input);  
      print uc($input)."\n";  
    } while(length($input) > 1);  
  
    exit(0);  
  } # End fork()  
  
  $client ->close; # nicht benötigt    <---------- (**)  
}  

Es wird ein Prozess gestartet, der auf Port 1234 lauscht. Die while-Schleife wartet auf eingehende Verbindungen. Falls eine kommt, wird ge-forkt und gleich weiter gelauscht. Der Kindprozess übernimmt die Kommunikation und gibt einfach jede Zeile, die er bekommt in Großbuchstaben zurück.

Funktioniert soweit wunderbar, aber nur mit

  
$SIG{CHLD} = 'IGNORE';  

Lasse ich den SIGCHLD-Handler ganz weg, bekomme ich natürlich Zombies. Was aber, wenn ich die PID des Kind verwenden möchte? Mit

  
$SIG{'CHLD'} = sub { my $c = wait(); print "Child $c terminated.\n" };  

wird der Serverprozess beendet, sobald das erste Kind stirbt, was natürlich sinnlos ist. Die Meldung "Child ... terminated" erscheint noch, danach wird die while-Schleife des Servers verlassen und dieser terminiert.

Außerdem verstehe ich nicht, wieso ich z.B. im Serverprozess die Client-Verbindung beenden kann (s. (**)), diese aber im Kindprozess dann noch zur Verfügung steht. Die Variablen werden ja kopiert, aber der Socket ist doch trotzdem nur einmal da. Das sieht irgendwie aus, als würden beide Prozesse am selben Objekt operieren, ohne zu wissen, was der andere tut. Das kann doch nicht gut gehen.

Würde mich freuen, wenn ihr mich ein bisschen aufklären könntet! ;-) Insbesondere wie man an die Child-PID kommt, ohne dass der Server dabei draufgeht.

Viele Grüße
Christian

  1. Kleiner Nachtrag:
    Das ganze habe ich unter Perl v5.8.8 beobachtet.

    Grüße Christian

  2. Hallo Christian,

    Lasse ich den SIGCHLD-Handler ganz weg, bekomme ich natürlich Zombies. Was aber, wenn ich die PID des Kind verwenden möchte? Mit

    $SIG{'CHLD'} = sub { my $c = wait(); print "Child $c terminated.\n" };

    
    > wird der Serverprozess beendet, sobald das erste Kind stirbt, was natürlich sinnlos ist. Die Meldung "Child ... terminated" erscheint noch, danach wird die while-Schleife des Servers verlassen und dieser terminiert.  
      
    Erstens, aus der Perl-Doku (perldoc perlipc):  
      
    |       On most Unix platforms, the "CHLD" (sometimes also known as "CLD") signal has special behavior with respect to a value of ’IGNORE’.  
    |       Setting $SIG{CHLD} to ’IGNORE’ on such a platform has the effect of not creating zombie processes when the parent process fails to  
    |       "wait()" on its child processes (i.e. child processes are automatically reaped).  Calling "wait()" with $SIG{CHLD} set to ’IGNORE’ usu‐  
    |       ally returns "-1" on such platforms.  
      
    Also nix Zombies bei 'IGNORE'. (Wobei Du dann aber auch keine Meldungen ausgeben lassen kannst, wenn der Prozess weg ist).  
      
    Woran Dein Problem jedoch scheitert, ist Deine accept()-Schleife. Durch ein Signal (das nicht ignoriert wird) werden nämlich System Calls unterbrochen. Dein accept() wird also unterbrochen, kehrt zurück, liefert aber keinen Socket, weswegen Du die Schleife fälschlicherweise beendest.  
      
    perldoc perlipc macht das ganze über eine globale Variable, ich passe das mal kurz an:  
      
    ~~~perl
    my $waitedpid = 0;  
      
    sub childhandler {  
      my $child;  
      # Auf ALLE kinder warten  
      while (($waitedpid = waitpid(-1,WNOHANG)) > 0);  
      # Portabilität mit Systemen, auf denen der Signalhandler neu gesetzt  
      # werden muss, nachdem er ausgeführt wurde  
      $SIG{'CHILD'} = \&childhandler;  
    }  
      
    $SIG{'CHILD'} = \&childhandler;
    

    und dann in der Schleife:

    for (  
      $waitedpid = 0;  
      ($client = $sock->accetp ()) || $waitedpid;  
      $waitedpid = 0, $client->close) {  
        next if $waitedpid and not $sock;  
        # Hier jetzt der normale Code vom inneren der Schleife ohne das  
        # $client->close weil das schon in der for()-Bedingung gemacht wird  
    }
    

    Außerdem verstehe ich nicht, wieso ich z.B. im Serverprozess die Client-Verbindung beenden kann (s. (**)), diese aber im Kindprozess dann noch zur Verfügung steht. Die Variablen werden ja kopiert, aber der Socket ist doch trotzdem nur einmal da. Das sieht irgendwie aus, als würden beide Prozesse am selben Objekt operieren, ohne zu wissen, was der andere tut. Das kann doch nicht gut gehen.

    Du missverstehst etwas: $socket->close schließt die Verbindung nicht. $socket->close schließt nur den Socket. Wenn der letzte Socket, der mit der Verbindung assoziiert war, geschlossen wird, erst DANN wird die eigentliche Verbindung geschlossen. Wenn Du nun fork() aufrufst, werden alle Dateideskriptoren (inklusive Sockets) dupliziert - d.h. im neuen Prozess hast Du ein anderes Socket, aber die gleiche Verbindung "hinter" dem Socket. Wenn Du also im Elternprozess das Socket schließt, dann schließt Du nicht die Verbindung, da der Kindprozess noch ein Socket auf die Verbindung besitzt.

    Viele Grüße,
    Christian

    1. Hallo Christian!

      Super! Vielen Dank für deine ausführliche und hilfreiche Antwort!

      Woran Dein Problem jedoch scheitert, ist Deine accept()-Schleife. [...]

      So erklärt sich das Phänomen also. Schade, dass die paar Beispiele, die es im Internet gibt, dies nicht berücksichtigen.

      perldoc perlipc macht das ganze über eine globale Variable, ich passe das mal kurz an:

      [...]

      Danke für den Code, ich denk ich hab ihn verstanden und werd ihn morgen ausprobieren.

      Du missverstehst etwas: $socket->close schließt die Verbindung nicht. $socket->close schließt nur den Socket. [...]

      Das ist also ähnlich den harten Links auf Dateien. Habe ich einen harten Link auf eine Datei erzeugt, kann ich die originale (oder die andere) löschen, aber die Datei existiert trotzdem noch.

      Danke nochmals für die alleserklärende Antwort! Meine (diese) Antwort ist so kurz, weil deine Erklärungen einfach keine Fragen offen lassen ;-)

      Viele Grüße
      Christian

    2. So, ausprobiert, etwas korrigiert und es funzt endlich! ;-)
      Am Ende sieht der Code dann etwa so aus:

        
      my $waitedpid = 0;  
      my $terminated_pid = 0;  
        
      sub childhandler {  
        my $child;  
        # Auf ALLE kinder warten  
        while (($waitedpid = waitpid(-1,WNOHANG)) > 0) {  
          $terminated_pid = $waitedpid;  # <-- neu  
        }  
        # Portabilität mit Systemen, auf denen der Signalhandler neu gesetzt  
        # werden muss, nachdem er ausgeführt wurde  
        $SIG{'CHILD'} = \&childhandler;  
      }  
        
      $SIG{'CHILD'} = \&childhandler;
      

      und dann in der Schleife:

        
      for ($terminated_pid = 0;  
           ($client = $sock->accept()) || $terminated_pid;  
           $terminated_pid = 0) {  
          next if ($terminated_pid);  
          # Hier jetzt der normale Code vom inneren der Schleife _mit_  
          # $client->close, damit dies nur bei Clients ausgeführt wird  
          # und nicht, wenn die Schleifenbedingung durch ein totes  
          # Kind zustande kommt.  
        
        $client->close;  
      }
      

      Bei deinem Code wird die while-Bedigung im SIGCHLD-Handler nochmals abgefragt und dann liefert waitpid 0 (gibt noch Kinder) oder -1 (keine Kinder mehr) und nicht mehr die PID des Kindes. Mit der zusätzlichen Variable $terminated_pid ist die PID nun wirklich im Hauptprogramm nutzbar.
      Alternativ fällt mir gerade ein, dass man im Handler auch ein return; in die while-Schleife setzen könnte, um eine erneute Auswertung von waitpid zu verhindern.

      Viele Grüße
      Christian