1. 程式人生 > 實用技巧 >殭屍程序與SIGCHLD訊號

殭屍程序與SIGCHLD訊號


什麼是殭屍程序?

首先核心會釋放終止程序(呼叫了exit系統呼叫)所使用的所有儲存區,關閉所有開啟的檔案等,但核心為每一個終止子程序儲存了一定量的資訊。這些資訊至少包括程序ID,程序的終止狀態,以及該程序使用的CPU時間,所以當終止子程序的父程序呼叫wait或waitpid時就可以得到這些資訊。

而殭屍程序就是指:一個程序執行了exit系統呼叫退出,而其父程序並沒有為它收屍(呼叫wait或waitpid來獲得它的結束狀態)的程序。

任何一個子程序(init除外)在exit後並非馬上就消失,而是留下一個稱外殭屍程序的資料結構,等待父程序處理。這是每個子程序都必需經歷的階段。另外子程序退出的時候會向其父程序傳送一個SIGCHLD訊號。

殭屍程序的目的?

設定僵死狀態的目的是維護子程序的資訊,以便父程序在以後某個時候獲取。這些資訊至少包括程序ID,程序的終止狀態,以及該程序使用的CPU時間,所以當終止子程序的父程序呼叫wait或waitpid時就可以得到這些資訊。如果一個程序終止,而該程序有子程序處於殭屍狀態,那麼它的所有殭屍子程序的父程序ID將被重置為1(init程序)。繼承這些子程序的init程序將清理它們(也就是說init程序將wait它們,從而去除它們的殭屍狀態)。


如何避免殭屍程序? 1、通過signal(SIGCHLD, SIG_IGN)通知核心對子程序的結束不關心,由核心回收。如果不想讓父程序掛起,可以在父程序中加入一條語句:signal(SIGCHLD,SIG_IGN);表示父程序忽略SIGCHLD訊號,該訊號是子程序退出的時候向父程序傳送的。 2、父程序呼叫wait/waitpid等函式等待子程序結束,如果尚無子程序退出wait會導致父程序阻塞。waitpid可以通過傳遞WNOHANG使父程序不阻塞立即返回。 3、如果父程序很忙可以用signal註冊訊號處理函式,在訊號處理函式呼叫wait/waitpid等待子程序退出。 4、通過兩次呼叫fork。父程序首先呼叫fork建立一個子程序然後waitpid等待子程序退出,子程序再fork一個孫程序後退出。這樣子程序退出後會被父程序等待回收,而對於孫子程序其父程序已經退出所以孫程序成為一個孤兒程序,孤兒程序由init程序接管,孫程序結束後,init會等待回收。 第一種方法忽略SIGCHLD訊號,這常用於併發伺服器的效能的一個技巧因為併發伺服器常常fork很多子程序,子程序終結之後需要伺服器程序去wait清理資源。如果將此訊號的處理方式設為忽略,可讓核心把殭屍子程序轉交給init程序去處理,省去了大量殭屍程序佔用系統資源。


  如果伺服器端因為客戶端的關閉,迴圈開啟的處理子程序會成為殭屍程序。當有5個子程序退出,就會有5個SIGCHLD 訊號發給父程序。當第一個訊號過來,父程序處於sighandler過程,在此過程中,若有其他訊號過來,訊號(不可靠訊號)會丟失,只排隊一個。所以最終只會處理兩個SIGCHLD訊號,剩3個殭屍程序;若是5個一起來,可能只處理第一個(剩4個殭屍程序)。用while解決

