PHP7之孤兒進程與殭屍進程

2020-07-16 10:06:10
基本概念

我們知道在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
[email protected]:~/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);
}

執行結果,另外一個終端視窗

[email protected]:~$ 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
[email protected]:~$ 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+] 狀態,然後十秒鐘過後進程消失了,也就是被父進程回收了,沒有變成殭屍進程。

[email protected]:~/test$ ps -aux|grep -v "grep|nginx|php-fpm" | grep php
[email protected]:~/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
[email protected]:~/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 程式的終端視窗

[email protected]:~/test$ php zombie3.php
int(18586)
int(0)
^C  

ctrl-c 傳送 SIGINT 信號給前台行程群組中的所有進程。常用於終止正在執行的程式。

下面是ps -aux終端視窗

[email protected]:~$ 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
[email protected]:~$ 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
[email protected]:~$ 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
[email protected]:~$ ps -aux|grep -v "grep|nginx|php-fpm" | grep php  // ctrl-c 後不再被阻塞
[email protected]:~$

實際上可以看到主進程是被阻塞的,一直到第十秒子進程退出了,父進程不再阻塞  

修改第四段程式碼,新增第三個引數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

[email protected]:~/test$ php zombie4.php
int(0)
int(0)
不阻塞,執行到這裡  

另一個ps -aux終端視窗

[email protected]:~$ 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
[email protected]:~$ 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之孤兒進程與殭屍進程的詳細內容,更多請關注TW511.COM其它相關文章!