1. 程式人生 > >什麼是PHP7中的孤兒程序與殭屍程序

什麼是PHP7中的孤兒程序與殭屍程序

什麼是PHP7中的孤兒程序與殭屍程序

基本概念

我們知道在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()如何正確使用啊?這樣用,看起來畢竟不太科學。

那麼,是時候引入訊號學了!

您可能感興趣的文章:

文章同步釋出: https://www.geek-share.com/detai