基本概念
我們知道在unix/linux中,正常情況下,子進程是通過父進程創(chuàng)建的,子進程在創(chuàng)建新的進程。子進程的結(jié)束和父進程的運行是一個異步過程,即父進程永遠無法預測子進程 到底什么時候結(jié)束。 當一個 進程完成它的工作終止之后,它的父進程需要調(diào)用wait()或者waitpid()系統(tǒng)調(diào)用取得子進程的終止狀態(tài)。
孤兒進程
一個父進程退出,而它的一個或多個子進程還在運行,那么那些子進程將成為孤兒進程。孤兒進程將被init進程(進程號為1)所收養(yǎng),并由init進程對它們完成狀態(tài)收集工作。
僵尸進程
一個進程使用fork創(chuàng)建子進程,如果子進程退出,而父進程并沒有調(diào)用wait或waitpid獲取子進程的狀態(tài)信息,那么子進程的進程描述符仍然保存在系統(tǒng)中。這種進程稱之為僵死進程。
問題及危害
unix提供了一種機制可以保證只要父進程想知道子進程結(jié)束時的狀態(tài)信息, 就可以得到。這種機制就是: 在每個進程退出的時候,內(nèi)核釋放該進程所有的資源,包括打開的文件,占用的內(nèi)存等。 但是仍然為其保留一定的信息(包括進程號the process ID,退出狀態(tài)the termination status of the process,運行時間the amount of CPU time taken by the process等)。直到父進程通過wait / waitpid來取時才釋放。 但這樣就導致了問題,如果進程不調(diào)用wait / waitpid的話, 那么保留的那段信息就不會釋放,其進程號就會一直被占用,但是系統(tǒng)所能使用的進程號是有限的,如果大量的產(chǎn)生僵死進程,將因為沒有可用的進程號而導致系統(tǒng)不能產(chǎn)生新的進程. 此即為僵尸進程的危害,應當避免。
孤兒進程是沒有父進程的進程,孤兒進程這個重任就落到了init進程身上,init進程就好像是一個民政局,專門負責處理孤兒進程的善后工作。每當出現(xiàn)一個孤兒進程的時候,內(nèi)核就把孤 兒進程的父進程設置為init,而init進程會循環(huán)地wait()它的已經(jīng)退出的子進程。這樣,當一個孤兒進程凄涼地結(jié)束了其生命周期的時候,init進程就會代表黨和政府出面處理它的一切善后工作。因此孤兒進程并不會有什么危害。
任何一個子進程(init除外)在exit()之后,并非馬上就消失掉,而是留下一個稱為僵尸進程(Zombie)的數(shù)據(jù)結(jié)構(gòu),等待父進程處理。這是每個 子進程在結(jié)束時都要經(jīng)過的階段。如果子進程在exit()之后,父進程沒有來得及處理,這時用ps命令就能看到子進程的狀態(tài)是“Z”。如果父進程能及時 處理,可能用ps命令就來不及看到子進程的僵尸狀態(tài),但這并不等于子進程不經(jīng)過僵尸狀態(tài)。 如果父進程在子進程結(jié)束之前退出,則子進程將由init接管。init將會以父進程的身份對僵尸狀態(tài)的子進程進行處理。
僵尸進程危害場景
例如有個進程,它定期的產(chǎn) 生一個子進程,這個子進程需要做的事情很少,做完它該做的事情之后就退出了,因此這個子進程的生命周期很短,但是,父進程只管生成新的子進程,至于子進程 退出之后的事情,則一概不聞不問,這樣,系統(tǒng)運行上一段時間之后,系統(tǒng)中就會存在很多的僵死進程,倘若用ps命令查看的話,就會看到很多狀態(tài)為Z的進程。 嚴格地來說,僵死進程并不是問題的根源,罪魁禍首是產(chǎn)生出大量僵死進程的那個父進程。因此,當我們尋求如何消滅系統(tǒng)中大量的僵死進程時,答案就是把產(chǎn)生大 量僵死進程的那個元兇槍斃掉(也就是通過kill發(fā)送SIGTERM或者SIGKILL信號啦)。槍斃了元兇進程之后,它產(chǎn)生的僵死進程就變成了孤兒進 程,這些孤兒進程會被init進程接管,init進程會wait()這些孤兒進程,釋放它們占用的系統(tǒng)進程表中的資源,這樣,這些已經(jīng)僵死的孤兒進程 就能瞑目而去了。
孤兒進程和僵尸進程測試
1、孤兒進程被init進程收養(yǎng)
$pid = pcntl_fork();
if ($pid > 0) {
// 顯示父進程的進程ID,這個函數(shù)可以是getmypid(),也可以用posix_getpid()
echo "Father PID:" . getmypid() . PHP_EOL;
// 讓父進程停止兩秒鐘,在這兩秒內(nèi),子進程的父進程ID還是這個父進程
sleep(2);
} else if (0 == $pid) {
// 讓子進程循環(huán)10次,每次睡眠1s,然后每秒鐘獲取一次子進程的父進程進程ID
for ($i = 1; $i = 10; $i++) {
sleep(1);
// posix_getppid()函數(shù)的作用就是獲取當前進程的父進程進程ID
echo posix_getppid() . PHP_EOL;
}
} else {
echo "fork error." . PHP_EOL;
}
測試結(jié)果:
php daemo001.php
Father PID:18046
18046
18046
www@iZ2zec3dge6rwz2uw4tveuZ:~/test$ 1
1
1
1
1
1
1
1
2、僵尸進程和危害
執(zhí)行以下代碼 php zombie1.php
$pid = pcntl_fork();
if( $pid > 0 ){
// 下面這個函數(shù)可以更改php進程的名稱
cli_set_process_title('php father process');
// 讓主進程休息60秒鐘
sleep(60);
} else if( 0 == $pid ) {
cli_set_process_title('php child process');
// 讓子進程休息10秒鐘,但是進程結(jié)束后,父進程不對子進程做任何處理工作,這樣這個子進程就會變成僵尸進程
sleep(10);
} else {
exit('fork error.'.PHP_EOL);
}
執(zhí)行結(jié)果,另外一個終端窗口
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>
通過執(zhí)行 ps -aux 命令可以看到,當程序在前十秒內(nèi)運行的時候,php child process 的狀態(tài)列為 [S+],然而在十秒鐘過后,這個狀態(tài)變成了 [Z+],也就是變成了危害系統(tǒng)的僵尸進程。
那么,問題來了?如何避免僵尸進程呢?
PHP通過 pcntl_wait() 和 pcntl_waitpid() 兩個函數(shù)來幫我們解決這個問題。了解Linux系統(tǒng)編程的應該知道,看名字就知道這其實就是PHP把C語言中的 wait() 和 waitpid() 包裝了一下。
通過代碼演示 pcntl_wait() 來避免僵尸進程。
pcntl_wait() 函數(shù):
這個函數(shù)的作用就是 “ 等待或者返回子進程的狀態(tài) ”,當父進程執(zhí)行了該函數(shù)后,就會阻塞掛起等待子進程的狀態(tài)一直等到子進程已經(jīng)由于某種原因退出或者終止。
換句話說就是如果子進程還沒結(jié)束,那么父進程就會一直等等等,如果子進程已經(jīng)結(jié)束,那么父進程就會立刻得到子進程狀態(tài)。這個函數(shù)返回退出的子進程的進程 ID 或者失敗返回 -1。
執(zhí)行以下代碼 zombie2.php
$pid = pcntl_fork();
if ($pid > 0) {
// 下面這個函數(shù)可以更改php進程的名稱
cli_set_process_title('php father process');
// 返回$wait_result,就是子進程的進程號,如果子進程已經(jīng)是僵尸進程則為0
// 子進程狀態(tài)則保存在了$status參數(shù)中,可以通過pcntl_wexitstatus()等一系列函數(shù)來查看$status的狀態(tài)信息是什么
$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秒鐘,但是進程結(jié)束后,父進程不對子進程做任何處理工作,這樣這個子進程就會變成僵尸進程
sleep(10);
} else {
exit('fork error.' . PHP_EOL);
}
在另外一個終端中通過ps -aux查看,可以看到在前十秒內(nèi),php child process 是 [S+] 狀態(tài),然后十秒鐘過后進程消失了,也就是被父進程回收了,沒有變成僵尸進程。
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() 有個很大的問題,就是阻塞。父進程只能掛起等待子進程結(jié)束或終止,在此期間父進程什么都不能做,這并不符合多快好省原則,所以 pcntl_waitpid() 閃亮登場。pcntl_waitpid( pid, status, $option = 0 )的第三個參數(shù)如果設置為WNOHANG,那么父進程不會阻塞一直等待到有子進程退出或終止,否則將會和pcntl_wait()的表現(xiàn)類似。
修改第三個案例的代碼,但是,我們并不添加WNOHANG,演示說明pcntl_waitpid()功能:
$pid = pcntl_fork();
if ($pid > 0) {
// 下面這個函數(shù)可以更改php進程的名稱
cli_set_process_title('php father process');
// 返回值保存在$wait_result中
// $pid參數(shù)表示 子進程的進程ID
// 子進程狀態(tài)則保存在了參數(shù)$status中
// 將第三個option參數(shù)設置為常量WNOHANG,則可以避免主進程阻塞掛起,此處父進程將立即返回繼續(xù)往下執(zhí)行剩下的代碼
$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秒鐘,但是進程結(jié)束后,父進程不對子進程做任何處理工作,這樣這個子進程就會變成僵尸進程
sleep(10);
} else {
exit('fork error.' . PHP_EOL);
}
下面是運行結(jié)果,一個執(zhí)行php zombie3.php 程序的終端窗口
www@iZ2zec3dge6rwz2uw4tveuZ:~/test$ php zombie3.php
int(18586)
int(0)
^C
ctrl-c 發(fā)送 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:~$
實際上可以看到主進程是被阻塞的,一直到第十秒子進程退出了,父進程不再阻塞
修改第四段代碼,添加第三個參數(shù)WNOHANG,代碼如下:
$pid = pcntl_fork();
if ($pid > 0) {
// 下面這個函數(shù)可以更改php進程的名稱
cli_set_process_title('php father process');
// 返回值保存在$wait_result中
// $pid參數(shù)表示 子進程的進程ID
// 子進程狀態(tài)則保存在了參數(shù)$status中
// 將第三個option參數(shù)設置為常量WNOHANG,則可以避免主進程阻塞掛起,此處父進程將立即返回繼續(xù)往下執(zhí)行剩下的代碼
$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秒鐘,但是進程結(jié)束后,父進程不對子進程做任何處理工作,這樣這個子進程就會變成僵尸進程
sleep(10);
} else {
exit('fork error.' . PHP_EOL);
}
執(zhí)行 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>
實際上可以看到主進程是被阻塞的,一直到第十秒子進程退出了,父進程不再阻塞。
問題出現(xiàn)了,竟然php child process進程狀態(tài)竟然變成了[Z+],這是怎么搞得?回頭分析一下代碼:
我們看到子進程是睡眠了十秒鐘,而父進程在執(zhí)行pcntl_waitpid()之前沒有任何睡眠且本身不再阻塞,所以,主進程自己先執(zhí)行下去了,而子進程在足足十秒鐘后才結(jié)束,進程狀態(tài)自然無法得到回收。
如果我們將代碼修改一下,就是在主進程的pcntl_waitpid()前睡眠15秒鐘,這樣就可以回收子進程了。但是即便這樣修改,細心想的話還是會有個問題,那就是在子進程結(jié)束后,在父進程執(zhí)行pcntl_waitpid()回收前,有五秒鐘的時間差,在這個時間差內(nèi),php child process也將會是僵尸進程。那么,pcntl_waitpid()如何正確使用???這樣用,看起來畢竟不太科學。
那么,是時候引入信號學了!
您可能感興趣的文章:- PHP多進程編程之僵尸進程問題的理解
- php 的多進程操作實踐案例分析
- php 多進程編程父進程的阻塞與非阻塞實例分析
- php實現(xiàn)的簡單多進程服務器類完整示例
- 深入解析PHP中的(偽)多線程與多進程
- 深入探究PHP的多進程編程方法
- PHP使用pcntl_fork實現(xiàn)多進程下載圖片的方法
- PHP 多進程 解決難題
- 解析PHP實現(xiàn)多進程并行執(zhí)行腳本
- 以實例全面講解PHP中多進程編程的相關(guān)函數(shù)的使用
- PHP基于文件鎖解決多進程同時讀寫一個文件問題示例
- php多進程并發(fā)編程防止出現(xiàn)僵尸進程的方法分析