【问题标题】:C: fork() inform parent when child process disconnectsC:当子进程断开连接时,fork() 通知父进程
【发布时间】:2016-04-06 09:01:47
【问题描述】:

我正在用 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, &amp;status, WNOHANG)) &gt; 0) 部分并告诉我第一个客户端 已断开连接。

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

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

更新

因为我是一个真正的 c 初学者,如果你在你的答案中提供一些简短的 sn-ps 会很好,这样我就可以真正理解它了 :-)

【问题讨论】:

  • 使用线程。 fork() 模型基本上已经过时了。
  • @EJP,fork() 绝不是过时的。请参阅下面的答案 - 他们实际上提供了一个 不可用 原生线程的解决方案。

标签: c sockets waitpid


【解决方案1】:

您的 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

【讨论】:

  • 您可以使用信号告诉父级客户端已与子级断开连接。
  • @Jean-Baptiste Yunès 你能举一个 SIGCHLD 用法的例子吗?
【解决方案2】:

如果我理解正确,您希望能够同时为多个客户端提供服务,因此您的 waitpid 调用是正确的,因为如果没有子节点终止,它不会阻塞。

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

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

但是,仍然存在竞争条件,其中一个子进程在您调用accept 之前 终止(即在已经有waitpidaccept 之间)。有两种主要的方法可以克服这个问题:

  1. 在您的 SIGCHLD 处理程序中执行所有子终止处理,而不是在主循环中。但是,这可能不可行,因为在信号处理程序中允许您执行的操作有很大的限制。例如,你不能打电话给printf(尽管你可以使用write)。

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

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

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

    Linux 上的另一个选择是使用signalfd (man page) 来避免创建管道和设置信号处理程序的需要(我认为)。如果你使用它,你应该屏蔽 SIGCHLD 信号(使用sigprocmask)。当poll(或等价物)表明signalfd处于活动状态时,从中读取信号数据(清除信号),然后调用waitpid收割子节点。

    在各种 BSD 系统上,您可以使用 kqueue (OpenBSD man page) 代替 poll 并监视信号,而无需建立信号处理程序。

    在其他 POSIX 系统上,您可能能够以与上述ppoll 类似的方式使用pselect (documentation)。

    还可以选择使用诸如libevent 之类的库来抽象出操作系统的细节。

Glibc 手册中有an example 使用select。有关这些功能的更多信息,请参阅 pollppollpselect 的手册页。使用 Libevent 有一个online book

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

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

如果你当前有accept,你需要检查服务器套接字的状态和管道的读取端:

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

使用其他机制通常更简单,因此我将其留作练习 :)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-12-06
    相关资源
    最近更新 更多