Unix网络编程(三、waitpid解决多客户端连接时引发的僵尸进程问题)

引发路径

上一节的代码解决的单客户端连接时引发的僵尸进程问题。但是我们的服务器环境通常都是有大量的客户端同时连接过来,在这种情况下,我们的代码就显得不是那么健壮了,它仍然会引发僵尸进程的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(int argc, const char *argv[])
{
int sockfd[5];
for (int i = 0; i < 5; ++i) {
sockfd[i] = e_socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(45678);
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
connect(sockfd[i], (sockaddr*)&servaddr, sizeof(servaddr));
}
// do something
return 0;
}

我们用上面这段代码模拟5个客户端同时连接的情况。当客户端进程终止时,所有打开的文件描述符会由内核自动关闭,在每一个文件描述符被关闭的时候,都会发送一个FIN给服务端。这5个FIN包几乎同时发到服务端fork出来的对应的5个子进程,也就会导致这5个子进程几乎同时终止,子进程终止时会将SIGCHLD信号递交到他的父进程中。也就是说父进程(也就是服务端进程)几乎同时收到这5个SIGCHLD信号,这就是引发问题所经过的路径。

验证路径


因为目前只用了5个客户端,这种状态还不太好捕获。但是在一个高并发的服务器上,出现的概率是非常大的。从图上也可以看出,已经出现了一个子进程变成了Z状态。出现这个问题的原因也很简单。5个SIGCHLD信号都是在信号处理函数之前出现的,而我们的信号处理函数只执行一次,而UNIX的信号是异步的,前一个信号到达后并不会阻塞后一个信号。如果第一个信号到达了,此时正在执行信号处理函数,此时有另外的信号到达,这些信号是不会被处理的。

解决方法

调用waitpid函数,在有子进程未结束的时候,不阻塞父进程。

1
2
3
4
5
6
void sig_child(int signo)
{
int stat, pid;
while ((pid = waitpid(-1, &stat, WNOHANG)) > 0){
}
}

这是更新后的信号处理函数。

总结

前篇文章基本讲述了一个处理多客户端连接的基本策略。包括

  1. fork子进程时,必须处理SIGCHLD信号
  2. 捕获信号时,必须处理被中断的系统调用
  3. SIGCHLD信号的处理函数必须能解决僵尸进程