/*
服務端每次會fork一個子程序處理客戶單請求,每個客戶端斷開連線,會有一個子程序成為殭屍程序(父程序不處理的話)
*/
#include<unistd.h>
#include<sys/types.h>
#include
<sys/socket.h> #include<string.h> #include<stdlib.h> #include<stdio.h> #include<errno.h> #include<netinet/in.h> #include<arpa/inet.h> #include<signal.h> #include<sys/wait.h> #define ERR_EXIT(m)\ do\ {\ perror(m);\ exit(EXIT_FAILURE);\ }while(0) ssize_t readn(int fd,void *buf,size_t count) { size_t nleft=count; ssize_t nread; char *bufp=(char*)buf; while(nleft>0) { if((nread=read(fd,bufp,nleft))<0) { if(errno==EINTR) continue; else return -1; } else if(nread==0) return (count-nleft); bufp+=nread; nleft-=nread; } return count; } ssize_t writen(int fd, const void *buf, size_t count) { size_t nleft=count; ssize_t nwritten; char *bufp=(char*)buf; while(nleft>0) { if((nwritten=write(fd,bufp,nleft))<=0) { if(errno==EINTR) continue; return -1; }else if(nwritten==0) continue; bufp+=nwritten; nleft-=nwritten; } return count; } ssize_t recv_peek(int sockfd,void *buf,size_t len) { while(1) { int ret=recv(sockfd,buf,len,MSG_PEEK);//從sockfd讀取內容到buf,但不去清空sockfd,偷窺 if(ret==-1&&errno==EINTR) continue; return ret; } } //偷窺方案實現readline避免一次讀取一個字元 ssize_t readline(int sockfd,void * buf,size_t maxline) { int ret; int nread; size_t nleft=maxline; char *bufp=(char*)buf; while(1) { ret=recv_peek(sockfd,bufp,nleft);//不清除sockfd,只是窺看 if(ret<0) return ret; else if(ret==0) return ret; nread=ret; int i; for(i=0;i<nread;i++) { if(bufp[i]=='\n') { ret=readn(sockfd,bufp,i+1);//讀出sockfd中的一行並且清空 if(ret!=i+1) exit(EXIT_FAILURE); return ret; } } if(nread>nleft) exit(EXIT_FAILURE); nleft-=nread; ret=readn(sockfd,bufp,nread); if(ret!=nread) exit(EXIT_FAILURE); bufp+=nread;//移動指標繼續窺看 } return -1; } void echo_srv(int conn) { int ret; char recvbuf[1024]; while(1) { memset(&recvbuf,0,sizeof(recvbuf)); //使用readn之後客戶端傳送的資料不足1024會阻塞 //在客戶端程式中確定訊息的邊界,傳送定長包 ret=readline(conn,recvbuf,1024); //客戶端關閉 if(ret==-1) ERR_EXIT("readline"); else if(ret==0) { printf("client close\n"); break;//不用繼續迴圈等待客戶端資料 } fputs(recvbuf,stdout); writen(conn,recvbuf,strlen(recvbuf)); } } void handle_sigchld(int sig) { //法一 :只能等待第一個,不能等待所有的子程序,通常剩餘3個殭屍程序。剩餘3個說明不可靠訊號排隊一個 //wait(NULL); //當沒有子程序退出,返回-1 while(waitpid(-1,NULL, WNOHANG)>0) ;//pid==-1等待所有子程序結束。等到一個子程序返回值大於0 } int main(void) { //方法一: //signal(SIGCHLD,SIG_IGN);//父程序忽略子程序終止訊號,解決殭屍程序 /*方法二 */ signal(SIGCHLD,handle_sigchld); int listenfd; if((listenfd=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0) ERR_EXIT("socket error"); //if((listenfd=socket(PF_INET,SOCK_STREAM,0))<0) //本地協議地址賦給一個套接字 struct sockaddr_in servaddr; memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family=AF_INET; servaddr.sin_port=htons(5188); servaddr.sin_addr.s_addr=htonl(INADDR_ANY);//表示本機地址 //開啟地址重複使用,關閉伺服器再開啟不用等待TIME_WAIT int on=1; if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0) ERR_EXIT("setsockopt error"); //繫結本地套接字 if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0) ERR_EXIT("bind error"); if(listen(listenfd,SOMAXCONN)<0)//設定監聽套接字(被動套接字) ERR_EXIT("listen error"); struct sockaddr_in peeraddr;//對方套接字地址 socklen_t peerlen=sizeof(peeraddr); int conn;//已連線套接字(主動套接字) pid_t pid; while(1){ if((conn=accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0) ERR_EXIT("accept error"); //連線好之後就構成連線,埠是客戶端的。peeraddr是對端 printf("ip=%s port=%d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port)); pid=fork(); if(pid==-1) ERR_EXIT("fork"); if(pid==0){ close(listenfd); echo_srv(conn); //某個客戶端關閉,結束該子程序,否則子程序也去接受連線 //雖然結束了exit退出,但是核心還保留了其資訊,父程序並未為其收屍。 exit(EXIT_SUCCESS); }else close(conn); } return 0; }

//客戶端:建立5個連線請求服務端,再關閉。

