PHP有一組進程式控制制函數(編譯時間需要–enable-pcntl與posix擴充),使得php能實現跟c一樣的建立子進程、使用exec函數執行程式、處理訊號等功能。
<?php header('content-type:text/html;charset=utf-8' ); // 必須載入擴充 if (!function_exists("pcntl_fork")) { die("pcntl extention is must !"); } //總進程的數量 $totals = 3; // 執行的指令碼數量 $cmdArr = array(); // 執行的指令碼數量的數組 for ($i = 0; $i < $totals; $i++) { $cmdArr[] = array("path" => __DIR__ . "/run.php", 'pid' =>$i ,'total' =>$totals); } /* 展開:$cmdArr Array ( [0] => Array ( [path] => /var/www/html/company/pcntl/run.php [pid] => 0 [total] => 3 ) [1] => Array ( [path] => /var/www/html/company/pcntl/run.php [pid] => 1 [total] => 3 ) [2] => Array ( [path] => /var/www/html/company/pcntl/run.php [pid] => 2 [total] => 3 ) ) */ pcntl_signal(SIGCHLD, SIG_IGN); //如果父進程不關心子進程什麼時候結束,子進程結束後,核心會回收。 foreach ($cmdArr as $cmd) { $pid = pcntl_fork(); //建立子進程 //父進程和子進程都會執行下面代碼 if ($pid == -1) { //錯誤處理:建立子進程失敗時返回-1. die('could not fork'); } else if ($pid) { //父進程會得到子進程號,所以這裡是父進程執行的邏輯 //如果不需要阻塞進程,而又想得到子進程的退出狀態,則可以注釋掉pcntl_wait($status)語句,或寫成: pcntl_wait($status,WNOHANG); //等待子進程中斷,防止子進程成為殭屍進程。 } else { //子進程得到的$pid為0, 所以這裡是子進程執行的邏輯。 $path = $cmd["path"]; $pid = $cmd['pid'] ; $total = $cmd['total'] ; echo exec("/usr/bin/php {$path} {$pid} {$total}")."\n"; exit(0) ; } } ?>
使用PHP真正的多進程運行模式,適用於資料擷取、郵件群發、資料來源更新、tcp伺服器等環節。
PHP有一組進程式控制制函數(編譯時間需要 –enable-pcntl與posix擴充),使得php能在*nix系統中實現跟c一樣的建立子進程、使用exec函數執行程式、處理訊號等功能。 PCNTL使用ticks來作為訊號處理機制(signal handle callback mechanism),可以最小程度地降低處理非同步事件時的負載。何謂ticks?Tick 是一個在程式碼片段中解譯器每執行 N 條低級語句就會發生的事件,這個程式碼片段需要通過declare來指定。
常用的PCNTL函數
1. pcntl_alarm ( int $seconds )
設定一個$seconds秒後發送SIGALRM訊號的計數器
2. pcntl_signal ( int $signo , callback $handler [, bool $restart_syscalls ] )
為$signo設定一個處理該訊號的回呼函數。下面是一個隔5秒發送一個SIGALRM訊號,並由signal_handler函數擷取,然後列印一個“Caught SIGALRM”的例子:
<?phpdeclare(ticks = 1); function signal_handler($signal) { print "Caught SIGALRM\n"; pcntl_alarm(5);} pcntl_signal(SIGALRM, "signal_handler", true);pcntl_alarm(5); for(;;) {} ?>
3. pcntl_exec ( string $path [, array $args [, array $envs ]] )
在當前的進程空間中執行指定程式,類似於c中的exec族函數。所謂當前空間,即載入指定程式的代碼覆蓋掉當前進程的空間,執行完該程式進程即結束。
<?php$dir = '/home/shankka/';$cmd = 'ls';$option = '-l';$pathtobin = '/bin/ls'; $arg = array($cmd, $option, $dir); pcntl_exec($pathtobin, $arg);echo '123'; //不會執行到該行?>
4. pcntl_fork ( void )
為當前進程建立一個子進程,並且先運行父進程,返回的是子進程的PID,肯定大於零。在父進程的代碼中可以用 pcntl_wait(&$status)暫停父進程知道他的子進程有傳回值。注意:父進程的阻塞同時會阻塞子進程。但是父進程的結束不影響子進程的運行。
父進程運行完了會接著運行子進程,這時子進程會從執行pcntl_fork()的那條語句開始執行(包括此函數),但是此時它返回的是零(代表這是一個子進程)。在子進程的代碼塊中最好有exit語句,即執行完子進程後立即就結束。否則它會又重頭開始執行這個指令碼的某些部分。
注意兩點:
- 子進程最好有一個exit;語句,防止不必要的出錯;
- pcntl_fork間最好不要有其它語句,例如:
<?php$pid = pcntl_fork();//這裡最好不要有其他的語句if ($pid == -1) { die('could not fork');} else if ($pid) { // we are the parentpcntl_wait($status); //Protect against Zombie children} else { // we are the child}?>
5. pcntl_wait ( int &$status [, int $options ] )
阻塞當前進程,只到當前進程的一個子進程退出或者收到一個結束當前進程的訊號。使用$status返回子進程的狀態代碼,並可以指定第二個參數來說明是否以阻塞狀態調用:
阻塞方式調用的,函數傳回值為子進程的pid,如果沒有子進程傳回值為-1;
非阻塞方式調用,函數還可以在有子進程在運行但沒有結束的子進程時返回0。
6. pcntl_waitpid ( int $pid , int &$status [, int $options ] )
功能同pcntl_wait,區別為waitpid為等待指定pid的子進程。當pid為-1時pcntl_waitpid與pcntl_wait 一樣。在pcntl_wait和pcntl_waitpid兩個函數中的$status中存了子進程的狀態資訊,這個參數可以用於 pcntl_wifexited、pcntl_wifstopped、pcntl_wifsignaled、pcntl_wexitstatus、 pcntl_wtermsig、pcntl_wstopsig、pcntl_waitpid這些函數。
例如:
<?php$pid = pcntl_fork();if($pid) { pcntl_wait($status); $id = getmypid(); echo "parent process,pid {$id}, child pid {$pid}\n";}else{ $id = getmypid(); echo "child process,pid {$id}\n"; sleep(2);}?>
子進程在輸出child process等字樣之後sleep了2秒才結束,而父進程阻塞著直到子進程退出之後才繼續運行。
7. pcntl_getpriority ([ int $pid [, int $process_identifier ]] )
取得進程的優先順序,即nice值,預設為0,在我的測試環境的linux中(CentOS release 5.2 (Final)),優先順序為-20到19,-20為優先順序最高,19為最低。(手冊中為-20到20)。
8. pcntl_setpriority ( int $priority [, int $pid [, int $process_identifier ]] )
設定進程的優先順序。
9. posix_kill
可以給進程發送訊號
10. pcntl_singal
用來設定訊號的回呼函數
當父進程退出時,子進程如何得知父進程的退出
當父進程退出時,子進程一般可以通過下面這兩個比較簡單的方法得知父進程已經退出這個訊息:
當父進程退出時,會有一個INIT進程來領養這個子進程。這個INIT進程的進程號為1,所以子進程可以通過使用getppid()來取得當前父進程的pid。如果返回的是1,表明父進程已經變為INIT進程,則原進程已經推出。
使用kill函數,向原有的父進程發送空訊號(kill(pid, 0))。使用這個方法對某個進程的存在性進行檢查,而不會真的發送訊號。所以,如果這個函數返回-1表示父進程已經退出。
除了上面的這兩個方法外,還有一些實現上比較複雜的方法,比如建立管道或socket來進行時時的監控等等。
PHP多進程採集資料的例子
<?php/*** Project: Signfork: php多線程庫* File: Signfork.class.php*/ class Signfork{ /** * 設定子進程通訊檔案所在目錄 * @var string */ private $tmp_path='/tmp/'; /** * Signfork引擎主啟動方法 * 1、判斷$arg類型,類型為數組時將值傳遞給每個子進程;類型為數值型時,代表要建立的進程數. * @param object $obj 執行對象 * @param string|array $arg 用於對象中的__fork方法所執行的參數 * 如:$arg,自動分解為:$obj->__fork($arg[0])、$obj->__fork($arg[1])... * @return array 返回 array(子進程式列=>子進程執行結果); */ public function run($obj,$arg=1){ if(!method_exists($obj,'__fork')){ exit("Method '__fork' not found!"); } if(is_array($arg)){ $i=0; foreach($arg as $key=>$val){ $spawns[$i]=$key; $i++; $this->spawn($obj,$key,$val); } $spawns['total']=$i; }elseif($spawns=intval($arg)){ for($i = 0; $i < $spawns; $i++){ $this->spawn($obj,$i); } }else{ exit('Bad argument!'); } if($i>1000) exit('Too many spawns!'); return $this->request($spawns); } /** * Signfork主進程式控制制方法 * 1、$tmpfile 判斷子進程檔案是否存在,存在則子進程執行完畢,並讀取內容 * 2、$data收集子進程運行結果及資料,並用於最終返回 * 3、刪除子進程檔案 * 4、輪詢一次0.03秒,直到所有子進程執行完畢,清理子進程資源 * @param string|array $arg 用於對應每個子進程的ID * @return array 返回 array([子進程式列]=>[子進程執行結果]); */ private function request($spawns){ $data=array(); $i=is_array($spawns)?$spawns['total']:$spawns; for($ids = 0; $ids<$i; $ids++){ while(!($cid=pcntl_waitpid(-1, $status, WNOHANG)))usleep(30000); $tmpfile=$this->tmp_path.'sfpid_'.$cid; $data[$spawns['total']?$spawns[$ids]:$ids]=file_get_contents($tmpfile); unlink($tmpfile); } return $data; } /** * Signfork子進程執行方法 * 1、pcntl_fork 產生子進程 * 2、file_put_contents 將'$obj->__fork($val)'的執行結果存入特定序列命名的文本 * 3、posix_kill殺死當前進程 * @param object $obj 待執行的對象 * @param object $i 子進程的序列ID,以便於返回對應每個子進程資料 * @param object $param 用於輸入對象$obj方法'__fork'執行參數 */ private function spawn($obj,$i,$param=null){ if(pcntl_fork()===0){ $cid=getmypid(); file_put_contents($this->tmp_path.'sfpid_'.$cid,$obj->__fork($param)); posix_kill($cid, SIGTERM); exit; } }}?>
php在pcntl_fork()後產生的子進程(通常為殭屍進程)必須由pcntl_waitpid()函數進行資源釋放。但在 pcntl_waitpid()不一定釋放的就是當前啟動並執行進程,也可能是過去產生的殭屍進程(沒有釋放);也可能是並發時其它訪問者的殭屍進程。但可以使用posix_kill($cid, SIGTERM)在子進程結束時殺掉它。
子進程會自動複製父進程空間裡的變數。
PHP多進程編程樣本2
<?php//.....//需要安裝pcntl的php擴充,並載入它if(function_exists("pcntl_fork")){ //產生子進程 $pid = pcntl_fork(); if($pid == -1){ die('could not fork'); }else{ if($pid){ $status = 0; //阻塞父進程,直到子進程結束,不適合需要長時間啟動並執行指令碼,可使用pcntl_wait($status, 0)實現非阻塞式 pcntl_wait($status); // parent proc code exit; }else{ // child proc code //結束當前子進程,以防止產生殭屍進程 if(function_exists("posix_kill")){ posix_kill(getmypid(), SIGTERM); }else{ system('kill -9'. getmypid()); } exit; } }}else{ // 不支援多進程處理時的代碼在這裡}//.....?>如果不需要阻塞進程,而又想得到子進程的退出狀態,則可以注釋掉pcntl_wait($status)語句,或寫成: <?phppcntl_wait($status, 1);//或pcntl_wait($status, WNOHANG);?>
在上面的代碼中,如果父進程退出(使用exit函數退出或redirect),則會導致子進程成為殭屍進程(會交給init進程式控制制),子進程不再執行。
殭屍進程是指的父進程已經退出,而該進程dead之後沒有進程接受,就成為殭屍進程.(zombie)進程。任何進程在退出前(使用exit退出) 都會變成殭屍進程(用於儲存進程的狀態等資訊),然後由init進程接管。如果不及時回收殭屍進程,那麼它在系統中就會佔用一個進程表項,如果這種殭屍進程過多,最後系統就沒有可以用的進程表項,於是也無法再運行其它的程式。
預防殭屍進程有以下幾種方法:
1. 父進程通過wait和waitpid等函數使其等待子進程結束,然後再執行父進程中的代碼,這會導致父進程掛起。上面的代碼就是使用這種方式實現的,但在WEB環境下,它不適合子進程需要長時間啟動並執行情況(會導致逾時)。
使用wait和waitpid方法使父進程自動回收其殭屍子進程(根據子進程的返回狀態),waitpid用於臨控指定子進程,wait是對於所有子進程而言。
2. 如果父進程很忙,那麼可以用signal函數為SIGCHLD安裝handler,因為子進程結束後,父進程會收到該訊號,可以在handler中調用wait回收
3. 如果父進程不關心子進程什麼時候結束,那麼可以用signal(SIGCHLD, SIG_IGN)通知核心,自己對子進程的結束不感興趣,那麼子進程結束後,核心會回收,並不再給父進程發送訊號,例如:
<?phppcntl_signal(SIGCHLD, SIG_IGN);$pid = pcntl_fork();//....code?>
4. 還有一個技巧,就是fork兩次,父進程fork一個子進程,然後繼續工作,子進程再fork一個孫進程後退出,那麼孫進程被init接管,孫進程結束後,init會回收。不過子進程的回收還要自己做。下面是一個例子:
#include "apue.h"#include <sys/wait.h> int main(void){pid_t pid; if ((pid = fork()) < 0){ err_sys("fork error");} else if (pid == 0){ /**//* first child */ if ((pid = fork()) < 0){ err_sys("fork error"); }elseif(pid > 0){ exit(0); /**//* parent from second fork == first child */ } /** * We're the second child; our parent becomes init as soon * as our real parent calls exit() in the statement above. * Here's where we'd continue executing, knowing that when * we're done, init will reap our status. */ sleep(2); printf("second child, parent pid = %d ", getppid()); exit(0);} if (waitpid(pid, NULL, 0) != pid) /**//* wait for first child */ err_sys("waitpid error"); /** * We're the parent (the original process); we continue executing, * knowing that we're not the parent of the second child. */ exit(0);}
在fork()/execve()過程中,假設子進程結束時父進程仍存在,而父進程fork()之前既沒安裝SIGCHLD訊號處理函數調用 waitpid()等待子進程結束,又沒有顯式忽略該訊號,則子進程成為殭屍進程,無法正常結束,此時即使是root身份kill-9也不能殺死殭屍進程。補救辦法是殺死殭屍進程的父進程(殭屍進程的父進程必然存在),殭屍進程成為”孤兒進程”,過繼給1號進程init,init會定期調用wait回收清理這些父進程已退出的殭屍子進程。
所以,上面的樣本可以改成:
<?php//.....//需要安裝pcntl的php擴充,並載入它if(function_exists("pcntl_fork")){ //產生第一個子進程$pid = pcntl_fork(); //$pid即所產生的子進程idif($pid == -1){ //子進程fork失敗 die('could not fork');}else{ if($pid){ //父進程code sleep(5); //等待5秒 exit(0); //或$this->_redirect('/'); }else{ //第一個子進程code //產生孫進程 if(($gpid = pcntl_fork()) < 0){ ////$gpid即所產生的孫進程id //孫進程產生失敗 die('could not fork'); }elseif($gpid > 0){ //第一個子進程code,即孫進程的父進程 $status = 0; $status = pcntl_wait($status); //阻塞子進程,並返回孫進程的退出狀態,用於檢查是否正常退出 if($status ! = 0) file_put_content('filename', '孫進程異常退出'); //得到父進程id //$ppid = posix_getppid(); //如果$ppid為1則表示其父進程已變為init進程,原父進程已退出 //得到子進程id:posix_getpid()或getmypid()或是fork返回的變數$pid //kill掉子進程 //posix_kill(getmypid(), SIGTERM); exit(0); }else{ //即$gpid == 0 //孫進程code //.... //結束孫進程(即當前進程),以防止產生殭屍進程 if(function_exists('posix_kill')){ posix_kill(getmypid(), SIGTERM); }else{ system('kill -9'. getmypid()); } exit(0); } }}}else{ // 不支援多進程處理時的代碼在這裡}//.....?>
怎樣產生殭屍進程的
一個進程在調用exit命令結束自己的生命的時候,其實它並沒有真正的被銷毀,而是留下一個稱為殭屍進程(Zombie)的資料結構(系統調用exit,它的作用是使進程退出,但也僅僅限於將一個正常的進程變成一個殭屍進程,並不能將其完全銷毀)。在Linux進程的狀態中,殭屍進程是非常特殊的一種,它已經放棄了幾乎所有記憶體空間,沒有任何可執行代碼,也不能被調度,僅僅在進程列表中保留一個位置,記載該進程的退出狀態等資訊供其他進程收集,除此之外,殭屍進程不再佔有任何記憶體空間。它需要它的父進程來為它收屍,如果他的父進程沒安裝SIGCHLD訊號處理函數調用wait或waitpid()等待子進程結束,又沒有顯式忽略該訊號,那麼它就一直保持殭屍狀態,如果這時父進程結束了,那麼init進程自動會接手這個子進程,為它收屍,它還是能被清除的。但是如果如果父進程是一個迴圈,不會結束,那麼子進程就會一直保持殭屍狀態,這就是為什麼系統中有時會有很多的殭屍進程。
任何一個子進程(init除外)在exit()之後,並非馬上就消失掉,而是留下一個稱為殭屍進程(Zombie)的資料結構,等待父進程處理。這是每個子進程在結束時都要經過的階段。如果子進程在exit()之後,父進程沒有來得及處理,這時用ps命令就能看到子進程的狀態是”Z”。如果父進程能及時 處理,可能用ps命令就來不及看到子進程的殭屍狀態,但這並不等於子進程不經過殭屍狀態。
如果父進程在子進程結束之前退出,則子進程將由init接管。init將會以父進程的身份對殭屍狀態的子進程進行處理。
另外,還可以寫一個php檔案,然後在以後台形式來運行它,例如:
<?php//Action代碼public function createAction(){ //.... //將args替換成要傳給insertLargeData.php的參數,參數間用空格間隔 system('php -f insertLargeData.php ' . ' args ' . '&'); $this->redirect('/');}?>
然後在insertLargeData.php檔案中做資料庫操作。也可以用cronjob + php的方式實現大資料量的處理。
如果是在終端運行php命令,當終端關閉後,剛剛執行的命令也會被強制關閉,如果你想讓其不受終端關閉的影響,可以使用nohup命令實現:
<?php//Action代碼public function createAction(){ //.... //將args替換成要傳給insertLargeData.php的參數,參數間用空格間隔 system('nohup php -f insertLargeData.php ' . ' args ' . '&'); $this->redirect('/');}?>
你還可以使用screen命令代替nohup命令。