并发服务器可以同时处理多个客户端的请求,服务器对客户端网络请求的处理是通过fork子服务进程来完成的。当有多个客户请求时,主服务器进程就会产生多个子服务器进程。当这些子服务器进程处理完客户请求时,就终止运行了。由于主服务器进程(也就是这些子服务器进程的父亲)仍然在运行,因此这些终止的子服务器进程在主服务器退出之前就成为了僵死进程。通常服务器进程都长时间处于运行状态,所以这些僵死进程就会一直存在于系统内。如果不及时清除这些僵死进程,他们将占用内核的空间,最终可能会耗尽系统资源。本文将描述如何清理这些僵死进程。
1. 僵死进程的产生
下面将通过一个简单的C/S模型回射程序来演示僵死进程的产生。我们首先将服务器置于后台运行,然后再查看当前终端(pts/3)下的进程状态:
edsionte@edsionte-desktop:~/echoCS$ ps -o pid,ppid,args,stat,wchan PID PPID COMMAND STAT WCHAN 5071 2305 bash Ss wait 6146 5071 ./server S inet_csk_wait_for_connect 6149 5071 ps -o pid,ppid,args,stat,wc R+ -
由上可以看出,服务器进程处于等待连接的状态,此刻它正在监听客户请求。接着运行客户端程序,连接成功后输入任意的字符串,可以看到服务器和客户端通信正常。
edsionte@edsionte-desktop:~/echoCS$ ./client 127.0.0.1 received a connection from:127.0.0.1 ps ps
我们在另一个终端下,查看位于终端pts/3下客户端和服务器进程的状态。
edsionte@edsionte-desktop:~/echoCS$ ps -t pts/3 -o pid,ppid,args,stat,wchan PID PPID COMMAND STAT WCHAN 5071 2305 bash Ss wait 6146 5071 ./server S inet_csk_wait_for_connect 6150 5071 ./client 127.0.0.1 S+ n_tty_read 6151 6146 ./server S sk_wait_data
由于客户进程发来连接请求,因此服务器进程fork了一个子服务器进程,从pid和ppid可以看到两个服务器进程之间的父子关系。父子进程此刻都处于等待状态,不过他们等待的目标不同:父进程监听客户请求,子进程则阻塞于read函数等待套接字数据的来临。
接下来通过发信号SIGINT(通过键盘上的ctrl+c可发出此信号)使客户进程终止。
edsionte@edsionte-desktop:~/echoCS$ ./client 127.0.0.1 received a connection from:127.0.0.1 ps ps ^C
我们在另一个终端查看pty/3终端当前进程的状态:
edsionte@edsionte-desktop:~/echoCS$ ps -t pts/3 -o pid,ppid,args,stat,wchan PID PPID COMMAND STAT WCHAN 5071 2305 bash Ss+ n_tty_read 6146 5071 ./server S inet_csk_wait_for_connect 6151 6146 [server] Z exit
可以看到,子服务器进程现在处于僵死状态。目前我们只有运行了一次客户端程序,如果一个服务器的网络请求繁忙,则会产生很多僵死进程。
2.处理僵死进程
当子进程终止时会给父进程发送SIGCHLD信号,因此我们可以利用信号处理函数捕获这个信号并对僵死进程进行处理。我们知道在父进程中调用wait函数可以防止先于父进程终止的子进程编程僵死进程,因此我们的信号捕获函数如下所示:
void sig_zchild(int signo) { pid_t pid; int stat; pid = wait(&stat); printf("child %d terminated\n", pid); return; }
并且我们需要适当的修改服务器程序,在accept函数调用之前调用signal函数:
if(listen(sockfd, BACKLOG) == -1) { printf("listen error!\n"); exit(1); } if (signal(SIGCHLD, sig_zchild) == SIG_ERR) { printf("signal error!\n"); exit(1); } while (1) { sin_size = sizeof(struct sockaddr_in); if ((client_fd = accept(sockfd, (struct sockaddr *)&remote_addr, &sin_size)) == -1) { printf("accept error!\n"); continue; } …… …… }//while
我们下面进行测试,我们首先在终端后台运行服务器程序,然后同时在其他多个终端分别运行客户端程序。当前终端的进程状态如下:
edsionte@edsionte-desktop:~/echoCS$ ps PID TTY TIME CMD 6699 pts/0 00:00:00 bash 6926 pts/0 00:00:00 server 6947 pts/0 00:00:00 server 6966 pts/0 00:00:00 server 6985 pts/0 00:00:00 server 6986 pts/0 00:00:00 ps
然后我们分别在运行客户程序的终端杀死客户端进程。再次查看当前终端下的进程状态,可以发现并没有僵死的子服务进程。因为每个子进程在终止时发送SIGCHLD信号都会被父进程中的信号处理函数捕获。
3.僵死进程处理的加强版
上述的客户端进程每次向服务器端只发出一个连接请求,当客户端终止时子服务器进程将终止;如果一个客户端向服务器发出多个连接请求,当该客户端终止时多个子服务器进程将同时终止,也就是说父服务器进程将面临同时处理多个SIGCHLD信号的情况。
按照上述的特殊客户端,我们将回射C/S模型中的客户端适当修改,使运行一次客户端就连接服务器5次,这样就会产生相应的5个子服务器进程。
int i; for (i = 0; i < 5; i++) { if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { printf("socket error!\n"); exit(1); } serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(SERV_PORT); serv_addr.sin_addr = *((struct in_addr *)host->h_addr); if(connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr)) == -1) { printf("connect error!\n"); exit(1); } } str_cli(stdin, sockfd);
运行服务器程序和刚刚修改过的客户端程序,我们作如下测试:
edsionte@edsionte-desktop:~/echoCS$ ps -t pts/0 -o pid,ppid,args,stat PID PPID COMMAND STAT 2651 2649 bash Ss 2861 2651 ./server S 2865 2651 ./mclient 127.0.0.1 S+ 2866 2861 ./server S 2867 2861 ./server S 2868 2861 ./server S 2869 2861 ./server S 2870 2861 ./server S edsionte@edsionte-desktop:~/echoCS$ kill 2865 edsionte@edsionte-desktop:~/echoCS$ ps -t pts/0 -o pid,ppid,args,stat PID PPID COMMAND STAT 2651 2649 bash Ss+ 2861 2651 ./server S 2867 2861 [server] Z 2868 2861 [server] Z 2869 2861 [server] Z 2870 2861 [server] Z
当杀死客户端进程后,只有一个子服务器进程被SIGCHLD信号处理函数捕获处理,其他四个子服务器进程仍然处于僵死状态。出现这种现象的原因是,主服务器进程中的信号处理函数使用了wait函数,该函数会一直阻塞到出现第一个终止的子进程。也就是说,该函数只等待第一个终止的子进程并返回。
本客户端程序终止后,将同时产生5个SIGCHLD信号,而wait函数只处理其中一个,解决这个问题的办法是使用waitpid函数。该函数将周期性的检查是否有子进程终止,也就是说可以等待所有的终止子进程。修改后的服务器信号处理函数如下:
void sig_zchild(int signo) { pid_t pid; int stat; while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) printf("child %d terminated\n", pid); return; }
按此修改后SIGCHLD信号处理函数后,就可以避免留下僵死进程。