基本概念
我們知道在unix/linux中,正常情況下,子進程是透過父進程創建的,子進程正在創建新的進程。子進程的結束和父進程的運行是一個非同步過程,即父進程永遠無法預測子進程 到底什麼時候結束。當一個 行程完成它的工作終止之後,它的父行程需要呼叫wait()或waitpid()系統呼叫取得子程序的終止狀態。
孤兒進程
一個父進程退出,而它的一個或多個子進程仍在運行,那麼那些子進程將成為孤兒進程。孤兒進程將被init進程(進程編號為1)所收養,並由init進程對它們完成狀態收集工作。
殭屍進程
一個進程使用fork建立子進程,如果子進程退出,而父進程並沒有呼叫wait或waitpid獲取子進程的狀態信息,那麼子進程的進程描述符仍然保存在系統中。這種進程稱之為僵死進程。
問題及危害
unix提供了一個機制可以保證只要父進程想知道子進程結束時的狀態信息, 就可以得到。這種機制就是: 在每個行程退出的時候,核心釋放該行程所有的資源,包括開啟的檔案,所佔用的記憶體等。但仍為其保留一定的資訊(包括進程號the process ID,退出狀態the termination status of the process,運行時間the amount of CPU time taken by the process等)。直到父行程透過wait / waitpid來取時才釋放。但這樣就導致了問題,如果進程不調用wait / waitpid的話, 那麼保留的那段資訊就不會釋放,其進程號就會一直被佔用,但是系統所能使用的進程號是有限的,如果大量的產生僵死進程,將因為沒有可用的進程號而導致系統不能產生新的進程. 此即為殭屍進程的危害,應當避免。
孤兒進程是沒有父進程的進程,孤兒進程這個重任就落到了init進程身上,init進程就好像是一個民政局,專門負責處理孤兒進程的善後工作。每當出現孤兒行程的時候,核心就把孤兒行程的父行程設定為init,而init行程會循環地wait()它的已經退出的子行程。這樣,當一個孤兒進程淒涼地結束了其生命週期的時候,init進程就會代表黨和政府出面處理它的一切善後工作。因此孤兒進程並不會有什麼危害。
任何一個子程序(init除外)在exit()之後,並非馬上就消失掉,而是留下一個稱為殭屍進程(Zombie)的資料結構,等待父進程處理。這是每個 子進程在結束時都要經過的階段。如果子行程在exit()之後,父行程還沒來得及處理,這時用ps指令就能看到子行程的狀態是「Z」。如果父行程能及時 處理,可能用ps指令就來不及看到子行程的殭屍狀態,但這並不等於子行程不經過殭屍狀態。如果父進程在子進程結束之前退出,則子進程將由init接管。 init將會以父行程的身份處理殭屍狀態的子行程。
殭屍行程危害場景
例如有個行程,它定期的產生一個子行程,這個子行程需要做的事情很少,做完它該做的事情之後就退出了,因此這個子進程的生命週期很短,但是,父進程只管生成新的子進程,至於子進程退出之後的事情,則一概不聞不問,這樣,系統運行上一段時間之後,系統中就會存在很多的僵死進程,倘若用ps指令查看的話,就會看到很多狀態為Z的進程。嚴格來說,僵死進程並不是問題的根源,罪魁禍首是產生大量僵死進程的那個父進程。因此,當我們尋求如何消滅系統中大量的僵死進程時,答案就是把產生大 量僵死進程的那個元兇槍斃掉(也就是透過kill發送SIGTERM或SIGKILL訊號啦)。槍斃了元兇進程之後,它產生的僵死進程就變成了孤兒進程,這些孤兒進程會被init進程接管,init進程會wait()這些孤兒進程,釋放它們佔用的系統進程表中的資源,這樣,這些已經僵死的孤兒進程就能瞑目而走了。
孤兒行程與殭屍行程測試
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 zombie1.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 指令可以看到,當程式在前十秒內運行的時候,php child process 的狀態列為[S ],然而在十秒鐘過後,這個狀態變成了[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中文網其他相關文章!