기본 개념
유닉스/리눅스에서는 정상적인 상황에서 하위 프로세스가 상위 프로세스를 통해 생성되고, 하위 프로세스가 새로운 프로세스를 생성한다는 것을 알고 있습니다. 자식 프로세스의 종료와 부모 프로세스의 실행은 비동기 프로세스입니다. 즉, 부모 프로세스는 자식 프로세스가 언제 끝날지 예측할 수 없습니다. 프로세스가 작업을 완료하고 종료되면 상위 프로세스는 wait() 또는 waitpid() 시스템 호출을 호출하여 하위 프로세스의 종료 상태를 가져와야 합니다.
고아 프로세스
하나 이상의 하위 프로세스가 계속 실행되는 동안 상위 프로세스가 종료되면 해당 하위 프로세스는 고아 프로세스가 됩니다. 고아 프로세스는 init 프로세스(프로세스 번호는 1)에 의해 채택되고 init 프로세스는 이에 대한 상태 수집 작업을 완료합니다.
좀비 프로세스
프로세스가 포크를 사용하여 하위 프로세스를 생성하고 상위 프로세스가 하위 프로세스의 상태 정보를 얻기 위해 wait 또는 waitpid를 호출하지 않으면 하위 프로세스 설명자가 생성됩니다. 프로세스는 여전히 시스템에 저장됩니다. 이 프로세스를 좀비 프로세스라고 합니다.
문제 및 위험
Unix는 하위 프로세스가 종료될 때 상위 프로세스가 상태 정보를 알고 싶어하는 한 이를 얻을 수 있도록 보장하는 메커니즘을 제공합니다. 이 메커니즘은 다음과 같습니다. 각 프로세스가 종료되면 커널은 열린 파일, 점유된 메모리 등을 포함하여 프로세스의 모든 리소스를 해제합니다. 그러나 특정 정보(프로세스 ID, 프로세스 종료 상태, 프로세스에 소요된 CPU 시간 등)는 여전히 유지됩니다. 상위 프로세스가 wait/waitpid를 통해 이를 검색할 때까지 해제되지 않습니다. 그러나 이로 인해 프로세스가 wait/waitpid를 호출하지 않으면 보유된 정보가 공개되지 않고 해당 프로세스 번호가 항상 점유됩니다. 그러나 시스템에서 사용할 수 있는 프로세스 번호의 수는 제한됩니다. 다수의 좀비 프로세스가 생성되면 사용 가능한 프로세스 번호가 없기 때문에 시스템에서 새 프로세스를 생성할 수 없습니다. 이는 좀비 프로세스의 피해이므로 피해야 합니다.
고아 프로세스는 부모 프로세스가 없는 프로세스입니다. 고아 프로세스의 중요한 책임은 init 프로세스에 있습니다. init 프로세스는 고아 프로세스의 여파를 처리하는 민사국과 같습니다. 고아 프로세스가 나타날 때마다 커널은 고아 프로세스의 상위 프로세스를 init로 설정하고 init 프로세스는 종료된 하위 프로세스를 주기적으로 wait()합니다. 이러한 방식으로 고아 프로세스가 수명 주기를 비참하게 종료하면 init 프로세스가 당과 정부를 대신하여 모든 여파를 처리합니다. 따라서 고아 프로세스는 해를 끼치지 않습니다.
모든 하위 프로세스(init 제외)는 종료() 직후 사라지지 않고 좀비 프로세스(Zombie)라는 데이터 구조를 남기고 상위 프로세스가 처리되기를 기다립니다. 모든 하위 프로세스가 마지막에 거치는 단계입니다. 자식 프로세스가 종료() 이후에 처리할 시간이 없다면 ps 명령을 사용하여 자식 프로세스의 상태가 "Z"인지 확인할 수 있습니다. 부모 프로세스가 제 시간에 처리할 수 있다면 ps 명령을 사용하여 자식 프로세스의 좀비 상태를 확인하기에는 너무 늦을 수 있지만, 그렇다고 자식 프로세스가 좀비 상태를 거치지 않는다는 의미는 아닙니다. 하위 프로세스가 끝나기 전에 상위 프로세스가 종료되면 하위 프로세스가 init에 의해 인수됩니다. init는 좀비 상태의 자식 프로세스를 부모 프로세스로 처리합니다.
좀비 프로세스의 위험 시나리오
예를 들어, 정기적으로 하위 프로세스를 생성하는 프로세스가 있습니다. 이 하위 프로세스는 수행할 작업이 거의 없으며 수행해야 할 작업을 완료한 후 종료됩니다. 주기는 매우 짧지만 상위 프로세스는 새로운 하위 프로세스만 생성하고 하위 프로세스가 종료된 후에는 어떤 일이 발생하는지 신경 쓰지 않습니다. 이런 식으로 시스템이 일정 기간 동안 실행된 후에는 좀비 프로세스가 많이 있게 됩니다. ps 명령을 사용하여 시스템을 보면 상태가 Z인 프로세스가 많이 표시됩니다. 엄밀히 말하면 좀비 프로세스는 문제의 원인이 아닙니다. 범인은 수많은 좀비 프로세스를 생성한 상위 프로세스입니다. 따라서 시스템에서 대량의 좀비 프로세스를 제거하는 방법을 찾을 때 답은 대량의 좀비 프로세스를 생성한 범인을 제거하는 것입니다(즉, kill을 통해 SIGTERM 또는 SIGKILL 신호를 보내는 것). 범인 프로세스가 삭제된 후 생성된 좀비 프로세스는 고아 프로세스가 됩니다. 이러한 고아 프로세스는 init 프로세스에 의해 인수됩니다. init 프로세스는 이러한 고아 프로세스를 기다리고 시스템 프로세스 테이블에서 차지하는 리소스를 해제합니다. 이러한 방식으로 죽은 고아 프로세스는 평화롭게 사라질 수 있습니다.
고아 프로세스 및 좀비 프로세스 테스트
1. 고아 프로세스는 init 프로세스에 의해 채택되었습니다
$pid = pcntl_fork(); if ($pid > 0) { // 显示父进程的进程ID,这个函数可以是getmypid(),也可以用posix_getpid() echo "Father PID:" . getmypid() . PHP_EOL; // 让父进程停止两秒钟,在这两秒内,子进程的父进程ID还是这个父进程 sleep(2); } else if (0 == $pid) { // 让子进程循环10次,每次睡眠1s,然后每秒钟获取一次子进程的父进程进程ID for ($i = 1; $i <= 10; $i++) { sleep(1); // posix_getppid()函数的作用就是获取当前进程的父进程进程ID echo posix_getppid() . PHP_EOL; } } else { echo "fork error." . PHP_EOL; }
테스트 결과:
php daemo001.php Father PID:18046 18046 18046 www@iZ2zec3dge6rwz2uw4tveuZ:~/test$ 1 1 1 1 1 1 1 1
2 좀비 프로세스 및 위험
다음 코드를 실행합니다. .php
$pid = pcntl_fork(); if( $pid > 0 ){ // 下面这个函数可以更改php进程的名称 cli_set_process_title('php father process'); // 让主进程休息60秒钟 sleep(60); } else if( 0 == $pid ) { cli_set_process_title('php child process'); // 让子进程休息10秒钟,但是进程结束后,父进程不对子进程做任何处理工作,这样这个子进程就会变成僵尸进程 sleep(10); } else { exit('fork error.'.PHP_EOL); }
실행 결과, 다른 터미널 창
www@iZ2zec3dge6rwz2uw4tveuZ:~$ ps -aux|grep -v "grep\|nginx\|php-fpm" | grep php www 18458 0.5 1.2 204068 25920 pts/1 S+ 16:34 0:00 php father process www 18459 0.0 0.3 204068 6656 pts/1 S+ 16:34 0:00 php child process www@iZ2zec3dge6rwz2uw4tveuZ:~$ ps -aux|grep -v "grep\|nginx\|php-fpm" | grep php www 18458 0.0 1.2 204068 25920 pts/1 S+ 16:34 0:00 php father process www 18459 0.0 0.0 0 0 pts/1 Z+ 16:34 0:00 [php] <defunct>
ps -aux 명령을 실행하면 처음 10초 이내에 프로그램이 실행되면 php 하위 프로세스의 상태가 [S+]로 나열되는 것을 확인할 수 있습니다. 그러나 10초가 지나면 이 상태는 [Z+]가 되는데, 이는 시스템에 해를 끼치는 좀비 프로세스가 된다는 의미이다.
그럼 질문은요? 좀비 프로세스를 피하는 방법은 무엇입니까?
PHP通过 pcntl_wait()
和 pcntl_waitpid()
两个函数来帮我们解决这个问题。了解Linux系统编程的应该知道,看名字就知道这其实就是PHP把C语言中的 wait()
和 waitpid()
包装了一下。
通过代码演示 pcntl_wait()
来避免僵尸进程。
pcntl_wait()
函数:
这个函数的作用就是 “ 等待或者返回子进程的状态 ”,当父进程执行了该函数后,就会阻塞挂起等待子进程的状态一直等到子进程已经由于某种原因退出或者终止。
换句话说就是如果子进程还没结束,那么父进程就会一直等等等,如果子进程已经结束,那么父进程就会立刻得到子进程状态。这个函数返回退出的子进程的进程 ID 或者失败返回 -1。
执行以下代码 zombie2.php
$pid = pcntl_fork(); if ($pid > 0) { // 下面这个函数可以更改php进程的名称 cli_set_process_title('php father process'); // 返回$wait_result,就是子进程的进程号,如果子进程已经是僵尸进程则为0 // 子进程状态则保存在了$status参数中,可以通过pcntl_wexitstatus()等一系列函数来查看$status的状态信息是什么 $wait_result = pcntl_wait($status); print_r($wait_result); print_r($status); // 让主进程休息60秒钟 sleep(60); } else if (0 == $pid) { cli_set_process_title('php child process'); // 让子进程休息10秒钟,但是进程结束后,父进程不对子进程做任何处理工作,这样这个子进程就会变成僵尸进程 sleep(10); } else { exit('fork error.' . PHP_EOL); }
在另外一个终端中通过ps -aux查看,可以看到在前十秒内,php child process 是 [S+] 状态,然后十秒钟过后进程消失了,也就是被父进程回收了,没有变成僵尸进程。
www@iZ2zec3dge6rwz2uw4tveuZ:~/test$ ps -aux|grep -v "grep\|nginx\|php-fpm" | grep php www@iZ2zec3dge6rwz2uw4tveuZ:~/test$ ps -aux|grep -v "grep\|nginx\|php-fpm" | grep php www 18519 0.5 1.2 204068 25576 pts/1 S+ 16:42 0:00 php father process www 18520 0.0 0.3 204068 6652 pts/1 S+ 16:42 0:00 php child process www@iZ2zec3dge6rwz2uw4tveuZ:~/test$ ps -aux|grep -v "grep\|nginx\|php-fpm" | grep php www 18519 0.0 1.2 204068 25576 pts/1 S+ 16:42 0:00 php father process
但是,pcntl_wait() 有个很大的问题,就是阻塞。父进程只能挂起等待子进程结束或终止,在此期间父进程什么都不能做,这并不符合多快好省原则,所以 pcntl_waitpid() 闪亮登场。pcntl_waitpid( pid, &status, $option = 0 )的第三个参数如果设置为WNOHANG,那么父进程不会阻塞一直等待到有子进程退出或终止,否则将会和pcntl_wait()的表现类似。
修改第三个案例的代码,但是,我们并不添加WNOHANG,演示说明pcntl_waitpid()
功能:
$pid = pcntl_fork(); if ($pid > 0) { // 下面这个函数可以更改php进程的名称 cli_set_process_title('php father process'); // 返回值保存在$wait_result中 // $pid参数表示 子进程的进程ID // 子进程状态则保存在了参数$status中 // 将第三个option参数设置为常量WNOHANG,则可以避免主进程阻塞挂起,此处父进程将立即返回继续往下执行剩下的代码 $wait_result = pcntl_waitpid($pid, $status); var_dump($wait_result); var_dump($status); // 让主进程休息60秒钟 sleep(60); } else if (0 == $pid) { cli_set_process_title('php child process'); // 让子进程休息10秒钟,但是进程结束后,父进程不对子进程做任何处理工作,这样这个子进程就会变成僵尸进程 sleep(10); } else { exit('fork error.' . PHP_EOL); }
下面是运行结果,一个执行php zombie3.php 程序的终端窗口
www@iZ2zec3dge6rwz2uw4tveuZ:~/test$ php zombie3.php int(18586) int(0) ^C
ctrl-c 发送 SIGINT 信号给前台进程组中的所有进程。常用于终止正在运行的程序。
下面是ps -aux终端窗口
www@iZ2zec3dge6rwz2uw4tveuZ:~$ ps -aux|grep -v "grep\|nginx\|php-fpm" | grep php www 18605 0.3 1.2 204068 25756 pts/1 S+ 16:52 0:00 php father process www 18606 0.0 0.3 204068 6636 pts/1 S+ 16:52 0:00 php child process www@iZ2zec3dge6rwz2uw4tveuZ:~$ ps -aux|grep -v "grep\|nginx\|php-fpm" | grep php www 18605 0.1 1.2 204068 25756 pts/1 S+ 16:52 0:00 php father process www@iZ2zec3dge6rwz2uw4tveuZ:~$ ps -aux|grep -v "grep\|nginx\|php-fpm" | grep php www 18605 0.0 1.2 204068 25756 pts/1 S+ 16:52 0:00 php father process www@iZ2zec3dge6rwz2uw4tveuZ:~$ ps -aux|grep -v "grep\|nginx\|php-fpm" | grep php // ctrl-c 后不再被阻塞 www@iZ2zec3dge6rwz2uw4tveuZ:~$
实际上可以看到主进程是被阻塞的,一直到第十秒子进程退出了,父进程不再阻塞
修改第四段代码,添加第三个参数WNOHANG,代码如下:
$pid = pcntl_fork(); if ($pid > 0) { // 下面这个函数可以更改php进程的名称 cli_set_process_title('php father process'); // 返回值保存在$wait_result中 // $pid参数表示 子进程的进程ID // 子进程状态则保存在了参数$status中 // 将第三个option参数设置为常量WNOHANG,则可以避免主进程阻塞挂起,此处父进程将立即返回继续往下执行剩下的代码 $wait_result = pcntl_waitpid($pid, $status, WNOHANG); var_dump($wait_result); var_dump($status); echo "不阻塞,运行到这里" . PHP_EOL; // 让主进程休息60秒钟 sleep(60); } else if (0 == $pid) { cli_set_process_title('php child process'); // 让子进程休息10秒钟,但是进程结束后,父进程不对子进程做任何处理工作,这样这个子进程就会变成僵尸进程 sleep(10); } else { exit('fork error.' . PHP_EOL); }
执行 php zombie4.php
www@iZ2zec3dge6rwz2uw4tveuZ:~/test$ php zombie4.php int(0) int(0) 不阻塞,运行到这里
另一个ps -aux终端窗口
www@iZ2zec3dge6rwz2uw4tveuZ:~$ ps -aux|grep -v "grep\|nginx\|php-fpm" | grep php www 18672 0.3 1.2 204068 26284 pts/1 S+ 17:00 0:00 php father process www 18673 0.0 0.3 204068 6656 pts/1 S+ 17:00 0:00 php child process www@iZ2zec3dge6rwz2uw4tveuZ:~$ ps -aux|grep -v "grep\|nginx\|php-fpm" | grep php www 18672 0.0 1.2 204068 26284 pts/1 S+ 17:00 0:00 php father process www 18673 0.0 0.0 0 0 pts/1 Z+ 17:00 0:00 [php] <defunct>
实际上可以看到主进程是被阻塞的,一直到第十秒子进程退出了,父进程不再阻塞。
问题出现了,竟然php child process进程状态竟然变成了[Z+],这是怎么搞得?回头分析一下代码:
我们看到子进程是睡眠了十秒钟,而父进程在执行pcntl_waitpid()之前没有任何睡眠且本身不再阻塞,所以,主进程自己先执行下去了,而子进程在足足十秒钟后才结束,进程状态自然无法得到回收。
如果我们将代码修改一下,就是在主进程的pcntl_waitpid()前睡眠15秒钟,这样就可以回收子进程了。但是即便这样修改,细心想的话还是会有个问题,那就是在子进程结束后,在父进程执行pcntl_waitpid()回收前,有五秒钟的时间差,在这个时间差内,php child process也将会是僵尸进程。那么,pcntl_waitpid()如何正确使用啊?这样用,看起来毕竟不太科学。
那么,是时候引入信号学了!
위 내용은 PHP7 고아 프로세스와 좀비 프로세스의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!