1

I am doing a simple server/client program in C which listens on a network interface and accepts clients. Each client is handled in a forked process.

The goal I have is to let the parent process know, once a client has disconnected from the child process.

Currently my main loop looks like this:

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 */ 

It all works well, the only problem I have is (probably) due to the blocking accept call.

When I connect to the above server, a new child process is created and child_processing is called.

However when I disconnect with that client, the main parent process does not know about it and does NOT output printf("[EXIT] child %d terminated\n", w);

But, when I connect with a second client after the first client has disconnected, the main loop is able to finally process the while ( (w = waitpid(-1, &status, WNOHANG)) > 0) part and tell me that the first client has disconnected.

If there will be only ever one client connecting and disconnecting afterwards, my main parent process will never be able to tell if it has disconnected or not.

Is there any way to tell the parent process that my client already left?

UPDATE

As I am a real beginner with c, it would be nice if you provide some short snippets to your answer so I can actually understand it :-)

lockdoc
  • 1,539
  • 1
  • 18
  • 31

2 Answers2

0

Your waitpid usage is not correct. You have a non-blocking call so if the child is not finished then then the call gets 0:

waitpid(): on success, returns the process ID of the child whose state has changed; if WNOHANG was specified and one or more child(ren) specified by pid exist, but have not yet changed state, then 0 is returned. On error, -1 is returned.

So your are going immediately out of the while loop. Of course this can be catched later when the first children terminates and a second one lets you process the waitpid again.

As you need to have a non-blocking call to wait I can suggest you not to manage termination directly but through SIGCHLD signal that will let you catch termination of any children and then appropriately call waitpid in the 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
Jean-Baptiste Yunès
  • 34,548
  • 4
  • 48
  • 69
0

If I understand correctly, you want to be able to servicve multiple clients at once, and therefore your waitpid call is correct in that it does not block if no child has terminated.

However, the problem you then have is that you need to be able to process asynchronous child termination while waiting for new clients via accept. Assuming that you're dealing with a POSIXy system, merely having a SIGCHLD handler established and having the signal unmasked (via sigprocmask, though IIRC it is unmasked by default), should be enough to cause accept to fail with EINTR if a child terminates while you are waiting for a new client to connect - and you can then handle EINTR appropriately.

The reason for this is that a SIGCHLD signal will be automatically sent to the parent process when a child process terminates. In general, system calls such as accept will return an error of EINTR ("interrupted") if a signal is received while they are waiting.

However, there would still be a race condition, where a child terminates just before you call accept (i.e. in between where already have waitpid and accept). There are two main possibilities to overcome this:

  1. Do all the child termination processing in your SIGCHLD handler, instead of the main loop. This may not be feasible, however, since there are significant limits to what you are allowed to do within a signal handler. You may not call printf for example (though you may use write).

    I do not suggest you go down this path, although it may seem simpler at first it is the least flexible option and may prove unworkable later.

  2. Write to one end of a non-blocking pipe in your SIGCHLD signal handler. Within the main loop, instead of calling accept directly, use poll (or select) to look for readiness on both the socket and the read end of the pipe, and handle each appropriately.

    On Linux (and OpenBSD, I'm not sure about others) you can use ppoll (man page) to avoid the need to create a pipe (and in this case you should leave the signal masked, and have it unmasked during the poll operation; if ppoll fails with EINTR, you know that a signal was received, and you should call waitpid). You still need to set a signal handler for SIGCHLD, but it doesn't need to do anything.

    Another option on Linux is to use signalfd (man page) to avoid both the need to create a pipe and set up a signal handler (I think). You should mask the SIGCHLD signal (using sigprocmask) if you use this. When poll (or equivalent) indicates that the signalfd is active, read the signal data from it (which clears the signal) and then call waitpid to reap the child.

    On various BSD systems you can use kqueue (OpenBSD man page) instead of poll and watch for signals without needing to establish a signal handler.

    On other POSIX systems you may be able to use pselect (documentation) in a similar way to ppoll as described above.

    There is also the option of using a library such as libevent to abstract away the OS-specifics.

The Glibc manual has an example of using select. Consult the manual pages for poll, ppoll, pselect for more information about those functions. There is an online book on using Libevent.

Rough example for using select, borrowed from Glibc documentation (and modified):

/* 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);

Where you currently have accept, you need to check status of the server socket and the read end of the pipe:

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 */
    }
}

Using other mechanisms is generally simpler, so I leave those as an exercise :)

davmac
  • 20,150
  • 1
  • 40
  • 68