什么是僵尸进程?
首先内核会释放终止进程(调用了exit系统调用)所使用的所有存储区,关闭所有打开的文件等,但内核为每一个终止子进程保存了一定量的信息。这些信息至少包括进程ID,进程的终止状态,以及该进程使用的CPU时间,所以当终止子进程的父进程调用wait或waitpid时就可以得到这些信息。
而僵尸进程就是指:一个进程执行了exit系统调用退出,而其父进程并没有为它收尸(调用wait或waitpid来获得它的结束状态)的进程。
任何一个子进程(init除外)在exit后并非马上就消失,而是留下一个称外僵尸进程的数据结构,等待父进程处理。这是每个子进程都必需经历的阶段。另外子进程退出的时候会向其父进程发送一个SIGCHLD信号。
在回射服务器程序中,当客户端关闭,服务器程序中处理该客户端通信的子进程结束。此时该子进程等待父进程回收,否则该子进程处于僵尸状态。
僵尸进程的目的?
设置僵死状态的目的是维护子进程的信息,以便父进程在以后某个时候获取。这些信息至少包括进程ID,进程的终止状态,以及该进程使用的CPU时间,所以当终止子进程的父进程调用wait或waitpid时就可以得到这些信息。如果一个进程终止,而该进程有子进程处于僵尸状态,那么它的所有僵尸子进程的父进程ID将被重置为1(init进程)。继承这些子进程的init进程将清理它们(也就是说init进程将wait它们,从而去除它们的僵尸状态)。
模拟5个客户端去连接服务器,然后退出,那么服务器就会有5个子进程来处理这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]==' ') { 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 "); 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 ",inet_ntoa(localaddr.sin_addr),ntohs(localaddr.sin_port)); //使用getpeername获取对方地址 } echo_cli(sock[0]);//选择一个与服务器通信 return 0; }
如何避免僵尸进程?
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进程去处理,省去了大量僵尸进程占用系统资源。
服务器端程序:
#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]==' ') { 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 "); break;//不用继续循环等待客户端数据 } fputs(recvbuf,stdout); writen(conn,recvbuf,strlen(recvbuf)); } } /* 1、客户端关闭时,有5个信号要发给父进程,如果在处理一个信号处理函数的时候,其他几个信号到来(不是同时到达),不可靠信号只能排队一个,那么就会处理两个 一个是当前处理的,一个是排队的,另外三个是不会被处理的,只会丢失,所以会有三个僵尸进程。 2、另一种情况可能出现4个:如果5个信号同时到达,那么只会保留一个,其他的4个信号丢失,那么会有4个僵尸进程。
只要用一个while循环即可解决这个问题:
1、这么考虑:如果每个子进程结束到达的信号不是一起的,那么每次都会调用信号处理函数来处理,不会有信号丢失;
2、如果像上面的两种情况,有子进程是同时终止的,就会同时发来几个信号,那么while循环正好可以处理掉这几个信号。处理掉一个返回pid>0,还在while循环,继续调用waitpid...
[root@VM_0_2_centos ~]# ps -aux | grep 01serve
root 9720 0.0 0.0 4160 488 pts/0 S+ 18:48 0:00 ./01serve
root 9729 0.0 0.0 0 0 pts/0 Z+ 18:48 0:00 [01serve] <defunct>
root 9730 0.0 0.0 0 0 pts/0 Z+ 18:48 0:00 [01serve] <defunct>
root 9731 0.0 0.0 0 0 pts/0 Z+ 18:48 0:00 [01serve] <defunct>
root 9732 0.0 0.0 0 0 pts/0 Z+ 18:48 0:00 [01serve] <defunct>
root 9753 0.0 0.0 112644 964 pts/1 R+ 18:49 0:00 grep --color=auto 01serve
*/ void handle_sigchld(int sig) { //wait(NULL);//只能等待第一个。 while(waitpid(-1,NULL, WNOHANG)>0) ;//pid==-1等待所有子进程结束,等到一个子进程返回值大于0.非阻塞其实是一直返回的,若没有子进程结束,一直返回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 ",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; }