以實例全面講解PHP中多進程編程的相關函數(shù)的使用
PHP有一組進程控制函數(shù)(編譯時需要–enable-pcntl與posix擴展),使得php能實現(xiàn)跟c一樣的創(chuàng)建子進程、使用exec函數(shù)執(zhí)行程序、處理信號等功能。
<?php
header('content-type:text/html;charset=utf-8' );
// 必須加載擴展
if (!function_exists("pcntl_fork")) {
die("pcntl extention is must !");
}
//總進程的數(shù)量
$totals = 3;
// 執(zhí)行的腳本數(shù)量
$cmdArr = array();
// 執(zhí)行的腳本數(shù)量的數(shù)組
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); //如果父進程不關心子進程什么時候結(jié)束,子進程結(jié)束后,內(nèi)核會回收。
foreach ($cmdArr as $cmd) {
$pid = pcntl_fork(); //創(chuàng)建子進程
//父進程和子進程都會執(zhí)行下面代碼
if ($pid == -1) {
//錯誤處理:創(chuàng)建子進程失敗時返回-1.
die('could not fork');
} else if ($pid) {
//父進程會得到子進程號,所以這里是父進程執(zhí)行的邏輯
//如果不需要阻塞進程,而又想得到子進程的退出狀態(tài),則可以注釋掉pcntl_wait($status)語句,或?qū)懗桑?
pcntl_wait($status,WNOHANG); //等待子進程中斷,防止子進程成為僵尸進程。
} else {
//子進程得到的$pid為0, 所以這里是子進程執(zhí)行的邏輯。
$path = $cmd["path"];
$pid = $cmd['pid'] ;
$total = $cmd['total'] ;
echo exec("/usr/bin/php {$path} {$pid} {$total}")."\n";
exit(0) ;
}
}
?>
使用PHP真正的多進程運行模式,適用于數(shù)據(jù)采集、郵件群發(fā)、數(shù)據(jù)源更新、tcp服務器等環(huán)節(jié)。
PHP有一組進程控制函數(shù)(編譯時需要 –enable-pcntl與posix擴展),使得php能在*nix系統(tǒng)中實現(xiàn)跟c一樣的創(chuàng)建子進程、使用exec函數(shù)執(zhí)行程序、處理信號等功能。 PCNTL使用ticks來作為信號處理機制(signal handle callback mechanism),可以最小程度地降低處理異步事件時的負載。何謂ticks?Tick 是一個在代碼段中解釋器每執(zhí)行 N 條低級語句就會發(fā)生的事件,這個代碼段需要通過declare來指定。
常用的PCNTL函數(shù)
1. pcntl_alarm ( int $seconds )
設置一個$seconds秒后發(fā)送SIGALRM信號的計數(shù)器
2. pcntl_signal ( int $signo , callback $handler [, bool $restart_syscalls ] )
為$signo設置一個處理該信號的回調(diào)函數(shù)。下面是一個隔5秒發(fā)送一個SIGALRM信號,并由signal_handler函數(shù)獲取,然后打印一個“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 ]] )
在當前的進程空間中執(zhí)行指定程序,類似于c中的exec族函數(shù)。所謂當前空間,即載入指定程序的代碼覆蓋掉當前進程的空間,執(zhí)行完該程序進程即結(jié)束。
<?php $dir = '/home/shankka/'; $cmd = 'ls'; $option = '-l'; $pathtobin = '/bin/ls'; $arg = array($cmd, $option, $dir); pcntl_exec($pathtobin, $arg); echo '123'; //不會執(zhí)行到該行 ?>
4. pcntl_fork ( void )
為當前進程創(chuàng)建一個子進程,并且先運行父進程,返回的是子進程的PID,肯定大于零。在父進程的代碼中可以用 pcntl_wait(&$status)暫停父進程知道他的子進程有返回值。注意:父進程的阻塞同時會阻塞子進程。但是父進程的結(jié)束不影響子進程的運行。
父進程運行完了會接著運行子進程,這時子進程會從執(zhí)行pcntl_fork()的那條語句開始執(zhí)行(包括此函數(shù)),但是此時它返回的是零(代表這是一個子進程)。在子進程的代碼塊中最好有exit語句,即執(zhí)行完子進程后立即就結(jié)束。否則它會又重頭開始執(zhí)行這個腳本的某些部分。
注意兩點:
- 子進程最好有一個exit;語句,防止不必要的出錯;
- 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 ] )
阻塞當前進程,只到當前進程的一個子進程退出或者收到一個結(jié)束當前進程的信號。使用$status返回子進程的狀態(tài)碼,并可以指定第二個參數(shù)來說明是否以阻塞狀態(tài)調(diào)用:
阻塞方式調(diào)用的,函數(shù)返回值為子進程的pid,如果沒有子進程返回值為-1;
非阻塞方式調(diào)用,函數(shù)還可以在有子進程在運行但沒有結(jié)束的子進程時返回0。
6. pcntl_waitpid ( int $pid , int &$status [, int $options ] )
功能同pcntl_wait,區(qū)別為waitpid為等待指定pid的子進程。當pid為-1時pcntl_waitpid與pcntl_wait 一樣。在pcntl_wait和pcntl_waitpid兩個函數(shù)中的$status中存了子進程的狀態(tài)信息,這個參數(shù)可以用于 pcntl_wifexited、pcntl_wifstopped、pcntl_wifsignaled、pcntl_wexitstatus、 pcntl_wtermsig、pcntl_wstopsig、pcntl_waitpid這些函數(shù)。
例如:
<?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秒才結(jié)束,而父進程阻塞著直到子進程退出之后才繼續(xù)運行。
7. pcntl_getpriority ([ int $pid [, int $process_identifier ]] )
取得進程的優(yōu)先級,即nice值,默認為0,在我的測試環(huán)境的linux中(CentOS release 5.2 (Final)),優(yōu)先級為-20到19,-20為優(yōu)先級最高,19為最低。(手冊中為-20到20)。
8. pcntl_setpriority ( int $priority [, int $pid [, int $process_identifier ]] )
設置進程的優(yōu)先級。
9. posix_kill
可以給進程發(fā)送信號
10. pcntl_singal
用來設置信號的回調(diào)函數(shù)
當父進程退出時,子進程如何得知父進程的退出
當父進程退出時,子進程一般可以通過下面這兩個比較簡單的方法得知父進程已經(jīng)退出這個消息:
當父進程退出時,會有一個INIT進程來領養(yǎng)這個子進程。這個INIT進程的進程號為1,所以子進程可以通過使用getppid()來取得當前父進程的pid。如果返回的是1,表明父進程已經(jīng)變?yōu)镮NIT進程,則原進程已經(jīng)推出。
使用kill函數(shù),向原有的父進程發(fā)送空信號(kill(pid, 0))。使用這個方法對某個進程的存在性進行檢查,而不會真的發(fā)送信號。所以,如果這個函數(shù)返回-1表示父進程已經(jīng)退出。
除了上面的這兩個方法外,還有一些實現(xiàn)上比較復雜的方法,比如建立管道或socket來進行時時的監(jiān)控等等。
PHP多進程采集數(shù)據(jù)的例子
<?php
/**
* Project: Signfork: php多線程庫
* File: Signfork.class.php
*/
class Signfork{
/**
* 設置子進程通信文件所在目錄
* @var string
*/
private $tmp_path='/tmp/';
/**
* Signfork引擎主啟動方法
* 1、判斷$arg類型,類型為數(shù)組時將值傳遞給每個子進程;類型為數(shù)值型時,代表要創(chuàng)建的進程數(shù).
* @param object $obj 執(zhí)行對象
* @param string|array $arg 用于對象中的__fork方法所執(zhí)行的參數(shù)
* 如:$arg,自動分解為:$obj->__fork($arg[0])、$obj->__fork($arg[1])...
* @return array 返回 array(子進程序列=>子進程執(zhí)行結(jié)果);
*/
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 判斷子進程文件是否存在,存在則子進程執(zhí)行完畢,并讀取內(nèi)容
* 2、$data收集子進程運行結(jié)果及數(shù)據(jù),并用于最終返回
* 3、刪除子進程文件
* 4、輪詢一次0.03秒,直到所有子進程執(zhí)行完畢,清理子進程資源
* @param string|array $arg 用于對應每個子進程的ID
* @return array 返回 array([子進程序列]=>[子進程執(zhí)行結(jié)果]);
*/
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子進程執(zhí)行方法
* 1、pcntl_fork 生成子進程
* 2、file_put_contents 將'$obj->__fork($val)'的執(zhí)行結(jié)果存入特定序列命名的文本
* 3、posix_kill殺死當前進程
* @param object $obj 待執(zhí)行的對象
* @param object $i 子進程的序列ID,以便于返回對應每個子進程數(shù)據(jù)
* @param object $param 用于輸入對象$obj方法'__fork'執(zhí)行參數(shù)
*/
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()函數(shù)進行資源釋放。但在 pcntl_waitpid()不一定釋放的就是當前運行的進程,也可能是過去生成的僵尸進程(沒有釋放);也可能是并發(fā)時其它訪問者的僵尸進程。但可以使用posix_kill($cid, SIGTERM)在子進程結(jié)束時殺掉它。
子進程會自動復制父進程空間里的變量。
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;
//阻塞父進程,直到子進程結(jié)束,不適合需要長時間運行的腳本,可使用pcntl_wait($status, 0)實現(xiàn)非阻塞式
pcntl_wait($status);
// parent proc code
exit;
}else{
// child proc code
//結(jié)束當前子進程,以防止生成僵尸進程
if(function_exists("posix_kill")){
posix_kill(getmypid(), SIGTERM);
}else{
system('kill -9'. getmypid());
}
exit;
}
}
}else{
// 不支持多進程處理時的代碼在這里
}
//.....
?>
如果不需要阻塞進程,而又想得到子進程的退出狀態(tài),則可以注釋掉pcntl_wait($status)語句,或?qū)懗桑?
<?php
pcntl_wait($status, 1);
//或
pcntl_wait($status, WNOHANG);
?>
在上面的代碼中,如果父進程退出(使用exit函數(shù)退出或redirect),則會導致子進程成為僵尸進程(會交給init進程控制),子進程不再執(zhí)行。
僵尸進程是指的父進程已經(jīng)退出,而該進程dead之后沒有進程接受,就成為僵尸進程.(zombie)進程。任何進程在退出前(使用exit退出) 都會變成僵尸進程(用于保存進程的狀態(tài)等信息),然后由init進程接管。如果不及時回收僵尸進程,那么它在系統(tǒng)中就會占用一個進程表項,如果這種僵尸進程過多,最后系統(tǒng)就沒有可以用的進程表項,于是也無法再運行其它的程序。
預防僵尸進程有以下幾種方法:
1. 父進程通過wait和waitpid等函數(shù)使其等待子進程結(jié)束,然后再執(zhí)行父進程中的代碼,這會導致父進程掛起。上面的代碼就是使用這種方式實現(xiàn)的,但在WEB環(huán)境下,它不適合子進程需要長時間運行的情況(會導致超時)。
使用wait和waitpid方法使父進程自動回收其僵尸子進程(根據(jù)子進程的返回狀態(tài)),waitpid用于臨控指定子進程,wait是對于所有子進程而言。
2. 如果父進程很忙,那么可以用signal函數(shù)為SIGCHLD安裝handler,因為子進程結(jié)束后,父進程會收到該信號,可以在handler中調(diào)用wait回收
3. 如果父進程不關心子進程什么時候結(jié)束,那么可以用signal(SIGCHLD, SIG_IGN)通知內(nèi)核,自己對子進程的結(jié)束不感興趣,那么子進程結(jié)束后,內(nèi)核會回收,并不再給父進程發(fā)送信號,例如:
<?php pcntl_signal(SIGCHLD, SIG_IGN); $pid = pcntl_fork(); //....code ?>
4. 還有一個技巧,就是fork兩次,父進程fork一個子進程,然后繼續(xù)工作,子進程再fork一個孫進程后退出,那么孫進程被init接管,孫進程結(jié)束后,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()過程中,假設子進程結(jié)束時父進程仍存在,而父進程fork()之前既沒安裝SIGCHLD信號處理函數(shù)調(diào)用 waitpid()等待子進程結(jié)束,又沒有顯式忽略該信號,則子進程成為僵尸進程,無法正常結(jié)束,此時即使是root身份kill-9也不能殺死僵尸進程。補救辦法是殺死僵尸進程的父進程(僵尸進程的父進程必然存在),僵尸進程成為”孤兒進程”,過繼給1號進程init,init會定期調(diào)用wait回收清理這些父進程已退出的僵尸子進程。
所以,上面的示例可以改成:
<?php
//.....
//需要安裝pcntl的php擴展,并加載它
if(function_exists("pcntl_fork")){
//生成第一個子進程
$pid = pcntl_fork(); //$pid即所產(chǎn)生的子進程id
if($pid == -1){
//子進程fork失敗
die('could not fork');
}else{
if($pid){
//父進程code
sleep(5); //等待5秒
exit(0); //或$this->_redirect('/');
}else{
//第一個子進程code
//產(chǎn)生孫進程
if(($gpid = pcntl_fork()) < 0){ ////$gpid即所產(chǎn)生的孫進程id
//孫進程產(chǎn)生失敗
die('could not fork');
}elseif($gpid > 0){
//第一個子進程code,即孫進程的父進程
$status = 0;
$status = pcntl_wait($status); //阻塞子進程,并返回孫進程的退出狀態(tài),用于檢查是否正常退出
if($status ! = 0) file_put_content('filename', '孫進程異常退出');
//得到父進程id
//$ppid = posix_getppid(); //如果$ppid為1則表示其父進程已變?yōu)閕nit進程,原父進程已退出
//得到子進程id:posix_getpid()或getmypid()或是fork返回的變量$pid
//kill掉子進程
//posix_kill(getmypid(), SIGTERM);
exit(0);
}else{ //即$gpid == 0
//孫進程code
//....
//結(jié)束孫進程(即當前進程),以防止生成僵尸進程
if(function_exists('posix_kill')){
posix_kill(getmypid(), SIGTERM);
}else{
system('kill -9'. getmypid());
}
exit(0);
}
}
}
}else{
// 不支持多進程處理時的代碼在這里
}
//.....
?>
怎樣產(chǎn)生僵尸進程的
一個進程在調(diào)用exit命令結(jié)束自己的生命的時候,其實它并沒有真正的被銷毀,而是留下一個稱為僵尸進程(Zombie)的數(shù)據(jù)結(jié)構(gòu)(系統(tǒng)調(diào)用exit,它的作用是使進程退出,但也僅僅限于將一個正常的進程變成一個僵尸進程,并不能將其完全銷毀)。在Linux進程的狀態(tài)中,僵尸進程是非常特殊的一種,它已經(jīng)放棄了幾乎所有內(nèi)存空間,沒有任何可執(zhí)行代碼,也不能被調(diào)度,僅僅在進程列表中保留一個位置,記載該進程的退出狀態(tài)等信息供其他進程收集,除此之外,僵尸進程不再占有任何內(nèi)存空間。它需要它的父進程來為它收尸,如果他的父進程沒安裝SIGCHLD信號處理函數(shù)調(diào)用wait或waitpid()等待子進程結(jié)束,又沒有顯式忽略該信號,那么它就一直保持僵尸狀態(tài),如果這時父進程結(jié)束了,那么init進程自動會接手這個子進程,為它收尸,它還是能被清除的。但是如果如果父進程是一個循環(huán),不會結(jié)束,那么子進程就會一直保持僵尸狀態(tài),這就是為什么系統(tǒng)中有時會有很多的僵尸進程。
任何一個子進程(init除外)在exit()之后,并非馬上就消失掉,而是留下一個稱為僵尸進程(Zombie)的數(shù)據(jù)結(jié)構(gòu),等待父進程處理。這是每個子進程在結(jié)束時都要經(jīng)過的階段。如果子進程在exit()之后,父進程沒有來得及處理,這時用ps命令就能看到子進程的狀態(tài)是”Z”。如果父進程能及時 處理,可能用ps命令就來不及看到子進程的僵尸狀態(tài),但這并不等于子進程不經(jīng)過僵尸狀態(tài)。
如果父進程在子進程結(jié)束之前退出,則子進程將由init接管。init將會以父進程的身份對僵尸狀態(tài)的子進程進行處理。
另外,還可以寫一個php文件,然后在以后臺形式來運行它,例如:
<?php
//Action代碼
public function createAction(){
//....
//將args替換成要傳給insertLargeData.php的參數(shù),參數(shù)間用空格間隔
system('php -f insertLargeData.php ' . ' args ' . '&');
$this->redirect('/');
}
?>
然后在insertLargeData.php文件中做數(shù)據(jù)庫操作。也可以用cronjob + php的方式實現(xiàn)大數(shù)據(jù)量的處理。
如果是在終端運行php命令,當終端關閉后,剛剛執(zhí)行的命令也會被強制關閉,如果你想讓其不受終端關閉的影響,可以使用nohup命令實現(xiàn):
<?php
//Action代碼
public function createAction(){
//....
//將args替換成要傳給insertLargeData.php的參數(shù),參數(shù)間用空格間隔
system('nohup php -f insertLargeData.php ' . ' args ' . '&');
$this->redirect('/');
}
?>
你還可以使用screen命令代替nohup命令。
相關文章
詳解no input file specified 三種解決方法
這篇文章主要介紹了詳解no input file specified 三種解決方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-11-11
thinkphp5框架實現(xiàn)的自定義擴展類操作示例
這篇文章主要介紹了thinkphp5框架實現(xiàn)的自定義擴展類操作,結(jié)合實例形式簡單分析了thinkPHP5在extend目錄下建立自定義擴展類的具體操作步驟與相關實現(xiàn)技巧,需要的朋友可以參考下2019-05-05
php項目中百度 UEditor 簡單安裝調(diào)試和調(diào)用
這篇文章主要介紹了php項目中百度 UEditor 簡單安裝調(diào)試和調(diào)用的相關資料,需要的朋友可以參考下2015-07-07

