使用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”的例子:
- declare(ticks = 1);
- function signal_handler($signal) {
- print “Caught SIGALRMn”;
- pcntl_alarm(5);
- }
- pcntl_signal(SIGALRM, “signal_handler”, true);
- pcntl_alarm(5);
- for(;;) {
- }
3.pcntl_exec ( string $path [, array $args [, array $envs ]] )
在當前的進程空間中執行指定程序,類似于c中的exec族函數,所謂當前空間,即載入指定程序的代碼覆蓋掉當前進程的空間,執行完該程序進程即結束,代碼如下:
- $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語句,即執行完子進程后立即就結束,否則它會又重頭開始執行這個腳本的某些部分。
注意兩點:
(1). 子進程最好有一個exit;語句,防止不必要的出錯;
(2). pcntl_fork間最好不要有其它語句,例如:
- $pid = pcntl_fork();
- //這里最好不要有其他的語句
- if ($pid == -1) {
- die('could not fork');
- } else if ($pid) {
- // we are the parent
- pcntl_wait($status); //Protect against Zombie children
- } else {
- // we are the child
- }
5. pcntl_wait ( int &$status [, int $options ] )
阻塞當前進程,只到當前進程的一個子進程退出或者收到一個結束當前進程的信號,使用$status返回子進程的狀態碼,并可以指定第二個參數來說明是否以阻塞狀態調用:
(1). 阻塞方式調用的,函數返回值為子進程的pid,如果沒有子進程返回值為-1;
(2). 非阻塞方式調用,函數還可以在有子進程在運行但沒有結束的子進程時返回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這些函數,例如如下代碼:
- $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
用來設置信號的回調函數,當父進程退出時,子進程如何得知父進程的退出,當父進程退出時,子進程一般可以通過下面這兩個比較簡單的方法得知父進程已經退出這個消息:
(1). 當父進程退出時,會有一個INIT進程來領養這個子進程。這個INIT進程的進程號為1,所以子進程可以通過使用getppid()來取得當前父進程的pid。如果返回的是1,表明父進程已經變為INIT進程,則原進程已經推出。
除了上面的這兩個方法外,還有一些實現上比較復雜的方法,比如建立管道或socket來進行時時的監控等等。
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,代碼如下:
- //…..
- //需要安裝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)語句,或寫成:
pcntl_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)通知內核,自己對子進程的結束不感興趣,那么子進程結束后,內核會回收,并不再給父進程發送信號,例如:
pcntl_signal(SIGCHLD, SIG_IGN);$pid = pcntl_fork();
4. 還有一個技巧,就是fork兩次,父進程fork一個子進程,然后繼續工作,子進程再fork一個孫進程后退出,那么孫進程被init接管,孫進程結束后,init會回收,不過子進程的回收還要自己做,下面是一個例子:
- #include “apue.h”
- #include
- 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回收清理這些父進程已退出的僵尸子進程。
所以,上面的示例可以改成:
- //…..
- //需要安裝pcntl的php擴展,并加載它
- if(function_exists(“pcntl_fork”)){
- //生成第一個子進程
- $pid = pcntl_fork(); //$pid即所產生的子進程id
- if($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文件,然后在以后臺形式來運行它,例如:
- //Action代碼
- public function createAction(){
- //….
- //將args替換成要傳給insertLargeData.php的參數,參數間用空格間隔
- system('php -f insertLargeData.php ' . ' args ' . '&');
- $this->redirect('/');
- }
然后在insertLargeData.php文件中做數據庫操作,也可以用cronjob + php的方式實現大數據量的處理。
如果是在終端運行php命令,當終端關閉后,剛剛執行的命令也會被強制關閉,如果你想讓其不受終端關閉的影響,可以使用nohup命令實現:
- //Action代碼
- public function createAction(){
- //….
- //將args替換成要傳給insertLargeData.php的參數,參數間用空格間隔
- system('nohup php -f insertLargeData.php ' . ' args ' . '&');
- $this->redirect('/');
- }
新聞熱點
疑難解答