使用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進程,則原進程已經推出。
2. 使用kill函數,向原有的父進程發送空信號(kill(pid, 0))。使用這個方法對某個進程的存在性進行檢查,而不會真的發送信號。所以,如果這個函數返回-1表示父進程已經退出。
除了上面的這兩個方法外,還有一些實現上比較復雜的方法,比如建立管道或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();
//….code
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('/');
}
?>
你還可以使用screen命令代替nohup命令。