我在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)
部分并告诉我第一个客户端已断开连接。
如果之后只有一个客户端连接和断开连接,我的主要父进程将永远无法判断它是否已断开连接。
有没有办法告诉父进程我的客户端已经离开?
更新
由于我是一个真正的初学者,如果你提供一些简短的片段给你答案,那么我真的可以理解它: - )
答案 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}的地方之间})。克服这一点有两种主要可能性:
在waitpid
处理程序中执行所有子终止处理,而不是主循环。但是,这可能是不可行的,因为在信号处理程序中允许执行的操作存在重大限制。例如,您可能不会调用accept
(尽管您可以使用SIGCHLD
)。
我不建议你沿着这条路走下去,虽然它起初可能看起来更简单但它是最不灵活的选择,可能在以后证明是行不通的。
写入printf
信号处理程序中非阻塞管道的一端。在主循环中,不是直接调用write
,而是使用SIGCHLD
(或accept
)在套接字和管道的读取端查找准备情况,并适当地处理每个。
在Linux上(和OpenBSD,我不确定其他人)你可以使用poll
(man page)来避免创建管道(在这种情况下你应该留下信号)屏蔽,并在轮询操作期间取消屏蔽;如果select
失败并显示ppoll
,则表示已收到信号,您应该调用ppoll
)。您仍然需要为SIGCHLD设置信号处理程序,但它不需要做任何事情。
Linux上的另一个选择是使用EINTR
(man page)来避免创建管道和设置信号处理程序(我认为)。如果使用此信号,则应屏蔽SIGCHLD信号(使用waitpid
)。当signalfd
(或等效物)指示signalfd处于活动状态时,从中读取信号数据(清除信号),然后调用sigprocmask
来收割孩子。
在各种BSD系统上,您可以使用poll
(OpenBSD man page)代替waitpid
并观察信号,而无需建立信号处理程序。
在其他POSIX系统上,您可以像kqueue
一样使用poll
(documentation),如上所述。
还可以选择使用libevent等库来抽象出特定于操作系统的内容。
Glibc手册使用pselect
an example。有关这些功能的详细信息,请参阅ppoll
,select
,poll
的手册页。使用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);
使用其他机制通常更简单,所以我把它们留作练习:)