1. 程式人生 > >以例項全面講解PHP中多程序程式設計的相關函式的使用,php函式

以例項全面講解PHP中多程序程式設計的相關函式的使用,php函式

 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”的例子:

<?php
declare(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語句,即執行完子程序後立即就結束。否則它會又重頭開始執行這個指令碼的某些部分。

注意兩點:

  1. 子程序最好有一個exit;語句,防止不必要的出錯;
  2. pcntl_fork間最好不要有其它語句,例如: 
<?php
$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返回子程序的狀態碼,並可以指定第二個引數來說明是否以阻塞狀態呼叫:
阻塞方式呼叫的,函式返回值為子程序的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)語句,或寫成:
 
<?php
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)通知核心,自己對子程序的結束不感興趣,那麼子程序結束後,核心會回收,並不再給父程序傳送訊號,例如:
 

<?php
pcntl_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即所產生的子程序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檔案,然後在以後臺形式來執行它,例如:

<?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命令。