2016-04-06 7 views
1

Ich mache ein einfaches Server/Client-Programm in C, die auf einer Netzwerkschnittstelle hört und akzeptiert Clients. Jeder Client wird in einem gegabelten Prozess behandelt.C: fork() informieren Eltern, wenn Kind Prozess trennt

Das Ziel, das ich habe, ist, den Elternprozess wissen zu lassen, sobald ein Client die Verbindung zum Kindprozess getrennt hat.

Derzeit meine Hauptschleife sieht wie folgt aus:

for (;;) { 

    /* 1. [network] Wait for new connection... (BLOCKING CALL) */ 
    fd_listen[client] = accept(fd_listen[server], (struct sockaddr *)&cli_addr, &clilen); 

    if (fd_listen[client] < 0) { 
     perror("ERROR on accept"); 
     exit(1); 
    } 


    /* 2. [process] Call socketpair */ 
    if (socketpair(AF_LOCAL, SOCK_STREAM, 0, fd_comm) != 0) { 
     perror("ERROR on socketpair"); 
     exit(1); 
    } 


    /* 3. [process] Call fork */ 
    pid = fork(); 

    if (pid < 0) { 
     perror("ERROR on fork"); 
     exit(1); 
    } 

    /* 3.1 [process] Inside the Child */ 
    if (pid == 0) { 

     printf("[child] num of clients: %d\n", num_client+1); 
     printf("[child] pid: %ld\n", (long) getpid()); 

     close(fd_comm[parent]);  // Close the parent socket file descriptor 
     close(fd_listen[server]); // Close the server socket file descriptor 

     // Tasks that the child process should be doing for the connected client 
     child_processing(fd_listen[client]); 

     exit(0); 
    } 
    /* 3.2 [process] Inside the Parent */ 
    else { 

     num_client++; 
     close(fd_comm[child]);  // Close the child socket file descriptor 
     close(fd_listen[client]); // Close the client socket file descriptor 

     printf("[parent] num of clients: %d\n", num_client); 

     while ((w = waitpid(-1, &status, WNOHANG)) > 0) { 
      printf("[EXIT] child %d terminated\n", w); 
      num_client--; 
     } 
    } 

}/* end of while */ 

Das alles funktioniert gut, das einzige Problem, das ich habe, ist (wahrscheinlich) aufgrund der Sperrung accept Anruf.

Wenn ich mich mit dem obigen Server verbinde, wird ein neuer untergeordneter Prozess erstellt und child_processing wird aufgerufen.

Jedoch, wenn ich mit diesem Client trennen, der wichtigste übergeordnete Prozess nicht weiß über sie und nicht ausgeben printf("[EXIT] child %d terminated\n", w);

Aber, wenn ich mit einem zweiten Client nach der ersten Client einer Verbindung getrennt hat, die Haupt Schleife ist in der Lage, den while ((w = waitpid(-1, &status, WNOHANG)) > 0) Teil schließlich zu verarbeiten und mir zu sagen, dass der erste Klient getrennt hat.

Wenn es nur einen Client gibt, der sich anschließend verbindet und trennt, kann mein Haupt-Elternprozess nie feststellen, ob die Verbindung getrennt wurde oder nicht.

Gibt es eine Möglichkeit, dem übergeordneten Prozess mitzuteilen, dass mein Client bereits verlassen hat?

UPDATE

Als ich einen echten Anfänger mit c bin, wäre es schön, wenn Sie ein paar kurze Ausschnitte auf Ihre Antwort geben, damit ich wirklich versteht, kann es :-)

+0

Verwenden Threads. Das 'fork()' Modell ist im Grunde veraltet. – EJP

+0

@EJP, auf keinen Fall ist 'fork()' veraltet. Siehe Antworten unten - sie bieten tatsächlich eine Lösung ** nicht verfügbar ** nativ mit Threads. – SergeyA

Antwort

0

Ihre waitpid Nutzung ist nicht richtig. Sie haben einen Anruf nicht blockierend so, wenn das Kind dann nicht beendet ist dann der Anruf bekommt 0:

waitpid(): auf Erfolg, gibt die Prozess-ID des Kindes, dessen Zustand hat dich geändert; Wenn WNOHANG angegeben wurde und ein oder mehrere Kind (ren) durch PID angegeben sind, aber noch nicht den Status geändert haben, wird 0 zurückgegeben . Bei einem Fehler wird -1 zurückgegeben.

So gehen Sie sofort aus der While-Schleife. Natürlich kann dies später abgefangen werden, wenn die ersten Kinder enden, und ein zweites lässt Sie die waitpid erneut verarbeiten.

Wie Sie eine nicht-blockierenden Aufruf haben, muss ich warten kann vorschlagen, dass Sie nicht Terminierung direkt zu verwalten, sondern durch SIGCHLD Signal, dass Sie fangen Beendigung von Kindern lassen und rufen Sie dann in geeigneter Weise waitpid im Handler:

void handler(int signal) { 
    while (waitpid(...)) { // find an adequate condition and paramters for your needs 
} 

... 
struct sigaction act; 
act.sa_flag = 0; 
sigemptyset(&(act.sa_mask)); 
act.sa_handler = handler; 
sigaction(SIGCHLD,&act,NULL); 
... // now ready to receive SIGCHLD when at least a children changes its state 
+0

Sie können Signale verwenden, um dem Elternteil mitzuteilen, dass ein Client vom Kind getrennt wurde. – Aeldred

+0

@ Jean-Baptiste Yunès könnten Sie ein Beispiel für die Verwendung SIGCHLD geben. – lockdoc

0

Wenn ich richtig verstehe, möchten Sie in der Lage sein, mehrere Clients gleichzeitig zu bedienen, und daher ist Ihr waitpid Aufruf korrekt, da es nicht blockiert, wenn kein Kind beendet wurde.

Das Problem, das Sie dann haben, ist jedoch, dass Sie in der Lage sein müssen, asynchrone untergeordnete Beendigung zu verarbeiten, während Sie auf neue Clients über accept warten. Angenommen, Sie haben mit einem POSIXy-System, nur mit einem SIGCHLD Handler eingerichtet und das Signal unmasked (über sigprocmask, obwohl IIRC ist standardmäßig unmasked), sollte genug sein, accept mit EINTR zu scheitern, wenn ein Kind beendet während Sie darauf warten, dass ein neuer Client eine Verbindung herstellt - und Sie können dann entsprechend mit EINTR umgehen.

Der Grund dafür ist, dass ein SIGCHLD-Signal automatisch an den übergeordneten Prozess gesendet wird, wenn ein untergeordneter Prozess beendet wird. Im Allgemeinen geben Systemaufrufe wie accept einen Fehler von EINTR ("unterbrochen") zurück, wenn ein Signal empfangen wird, während sie warten.

Allerdings wäre es noch eine Race-Bedingung, wo ein Kind endet gerade vor Sie accept nennen (das heißt zwischen dem bereits waitpid und accept). Es gibt im Wesentlichen zwei Möglichkeiten, dies zu überwinden:

  1. Haben alle die Kind Beendigungsverarbeitung in Ihrem SIGCHLD Handler anstelle der Hauptschleife. Dies ist jedoch möglicherweise nicht durchführbar, da dem, was Sie innerhalb eines Signal-Handlers tun dürfen, erhebliche Grenzen gesetzt sind. Sie dürfen zum Beispiel nicht printf anrufen (obwohl Sie write verwenden können).

    Ich schlage nicht vor, dass Sie diesen Weg gehen, obwohl es zunächst einfacher scheint, ist es die am wenigsten flexible Option und kann sich später als nicht praktikabel erweisen.

  2. Schreiben Sie in Ihrem SIGCHLD Signalhandler an ein Ende einer nicht blockierenden Leitung. Anstatt innerhalb der Hauptschleife accept direkt aufzurufen, verwenden Sie poll (oder select), um sowohl am Socket als auch am Leseende der Pipe auf Bereitschaft zu prüfen und sie entsprechend zu behandeln.

    Unter Linux (und OpenBSD, bin ich mir nicht sicher über andere) können Sie ppoll (man page) verwenden, um die Notwendigkeit zu vermeiden, eine Pipe zu erstellen (und in diesem Fall sollten Sie das Signal maskiert lassen und es währenddessen maskieren die Abfrage Operation, wenn ppoll schlägt mit EINTR, wissen Sie, dass ein Signal empfangen wurde, und Sie sollten waitpid aufrufen). Sie müssen noch einen Signalhandler für SIGCHLD setzen, müssen aber nichts tun.

    Eine andere Option unter Linux ist signalfd (man page) zu verwenden, um sowohl die Notwendigkeit zu vermeiden, eine Pipe zu erstellen und einen Signalhandler einzurichten (glaube ich). Sie sollten das SIGCHLD-Signal (mit sigprocmask) maskieren, wenn Sie dies verwenden. Wenn poll (oder gleichwertig) anzeigt, dass das Signalfd aktiv ist, lesen Sie die Signaldaten von ihm (wodurch das Signal gelöscht wird) und rufen Sie dann waitpid an, um das Kind zu ernten.

    Auf verschiedenen BSD-Systemen können Sie kqueue (OpenBSD man page) statt poll verwenden und nach Signalen suchen, ohne einen Signal-Handler einrichten zu müssen.

    Auf anderen POSIX-Systemen können Sie pselect (documentation) ähnlich wie ppoll wie oben beschrieben verwenden.

    Es gibt auch die Möglichkeit, eine Bibliothek wie libevent zu verwenden, um die Betriebssystemspezifikationen zu abstrahieren.

Die Glibc Handbuch hat an exampleselect zu verwenden. Weitere Informationen zu diesen Funktionen finden Sie in den Handbüchern für poll, ppoll, . Es gibt eine online book mit Libevent.

Raue Beispiel für die Verwendung von select, entlehnt von Glibc Dokumentation (und modifiziert):

/* Set up a pipe and set signal handler for SIGCHLD */ 

int pipefd[2]; /* must be a global variable */ 
pipe(pipefd); /* TODO check for error return */ 
fcntl(pipefd[1], F_SETFL, O_NONBLOCK); /* set write end non-blocking */ 

/* signal handler */ 
void sigchld_handler(int signum) 
{ 
    char a = 0; /* write anything, doesn't matter what */ 
    write(pipefd[1], &a, 1); 
} 

/* set up signal handler */ 
signal(SIGCHLD, sigchld_handler); 

Wo Sie gerade accept haben, müssen Sie den Status der Server-Socket und das Leseende des Rohres überprüfen:

fd_set set, outset; 
struct timeval timeout; 

/* Initialize the file descriptor set. */ 
FD_ZERO (&set); 
FD_SET (fdlisten[server], &set); 
FD_SET (pipefds[0], &set); 
FD_ZERO(&outset); 

for (;;) { 
    select (FD_SETSIZE, &set, NULL, &outset, NULL /* no timeout */)); 
    /* TODO check for error return. 
     EINTR should just continue the loop. */ 

    if (FD_ISSET(fdlisten[server], &outset)) { 
     /* now do accept() etc */ 
    } 

    if (FD_ISSET(pipefds[0], &outset)) { 
     /* now do waitpid(), and read a byte from the pipe */ 
    } 
} 

andere Mechanismen ist in der Regel einfacher, also lasse ich jene als eine Übung :)