좀비 상태를 프로세스로 설정하는 목적은 나중에 부모 프로세스가 얻을 수 있도록 자식 프로세스의 정보를 유지하는 것입니다. 이 정보에는 하위 프로세스의 프로세스 ID, 종료 상태, 리소스 활용 정보(CPU 시간, 메모리 사용량 등)가 포함됩니다. 프로세스가 종료되고 좀비 상태의 하위 프로세스가 있는 경우 모든 좀비 하위 프로세스의 상위 프로세스 ID는 1(init 프로세스)로 재설정됩니다. 이러한 하위 프로세스를 상속하는 init 프로세스는 해당 하위 프로세스를 정리합니다(init 프로세스는 해당 하위 프로세스를 기다려 좀비 상태를 제거합니다).
그러나 일반적으로 우리는 좀비 프로세스를 유지하려고 하지 않습니다. 좀비 프로세스는 커널의 공간을 차지하고 결국 프로세스 리소스를 고갈시킬 수 있습니다. 그렇다면 좀비 프로세스가 생성되는 이유와 이를 방지하는 방법은 무엇입니까? 아래에서 이 두 가지 측면을 분석하겠습니다.
좀비 프로세스가 발생하는 이유
현재 프로세스에서 하위 프로세스를 생성하려면 일반적으로 포크 시스템 호출을 호출해야 한다는 것을 알고 있습니다. 한 번은 부모 프로세스로, 한 번은 자식 프로세스로 반환을 두 번 호출합니다. 반환 값으로 반환 지점을 판단할 수 있습니다.
pid_t child = fork();if( child < 0 ) { //fork error. perror("fork process fail.\n"); } else if( child ==0 ) { // in child process printf(" fork succ, this run in child process\n "); } else { // in parent process printf(" this run in parent process\n "); }
자식 프로세스가 부모 프로세스보다 먼저 종료되고 상위 프로세스는 그렇지 않습니다. wait/waitpid가 호출되면 하위 프로세스는 좀비 프로세스가 됩니다. ps 명령을 통해 그림 1과 같이 프로세스 상태가 Z(좀비를 나타냄)임을 알 수 있습니다.
(그림 1)
참고: 일부 Unix 시스템에서는 21eba13340b1d3a525d7480ca5bbd370를 사용하여 ps 명령 출력의 COMMAND 열에 좀비 프로세스를 나타냅니다.
코드는 다음과 같습니다.
if( child == -1 ) { //error perror("\nfork child error."); exit(0); } else if(child == 0){ cout << "\nIm in child process:" << getpid() << endl; exit(0); } else { cout << "\nIm in parent process." << endl; sleep(600); }
상위 프로세스를 600초 동안 대기시킨 후 하위 프로세스가 먼저 종료되는 것을 볼 수 있습니다. 프로세스 (프로세스 상태는 Z)
좀비 프로세스 방지
좀비 프로세스가 생성되는 이유를 알았습니다. 좀비 프로세스를 방지하는 방법을 살펴보겠습니다.
일반적으로 좀비 프로세스가 생성되는 것을 방지하려면 자식 프로세스를 포크한 후 기다려야 하며 동시에 자식 프로세스가 종료되면 커널은 부모 프로세스에 SIGCHLD를 제공합니다. SIGCHLD 신호의 신호 처리 함수는 좀비 프로세스를 방지하기 위해 함수 본문에서 wait(또는 waitpid)를 호출하여 기존 하위 프로세스를 정리할 수 있습니다. 다음 코드와 같이
void sig_chld( int signo ) { pid_t pid; int stat; pid = wait(&stat); printf( "child %d exit\n", pid ); return; }int main() { signal(SIGCHLD, &sig_chld); }
이제 메인 함수에 SIGCHLD 신호에 대한 신호 처리 함수(sig_chld)를 등록한 다음 하위 프로세스가 종료되면 커널이 SIGCHLD를 제출할 때 메인 프로세스에 의해 캡처됩니다. 신호 처리 함수 sig_chld를 입력한 다음 sig_chld에서 wait를 호출하여 종료된 하위 프로세스를 정리합니다. 이런 방식으로 종료된 자식 프로세스는 좀비 프로세스가 되지 않습니다.
그러면 SIGCHLD 신호를 캡처하고 종료 프로세스를 정리하기 위해 wait를 호출하더라도 여전히 좀비 프로세스 생성을 완전히 피할 수는 없습니다. 특별한 상황을 살펴보겠습니다.
클라이언트/서버 프로그램이 있다고 가정하면, 연결된 각 클라이언트에 대해 서버는 이 클라이언트의 요청을 처리하기 위해 새로운 프로세스를 시작합니다. 그런 다음 클라이언트 프로세스가 있습니다. 이 프로세스에서는 서버에 대한 여러 요청이 시작되고(5로 가정) 서버는 클라이언트 입력을 읽고 처리하기 위해 5개의 하위 프로세스를 포크합니다(동시에 클라이언트가 닫힐 때). 소켓, 각 하위 프로세스가 종료됨) 클라이언트 프로세스를 종료하면 커널은 클라이언트 프로세스에 의해 열린 모든 소켓을 자동으로 닫은 다음 클라이언트 프로세스에 의해 시작된 5개의 연결이 기본적으로 동시에 종료됩니다. 이렇게 하면 각 연결마다 하나씩 5개의 FIN이 발생합니다. 서버가 이 5개의 FIN을 수신하면 기본적으로 5개의 하위 프로세스가 동시에 종료됩니다. 이로 인해 그림 2에 표시된 것처럼 5개의 SIGCHLD 신호가 거의 동시에 상위 프로세스에 제출됩니다.
(그림 2)
정확히 동일한 신호의 여러 인스턴스 전달은 우리가 곧 살펴보려는 문제를 야기합니다.
먼저 서버 프로그램을 실행한 다음 클라이언트 프로그램을 실행하고 ps 명령을 사용하여 그림 3과 같이 서버가 5개의 하위 프로세스를 분기했는지 확인합니다.
(图3)
然后我们Ctrl+C终止客户端进程,在我机器上边测试,可以看到信号处理函数运行了3次,还剩下2个僵尸进程,如图4:
(图4)
通过上边这个实验我们可以看出,建立信号处理函数并在其中调用wait并不足以防止出现僵尸进程,其原因在于:所有5个信号都在信号处理函数执行之前产生,而信号处理函数只执行一次,因为Unix信号一般是不排队的。
更为严重的是,本问题是不确定的,依赖于客户FIN到达服务器主机的时机,信号处理函数执行的次数并不确定。
正确的解决办法是调用waitpid而不是wait,这个办法的方法为:信号处理函数中,在一个循环内调用waitpid,以获取所有已终止子进程的状态。我们必须指定WNOHANG选项,他告知waitpid在有尚未终止的子进程在运行时不要阻塞。(我们不能在循环内调用wait,因为没有办法防止wait在尚有未终止的子进程在运行时阻塞,wait将会阻塞到现有的子进程中第一个终止为止),下边的程序分别给出了这两种处理办法(func_wait, func_waitpid)。
//server.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/socket.h> #include <errno.h> #include <error.h> #include <netinet/in.h> #include <netinet/ip.h> #include <arpa/inet.h> #include <string.h> #include <signal.h> #include <sys/wait.h> typedef void sigfunc(int); void func_wait(int signo) { pid_t pid; int stat; pid = wait(&stat); printf( "child %d exit\n", pid ); return; } void func_waitpid(int signo) { pid_t pid; int stat; while( (pid = waitpid(-1, &stat, WNOHANG)) > 0 ) { printf( "child %d exit\n", pid ); } return; } sigfunc* signal( int signo, sigfunc *func ) { struct sigaction act, oact; act.sa_handler = func; sigemptyset(&act.sa_mask); act.sa_flags = 0; if ( signo == SIGALRM ) { #ifdef SA_INTERRUPT act.sa_flags |= SA_INTERRUPT; /* SunOS 4.x */ #endif } else { #ifdef SA_RESTART act.sa_flags |= SA_RESTART; /* SVR4, 4.4BSD */ #endif } if ( sigaction(signo, &act, &oact) < 0 ) { return SIG_ERR; } return oact.sa_handler; } void str_echo( int cfd ) { ssize_t n; char buf[1024]; again: memset(buf, 0, sizeof(buf)); while( (n = read(cfd, buf, 1024)) > 0 ) { write(cfd, buf, n); } if( n <0 && errno == EINTR ) { goto again; } else { printf("str_echo: read error\n"); } } int main() { signal(SIGCHLD, &func_waitpid); int s, c; pid_t child; if( (s = socket(AF_INET, SOCK_STREAM, 0)) < 0 ) { int e = errno; perror("create socket fail.\n"); exit(0); } struct sockaddr_in server_addr, child_addr; bzero(&server_addr, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(9998); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); if( bind(s, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0 ) { int e = errno; perror("bind address fail.\n"); exit(0); } if( listen(s, 1024) < 0 ) { int e = errno; perror("listen fail.\n"); exit(0); } while(1) { socklen_t chilen = sizeof(child_addr); if ( (c = accept(s, (struct sockaddr *)&child_addr, &chilen)) < 0 ) { perror("listen fail."); exit(0); } if( (child = fork()) == 0 ) { close(s); str_echo(c); exit(0); } close(c); } } //client.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/socket.h> #include <errno.h> #include <error.h> #include <netinet/in.h> #include <netinet/ip.h> #include <arpa/inet.h> #include <string.h> #include <signal.h> void str_cli(FILE *fp, int sfd ) { char sendline[1024], recvline[2014]; memset(recvline, 0, sizeof(sendline)); memset(sendline, 0, sizeof(recvline)); while( fgets(sendline, 1024, fp) != NULL ) { write(sfd, sendline, strlen(sendline)); if( read(sfd, recvline, 1024) == 0 ) { printf("server term prematurely.\n"); } fputs(recvline, stdout); memset(recvline, 0, sizeof(sendline)); memset(sendline, 0, sizeof(recvline)); } } int main() { int s[5]; for (int i=0; i<5; i++) { if( (s[i] = socket(AF_INET, SOCK_STREAM, 0)) < 0 ) { int e = errno; perror("create socket fail.\n"); exit(0); } } for (int i=0; i<5; i++) { struct sockaddr_in server_addr, child_addr; bzero(&server_addr, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(9998); inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr); if( connect(s[i], (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0 ) { perror("connect fail."); exit(0); } } sleep(10); str_cli(stdin, s[0]); exit(0); }
更多 linux僵尸进程产生的原因以及如何避免产生僵尸进程相关文章请关注PHP中文网!