//五個客戶端去連線併發伺服器 
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<stdlib.h>
#include<stdio.h>
#include<errno.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<signal.h>
#define ERR_EXIT(m)\
    do\
    {\
        perror(m);\
        exit(EXIT_FAILURE);\
    }while(0)
ssize_t readn(int fd,void *buf,size_t count)
{
    size_t nleft=count;
    ssize_t nread;
    char *bufp=(char*)buf;
    while(nleft>0)
    {
        if((nread=read(fd,bufp,nleft))<0)
        {
            if(errno==EINTR)
                continue;
            else
                return -1;
        }
        else if(nread==0)
            return (count-nleft);
        bufp+=nread;
        nleft-=nread;
    }
    return count;
}
ssize_t writen(int fd, const void *buf, size_t count)
{
    size_t nleft=count;
    ssize_t nwritten;
    char *bufp=(char*)buf;
    while(nleft>0)
    {
        if((nwritten=write(fd,bufp,nleft))<=0)
        {
            if(errno==EINTR)
                continue;
            return -1;
        }else if(nwritten==0)
            continue;
        bufp+=nwritten;
        nleft-=nwritten;
    }
    return count;

}
ssize_t recv_peek(int sockfd,void *buf,size_t len)
{
    while(1)
    {
        int ret=recv(sockfd,buf,len,MSG_PEEK);//從sockfd讀取內容到buf,但不去清空sockfd,偷窺
        if(ret==-1&&errno==EINTR)
            continue;
        return ret;
    }
}
//偷窺方案實現readline避免一次讀取一個字元
ssize_t readline(int sockfd,void * buf,size_t maxline)
{
    int ret;
    int nread;
    size_t nleft=maxline;
    char *bufp=(char*)buf;
    while(1)
    {
        ret=recv_peek(sockfd,bufp,nleft);//不清除sockfd,只是窺看
        if(ret<0)
            return ret;
        else if(ret==0)
            return ret;
        nread=ret;
        int i;
        for(i=0;i<nread;i++)
        {
            if(bufp[i]=='\n')
            {
                ret=readn(sockfd,bufp,i+1);//讀出sockfd中的一行並且清空
                if(ret!=i+1)
                    exit(EXIT_FAILURE);
                return ret;
            }
        }
        if(nread>nleft)
            exit(EXIT_FAILURE);
        nleft-=nread;
        ret=readn(sockfd,bufp,nread);
        if(ret!=nread)
            exit(EXIT_FAILURE);
        bufp+=nread;//移動指標繼續窺看
    }
    return -1;
}
void echo_cli(int sock)
{
    char sendbuf[1024]={0};
    char recvbuf[1024]={0};    
    while(fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)//預設有換行符
    {
        writen(sock,sendbuf,strlen(sendbuf));
        int ret=readline(sock,recvbuf,1024);
        if(ret==-1)
            ERR_EXIT("readline");
        else if(ret==0)
        {
            printf("service closed\n");
            break;
        }
        fputs(recvbuf,stdout);
        memset(sendbuf,0,sizeof(sendbuf));
        memset(recvbuf,0,sizeof(recvbuf));
    }
    close(sock);
}
int main(void)
{
    int sock[5];//客戶端建立5個套接字
    int i=0;
    for(i=0;i<5;i++)
    {
        if((sock[i]=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)
            ERR_EXIT("socket error");
    
        struct sockaddr_in servaddr;//本地協議地址賦給一個套接字
        memset(&servaddr,0,sizeof(servaddr));
        servaddr.sin_family=AF_INET;
        servaddr.sin_port=htons(5188);
    
        servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");//伺服器段地址
        //inet_aton("127.0.0.1",&servaddr.sin_addr);
    
        if(connect(sock[i],(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
            ERR_EXIT("connect");

        //利用getsockname獲取客戶端本身地址和埠,即為對方accept中的對方套介面
        struct sockaddr_in localaddr;
        socklen_t addrlen=sizeof(localaddr);
        if(getsockname(sock[i],(struct sockaddr *)&localaddr,&addrlen)<0)
            ERR_EXIT("getsockname error");
        printf("local IP=%s, local port=%d\n",inet_ntoa(localaddr.sin_addr),ntohs(localaddr.sin_port));    
        //使用getpeername獲取對方地址
    
        
    }
    echo_cli(sock[0]);//選擇一個與伺服器通訊
    return 0;
}