本篇文章給大家分享的內容是PHP實現系統編程之 多進程編程介紹及孤兒進程、殭屍進程 ,有著一定的參考價值,有需要的朋友可以參考一下
多進程編程也是系統編程的一個重要方面,但PHP程式員通常不需要關心多進程的問題,因為web伺服器或者PHP-FPM已經幫我們管理好進程方面的問題了,但是如果我們想要用PHP來開發CLI程式,多進程編程是不可或缺的基本技術。
PHP中關於進程式控制制的方法主要使用到PCNTL(Process Control)擴充, 所以,在進行多進程編程之前,首先要確保你的PHP已經安裝了最新的PCNTL擴充,可以輸入php -m命令來查看當前已經安裝的擴充:
該擴充給我們提供了一組用於進程操作的方法:
PCNTL 函數pcntl_alarm — 為進程設定一個alarm鬧鐘訊號pcntl_errno — 別名 pcntl_get_last_errorpcntl_exec — 在當前進程空間執行指定程式pcntl_fork — 在當前進程當前位置產生分支(子進程)。pcntl_get_last_error — Retrieve the error number set by the last pcntl function which failedpcntl_getpriority — 擷取任意進程的優先順序pcntl_setpriority — 修改任意進程的優先順序pcntl_signal_dispatch — 調用等待訊號的處理器pcntl_signal_get_handler — Get the current handler for specified signalpcntl_signal — 安裝一個訊號處理器pcntl_sigprocmask — 設定或檢索阻塞訊號pcntl_sigtimedwait — 帶逾時機制的訊號等待pcntl_sigwaitinfo — 等待訊號pcntl_strerror — Retrieve the system error message associated with the given errnopcntl_wait — 等待或返回fork的子進程狀態pcntl_waitpid — 等待或返回fork的子進程狀態pcntl_wexitstatus — 返回一個中斷的子進程的傳回碼pcntl_wifexited — 檢查狀態碼是否代表一個正常的退出。pcntl_wifsignaled — 檢查子進程狀態代碼是否代表由於某個訊號而中斷pcntl_wifstopped — 檢查子進程當前是否已經停止pcntl_wstopsig — 返回導致子進程停止的訊號pcntl_wtermsig — 返回導致子進程中斷的訊號
pcntl_fork — 在當前進程當前位置產生分支(子進程)。譯註:fork是建立了一個子進程,父進程和子進程 都從fork的位置開始向下繼續執行,不同的是父進程執行過程中,得到的fork傳回值為子進程號,而子進程得到的是0。
fork出的子進程幾近於完全的複製了父進程,父子進程共用程式碼片段,雖然父子進程的資料區段、堆、棧是相互獨立的,但在一開始,子進程完全複製了父進程的這些資料,但之後的修改互不影響。
int pcntl_fork ( void )
建立5個子進程代碼示範:
<?phpfor($i = 0; $i < 5; $i++){ $pid = pcntl_fork(); //建立子進程,子進程也是從這裡開始執行。 if ($pid == 0) { break; //由於子進程也會執行迴圈的代碼,所以讓子進程退出迴圈,否則子進程又會建立自己的子進程。 }}sleep($i); //第一個建立的子進程將睡眠0秒,第二個將睡眠1s,依次類推...主進程會睡眠5秒if ($i < 5){ exit("第 " . ($i+1) . " 個子進程退出..." . time() . PHP_EOL);}else{ exit("父進程退出..." . time() . PHP_EOL);}
運行結果:
[root@localhost process]# php process.php 第 1 個子進程退出...1503322773第 2 個子進程退出...1503322774第 3 個子進程退出...1503322775第 4 個子進程退出...1503322776第 5 個子進程退出...1503322777父進程退出...1503322778
對於pcntl_fork函數要重點理解:“fork是建立了一個子進程,父進程和子進程 都從fork的位置開始向下繼續執行,不同的是父進程執行過程中,得到的fork傳回值為子進程號,而子進程得到的是0”
把上面的代碼稍作修改,不讓進程退出,然後利用ps命令查看系統狀態:
<?phpfor($i = 0; $i < 5; $i++){ $pid = pcntl_fork(); if ($pid == 0) { break; //由於子進程也會執行迴圈的代碼,所以讓子進程退出迴圈 }}sleep($i); //第一個建立的子進程將睡眠0秒,第二個將睡眠1s,依次類推...主進程會睡眠5秒/*if ($i < 5){ exit("第 " . ($i+1) . " 個子進程退出..." . time() . PHP_EOL);}else{ exit("父進程退出..." . time() . PHP_EOL);}*/while(1){ sleep(1); //執行死迴圈不退出}
運行後輸入 ps -ef | grep php 查看系統進程
[root@localhost ~]# ps -ef | grep phproot 3670 3609 0 21:54 pts/0 00:00:00 php process.phproot 3671 3670 0 21:54 pts/0 00:00:00 php process.phproot 3672 3670 0 21:54 pts/0 00:00:00 php process.phproot 3673 3670 0 21:54 pts/0 00:00:00 php process.phproot 3674 3670 0 21:54 pts/0 00:00:00 php process.phproot 3675 3670 0 21:54 pts/0 00:00:00 php process.phproot 3677 3646 0 21:54 pts/1 00:00:00 grep php
可以看到6個 php process.php 進程,其中第二列是進程號,第三列是進程的父進程號,可以看到後面五個進程的父進程號都是第一個進程的進程號。
上面的代碼子進程和父進程都是執行相同的代碼,有沒有辦法讓子進程和父進程做不同的事呢,最簡單的辦法就是if判斷,子進程執行子進程的代碼,父進程執行父進程的代碼:
<?php$ppid = posix_getpid(); //記錄父進程的進程號for($i = 0; $i < 5; $i++){ $pid = pcntl_fork(); if ($pid == 0) { break; //由於子進程也會執行迴圈的代碼,所以讓子進程退出迴圈 }}if ($ppid == posix_getpid()){ //父進程 while(1) { sleep(1); }}else{ //子進程 for($i = 0; $i < 100; $i ++) { echo "子進程" . posix_getpid() . " 迴圈 $i ...\n"; sleep(1); }}
[root@localhost process]# php process.php 子進程6677 迴圈 0 ...子進程6676 迴圈 0 ...子進程6678 迴圈 0 ...子進程6680 迴圈 0 ...子進程6679 迴圈 0 ...子進程6677 迴圈 1 ...子進程6676 迴圈 1 ...子進程6678 迴圈 1 ...子進程6680 迴圈 1 ...子進程6679 迴圈 1 ...子進程6677 迴圈 2 ...子進程6676 迴圈 2 ...子進程6678 迴圈 2 ...子進程6680 迴圈 2 ...
其實上面的程式父子進程還是執行了相同的代碼,只是進入的if分支不一樣,而pcntl_exec則可以讓子進程完全脫離父進程的影響,去執行新的程式。
pcntl_exec — 在當前進程空間執行指定程式
void pcntl_exec ( string $path [, array $args [, array $envs ]] )
path
path必須時可執行二進位檔案路徑或一個在檔案第一行指定了 一個可執行檔路徑標題的指令碼(比如檔案第一行是#!/usr/local/bin/perl的perl指令碼)。 更多的資訊請查看您系統的execve(2)手冊。
args
args是一個要傳遞給程式的參數的字串數組。
envs
envs是一個要傳遞給程式作為環境變數的字串數組。這個數組是 key => value格式的,key代表要傳遞的環境變數的名稱,value代表該環境變數值。
注意該方法的傳回值比較特殊:當發生錯誤時返回 FALSE ,沒有錯誤時沒有返回,因為pcntl_exec調用成功,子進程就去運行新的程式 從父進程繼承的程式碼片段、資料區段、堆、棧等資訊全部被替換成新的,此時的pcntl_exec函數調用棧已經不存在了,所以也就沒有返回了。程式碼範例:
<?phpfor($i = 0; $i < 3; $i++){ $pid = pcntl_fork(); if($pid == 0) { echo "子進程pid = " . posix_getpid() . PHP_EOL; $ret = pcntl_exec('/bin/ls'); //執行 ls 命令, 此處調用成功子進程將不會再回來執行下面的任何代碼 var_dump($ret); // 此處的代碼不會再執行 }}sleep(5); //睡眠5秒以確保子進程執行完畢,原因後面會說exit( "主進程退出...\n");
運行結果:
[root@localhost process]# php pcntl_exec.php 子進程pid = 6728子進程pid = 6729子進程pid = 6727pcntl_exec.phpprocess.phppcntl_exec.phpprocess.phppcntl_exec.phpprocess.php主進程退出...[root@localhost process]# lspcntl_exec.php process.php
以上就是對PHP多進程開發的簡單介紹,對於子進程不同的存續狀態,引出孤兒進程和殭屍進程的概念,在linux系統中,init進程(1號進程)是所有進程的祖先,其他進程要麼是該進程的子進程,要麼是子進程的子進程,子進程的子進程的子進程...,linux系統中可以用 pstree 命令查看進程樹結構:
在多進程程式中,如果父進程先於子進程退出,那麼子進程將會被init進程收養,成為init進程的子進程,這種進程被稱為孤兒進程,我們可以把上面的代碼稍作修改來示範這種情況:
<?php$ppid = posix_getpid(); //記錄父進程的進程號for($i = 0; $i < 5; $i++){ $pid = pcntl_fork(); if ($pid == 0) { break; //由於子進程也會執行迴圈的代碼,所以讓子進程退出迴圈 }}if ($ppid == posix_getpid()){ //父進程直接退出,它的子進程都會成為孤兒進程 exit(0);}else{ //子進程 for($i = 0; $i < 100; $i ++) { echo "子進程" . posix_getpid() . " 迴圈 $i ...\n"; sleep(1); }}
運行該程式,然後查看進程狀態:
[root@localhost ~]# ps -ef | grep phproot 2903 1 0 12:09 pts/0 00:00:00 php pcntl.fork.phproot 2904 1 0 12:09 pts/0 00:00:00 php pcntl.fork.phproot 2905 1 0 12:09 pts/0 00:00:00 php pcntl.fork.phproot 2906 1 0 12:09 pts/0 00:00:00 php pcntl.fork.phproot 2907 1 0 12:09 pts/0 00:00:00 php pcntl.fork.phproot 2935 2912 0 12:10 pts/1 00:00:00 grep php
可以看到五個子進程的父進程號都是1了,並且這時控制台不再被程式佔用,子進程轉到了後台運行,這種孤兒進程被init進程收養的機制是實現後面將要介紹的守護進程的必要條件之一。
子進程還有一種狀態叫殭屍進程,子進程結束時並不是完全退出,核心進程表中仍舊保有該進程的記錄,這樣做的目的是能夠讓父進程可以得知子進程的退出狀態,以及子進程是自殺(調用exit或代碼執行完畢)還是他殺(被訊號終止),父進程可以調用pcntl_wait 或 pcntl_waitpid 方法來回收子進程(收屍),釋放子進程佔用的所有資源,並獲得子進程的退出狀態,如果父進程不做回收,則殭屍進程一直存在,如果這時父進程也退出了,則這些殭屍進程會被init進程接管並自動回收。
對於linux系統來說,一個長時間啟動並執行多進程程式一定要回收子進程,因為系統的進程資源是有限的,殭屍進程會讓系統的可用資源減少。
代碼示範殭屍進程的產生:
<?php$ppid = posix_getpid(); //記錄父進程的進程號for($i = 0; $i < 5; $i++){ $pid = pcntl_fork(); if ($pid == 0) { break; //由於子進程也會執行迴圈的代碼,所以讓子進程退出迴圈 }}if ($ppid == posix_getpid()){ //父進程不退出,也不回收子進程 while(1) { sleep(1); }}else{ //子進程退出,會成為殭屍進程 exit("子進程退出 $ppid ...\n");}
運行之後查看進程狀態:
[root@localhost ~]# ps -ef | grep phproot 2971 2864 0 14:13 pts/0 00:00:00 php pcntl.fork.phproot 2972 2971 0 14:13 pts/0 00:00:00 [php] <defunct>root 2973 2971 0 14:13 pts/0 00:00:00 [php] <defunct>root 2974 2971 0 14:13 pts/0 00:00:00 [php] <defunct>root 2975 2971 0 14:13 pts/0 00:00:00 [php] <defunct>root 2976 2971 0 14:13 pts/0 00:00:00 [php] <defunct>root 2978 2912 0 14:13 pts/1 00:00:00 grep php
殭屍進程會用 <defunct>(死者,死人) 來標識,除非我們結束父進程,否則這些殭屍進程會一直存在,也無法用kill命令來殺死。
PHP的pcntl擴充提供了兩個回收子進程的方法供我們調用:
int pcntl_wait ( int &$status [, int $options = 0 ] )int pcntl_waitpid ( int $pid , int &$status [, int $options = 0 ] )
pcntl_wait函數掛起當前進程的執行直到一個子進程退出或接收到一個訊號要求中斷當前進程或調用一個訊號處理函數。 如果一個子進程在調用此函數時已經退出(俗稱殭屍進程),此函數立刻返回。子進程使用的所有系統資源將被釋放。
關於wait在您系統上工作的詳細規範請查看您系統的wait(2)手冊。這個函數等同於以-1作為參數pid 的值並且沒有options參數來調用pcntl_waitpid() 函數。
程式碼範例:
<?php$ppid = posix_getpid(); //記錄父進程的進程號for($i = 0; $i < 5; $i++){ $pid = pcntl_fork(); if ($pid == 0) { break; //由於子進程也會執行迴圈的代碼,所以讓子進程退出迴圈 }}if ($ppid == posix_getpid()){ //父進程迴圈回收收子進程 while(($id = pcntl_wait($status)) > 0) //如果沒有子進程退出, pcntl_wait 會一直阻塞 { echo "回收子進程:$id, 子進程退出狀態值: $status...\n"; } exit("父進程退出 $id....\n"); //當子進程全部結束 pcntl_wait 返回-1}else{ //子進程退出,會成為殭屍進程 sleep($i); exit($i); }
運行結果:
[root@localhost php]# php pcntl.fork.php 回收子進程:3043, 子進程退出狀態值: 0...回收子進程:3044, 子進程退出狀態值: 256...回收子進程:3045, 子進程退出狀態值: 512...回收子進程:3046, 子進程退出狀態值: 768...回收子進程:3047, 子進程退出狀態值: 1024...父進程退出 -1....
這裡只是對PHP多進程編程做了基本的介紹,後面會結合 訊號、處理序間通訊以及守護進程 做更進一步的介紹,歡迎大家關注後續文章。
PHP是世界上最好的語言 That's all :)
相關推薦:
PHP實現系統編程之網路Socket及IO多工