C:fork()在子进程断开连接时通知父进程

时间:2016-04-06 09:01:47

标签: c sockets waitpid

我在C中做一个简单的服务器/客户端程序,它监听网络接口并接受客户端。每个客户端都在分叉的过程中处理。

我的目标是在客户端与子进程断开连接后让父进程知道。

目前我的主循环看起来像这样:

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

这一切都运作良好,我唯一的问题是(可能)由于阻止accept调用。

当我连接到上述服务器时,会创建一个新的子进程并调用child_processing

但是,当我与该客户端断开连接时,主要父进程不知道它并且不输出printf("[EXIT] child %d terminated\n", w);

但是,当我连接第二个客户端第一个客户端断开连接时,主循环终于可以处理while ( (w = waitpid(-1, &status, WNOHANG)) > 0)部分并告诉我第一个客户端已断开连接。

如果之后只有一个客户端连接和断开连接,我的主要父进程将永远无法判断它是否已断开连接。

有没有办法告诉父进程我的客户端已经离开?

更新

由于我是一个真正的初学者,如果你提供一些简短的片段给你答案,那么我真的可以理解它: - )

2 个答案:

答案 0 :(得分:0)

您的waitpid使用情况不正确。你有一个非阻塞的呼叫,所以如果孩子没有完成,那么呼叫将获得0:

  

waitpid():成功时,返回其状态的子进程ID   已经改变;如果指定了WNOHANG且一个或多个孩子(ren)   由pid指定存在,但尚未改变状态,则为0   回。出错时,返回-1。

所以你要立即离开while循环。当然,这可以在第一个孩子终止时被捕获,而第二个孩子可以让你再次处理waitpid

由于你需要有一个非阻塞的等待呼叫,我建议你不要直接管理终止,而是通过SIGCHLD信号让你终止任何孩子,然后适当地拨打waitpid在处理程序中:

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

答案 1 :(得分:0)

如果我理解正确,您希望能够同时为多个客户提供服务,因此您的waitpid来电是正确的,因为如果没有孩子终止,它就不会阻止。

但是,您遇到的问题是,您需要能够在通过accept等待新客户端时处理异步子终止。 假设你正在处理POSIXy系统,只是建立了SIGCHLD处理程序并且信号未被屏蔽(通过sigprocmask,虽然IIRC默认情况下是未屏蔽的),应该足以导致{{ 1}}如果孩子在等待新客户端连接时终止,则accept失败 - 然后您可以适当地处理EINTR

原因是当子进程终止时,SIGCHLD信号将自动发送到父进程。通常,如果在等待时收到信号,EINTR等系统调用将返回accept(“中断”)的错误。

然而,仍然存在竞争条件,其中一个孩子在致电EINTR之前终止(即在已经有accept和{{1}的地方之间})。克服这一点有两种主要可能性:

  1. waitpid处理程序中执行所有子终止处理,而不是主循环。但是,这可能是不可行的,因为在信号处理程序中允许执行的操作存在重大限制。例如,您可能不会调用accept(尽管您可以使用SIGCHLD)。

    我不建议你沿着这条路走下去,虽然它起初可能看起来更简单但它是最不灵活的选择,可能在以后证明是行不通的。

  2. 写入printf信号处理程序中非阻塞管道的一端。在主循环中,不是直接调用write,而是使用SIGCHLD(或accept)在套接字和管道的读取端查找准备情况,并适当地处理每个。

    在Linux上(和OpenBSD,我不确定其他人)你可以使用pollman page)来避免创建管道(在这种情况下你应该留下信号)屏蔽,并在轮询操作期间取消屏蔽;如果select失败并显示ppoll,则表示已收到信号,您应该调用ppoll)。您仍然需要为SIGCHLD设置信号处理程序,但它不需要做任何事情。

    Linux上的另一个选择是使用EINTRman page)来避免创建管道和设置信号处理程序(我认为)。如果使用此信号,则应屏蔽SIGCHLD信号(使用waitpid)。当signalfd(或等效物)指示signalfd处于活动状态时,从中读取信号数据(清除信号),然后调用sigprocmask来收割孩子。

    在各种BSD系统上,您可以使用pollOpenBSD man page)代替waitpid并观察信号,而无需建立信号处理程序。

    在其他POSIX系统上,您可以像kqueue一样使用polldocumentation),如上所述。

    还可以选择使用libevent等库来抽象出特定于操作系统的内容。

  3. Glibc手册使用pselect an example。有关这些功能的详细信息,请参阅ppollselectpoll的手册页。使用Libevent时有一个online book

    使用ppoll的粗略示例,借用Glibc文档(并修改):

    pselect

    您当前拥有select的位置,您需要检查服务器套接字的状态和管道的读取端:

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

    使用其他机制通常更简单,所以我把它们留作练习:)