基本概念php
咱們知道在unix/linux中,正常狀況下,子進程是經過父進程建立的,子進程在建立新的進程。子進程的結束和父進程的運行是一個異步過程,即父進程永遠沒法預測子進程 到底何時結束。 當一個 進程完成它的工做終止以後,它的父進程須要調用wait()或者waitpid()系統調用取得子進程的終止狀態。html
孤兒進程linux
一個父進程退出,而它的一個或多個子進程還在運行,那麼那些子進程將成爲孤兒進程。孤兒進程將被init進程(進程號爲1)所收養,並由init進程對它們完成狀態收集工做。nginx
殭屍進程編程
一個進程使用fork建立子進程,若是子進程退出,而父進程並無調用wait或waitpid獲取子進程的狀態信息,那麼子進程的進程描述符仍然保存在系統中。這種進程稱之爲僵死進程。php7
問題及危害數據結構
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將會以父進程的身份對殭屍狀態的子進程進行處理。php-fpm
殭屍進程危害場景
例若有個進程,它按期的產 生一個子進程,這個子進程須要作的事情不多,作完它該作的事情以後就退出了,所以這個子進程的生命週期很短,可是,父進程只管生成新的子進程,至於子進程 退出以後的事情,則一律漠不關心,這樣,系統運行上一段時間以後,系統中就會存在不少的僵死進程,假若用ps命令查看的話,就會看到不少狀態爲Z的進程。 嚴格地來講,僵死進程並非問題的根源,罪魁禍首是產生出大量僵死進程的那個父進程。所以,當咱們尋求如何消滅系統中大量的僵死進程時,答案就是把產生大 量僵死進程的那個元兇槍斃掉(也就是經過kill發送SIGTERM或者SIGKILL信號啦)。槍斃了元兇進程以後,它產生的僵死進程就變成了孤兒進 程,這些孤兒進程會被init進程接管,init進程會wait()這些孤兒進程,釋放它們佔用的系統進程表中的資源,這樣,這些已經僵死的孤兒進程 就能瞑目而去了。
孤兒進程和殭屍進程測試
一、孤兒進程被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
二、殭屍進程和危害
執行如下代碼 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()如何正確使用啊?這樣用,看起來畢竟不太科學。
那麼,是時候引入信號學了!