上节发现了新的fork程序还有问题, 本节就来解决这个问题.
问题回顾
将两个客户端关闭, 结果呢, 服务端的两个子进程的资源没有被回收从而成为了僵尸进程了. 父进程没有正确的回收子进程的资源, 所以人家就成了僵尸.
其实在apue书中就讲到过, 子进程退出后父进程会收到子进程的SIGCHLD
信号, 但是该信号父进程默认是忽略的. 而现在想要解决这个问题, 就必须让父进程捕捉该信号, 并作出处理.
捕捉信号
既然知道问题所在, 现在就来想办法捕捉信号. 信号捕捉可以使用signal
函数也可以使用sigaction
函数. 推荐使用后者比较好, 这里我就是用后者.
父进程等待子进程退出有wait
和waitpid
两个函数, 前者调用后只有被阻塞到有子进程退出, 后者有多个选择还可以可以设置进程不阻塞. 而服务端最好不能被阻塞, 所以我们只有选择用waitpid
函数.
实验代码
现在来看我们修改的代码, 完整代码service_fork_SIGCHLD.c .
首先捕捉和处理SIGCHLD信号
// SIGCHLD的信号处理
void sighandler(int signo)
{
if(signo == SIGCHLD)
{
pid_t pid;
// 一定要死循环
while(1)
{
// waitpid设置为WNOGANG, 非阻塞
if((pid = waitpid(-1, NULL, WNOHANG)) <= 0)
break;
printf("child %d terminated\n", pid);
}
}
int main(int argc, char *argv[])
{
struct sigaction action;
action.sa_handler = sighandler;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
if(sigaction(SIGCHLD, &action, NULL) != 0)
EXIT("sigaction");
}
信号处理中要注意两点 :
- 一定要死循环. UNIX信号默认不排队, 多个相同信号到来最后只会处理一次信号
- waitpid要设置为
WNOHANG
. 让waitpid不阻塞, 因为随时都可能有未终止的子进程在运行.
最后看服务端的修改 :
// 服务端
int service(int port, const char *ser_addr)
{
while(1)
{
clientfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_len);
// accept被信号打断后重新运行
if(clientfd < 0)
{
if(errno == EINTR)
continue;
else
EXIT("accept");
}
...
}
close(sockfd);
return 0;
}
因为accept在阻塞等待连接信号到来的时候可能被中断打断(比如信号), 导致errno
设置为了EINTR, 但是这种错误又不是致命的, 所以判断是中断打断就重新阻塞就行了.
服务端 :
./a.out 1 8080 127.0.0.1
客服端 :
./a.out 2 8080 127.0.0.1
现在重新运行就发现服务端的问题解决了.
总结
解决了fork带来的问题后, 接下来我们继续来解决客服端的问题. 解决了fork带来的问题后, 接下来我们继续来解决客服端的问题. 当然也可能并没有注意到客服端的问题所在, 不急, 下节就能知道.
- accept可能会被中断打断, 但不是致命错误, 需要重启
- 捕捉SIGCHLD信号注意点.