tcp回射服務器程序處理僵死進程
什麼是僵死進程
當子進程調用exit指令退出的時候,會留下一個一個成爲僵死(zombie)的數據結構,目的是維護子進程的信息,以便父進程在以後的某個時候獲取,這些信息包括子進程的進程ID,終止狀態以及資源利用信息等。在退出時他會向父進程發送SIGCHLD信號,如果父進程沒有對該信號進行處理,該退出的子進程就會一直處於僵死狀態,佔用內核中的空間,多了以後甚至會導致我們耗盡進程資源。
如果tcp回射服務器不對SIGCHLD信號進行處理
tcp回射服務器程序如下(摘錄自《UNIX網絡編程 卷一》)
#include "unp.h"
int
main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
for ( ; ; ) {
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
if ( (childpid = Fork()) == 0) { /* child process */
Close(listenfd); /* close listening socket */
str_echo(connfd); /* process the request */
exit(0);
}
Close(connfd); /* parent closes connected socket */
}
}
以上代碼段未對SIGCHLD信號進行處理,所以導致我們鍵入EOF字符來終止客戶時候,對應該套接字的子進程就會一直處於僵死狀態。
把兩個客戶端程序關閉,父進程沒有對子進程發來的SIGCHLD信號處理導致子進程一直處於僵死狀態。
對SIGCHLD信號進行處理
父進程接收到SIGCHLD信號後調用wait或waitpid函數來爲子進程”收屍”。這兩個函數的原型如下:
#include<sys/wait.h>
#include "unp.h"
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid,int *statloc,int options);
/*這兩個函數均返回兩個值:已終止的進程ID號和通過statloc指針返回的子進
程終止狀態。通過設置options,我們可以指定附加選項,常用的選項是
WNOHANG,它告知內核在沒有已終止子進程時不要阻塞。pid參數允許我們制定
想等待的進程ID,如果值爲-1則表示等待第一個終止的進程。
*/
/*
如果調用wait的進程沒有已終止的子進程,不過有一個或多個子進程仍在執行,
那麼wait將阻塞到現有的子進程第一個終止爲止
*/
void sig_chld(int signo) {
pid_t pid;
int stat;
pid = wait(&stat);
printf("chlild %d terminated\n",pid);
return;
}
//如果調用waitpid則可以通過循環等待所有子進程結束
void sig_chld(int signo) {
pid_t pid;
int stat;
while(pid = waitpid(-1,&stat,WNOHANG)) > 0)
printf("child %d terminated\n",pid);
return;
}
我們應該使用wait還是waitpid?答案肯定是waitpid。我們假設一個情景:客戶端程序通過循環與服務器建立了5個TCP連接,然後退出客戶端程序,這5個連接幾乎在同一時刻發給父進程SIGCHLD信號。因爲Unix信號一般是不排隊的,信號處理函數只執行一次。所以,如果使用wait,處理完第一個信號,信號處理函數就返回了,導致其他四個信號沒有得到處理,還是導致了僵死。如果用waitpid的時候,我們可以通過一個循環,處理所有SIGCHLD信號,這樣就沒有僵死信號存留了。
注意:我們應該在listen調用之後增加該信號處理函數
Signal(SIGCHLD,sig_chld);
這必須在fork第一個子進程之前完成,且只做一次)
編寫捕獲信號的網絡程序時,必須認清被中斷的系統調用並處理他們
我們的服務器程序阻塞於慢系統調用(accept)捕獲該信號,內核就會使accept返回一個EINTR錯誤(被中斷的系統調用)而如果父進程不處理該錯誤,就會被中止。對於那些可能永遠阻塞的函數,我們可以稱之爲慢系統調用。有些內核會自動重啓被中斷的系統調用,有些不會。所以爲了程序的健壯性。我們要做的事情就是自己重啓被中斷的系統調用。
//在判斷到EINTR錯誤的時候,執行continue返回循環重啓accept
//因爲本文的程序基本摘抄自《Unix網絡編程 卷一》所以讀者如過要
//編譯這些代碼的話需要該書提供的一些庫
for( ; ;) {
clilen = sizeof(cliaddr);
if( (connfd = accept(listenfd, (SA*) &cliaddr, &clilen) < 0) {
if(errno == EINTR)
continue;
}
else
err_sys("accept error");
}
總結
我們在網絡編程的時候要注意這三種情況:
- 當fork子進程時,必須捕獲SIGCHLD信號
- 當捕獲信號時,必須處理被中斷的系統調用
SIGCHLD的信號處理函數必須正確編寫,應使用waitpid函數以免下僵死進程。
最後貼出注意以上三點後的服務器程序代碼
#include "unp.h"
int
main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
void sig_chld(int);
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
Signal(SIGCHLD, sig_chld); /* must call waitpid() */
for ( ; ; ) {
clilen = sizeof(cliaddr);
if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
if (errno == EINTR)
continue; /* back to for() */
else
err_sys("accept error");
}
if ( (childpid = Fork()) == 0) { /* child process */
Close(listenfd); /* close listening socket */
str_echo(connfd); /* process the request */
exit(0);
}
Close(connfd); /* parent closes connected socket */
}
}