php 多进程编程的深入探讨

发布时间:2019-11-23编辑:脚本学堂
本文介绍下,有关php多进程编程的相关知识,举了多个实例,有需要的朋友,参考下吧。

使用PHP真正的多进程运行模式,适用于数据采集、邮件群发、数据源更新、tcp服务器等环节。

PHP有一组进程控制函数(编译时需要 –enable-pcntl与posix扩展),使得php能在*nix系统中实现跟c一样的创建子进程、使用exec函数执行程序、处理信号等功能。PCNTL使用ticks来作为信号处理机制(signal handle callback mechanism),可以最小程度地降低处理异步事件时的负载。何谓ticks?Tick 是一个在代码段中解释器每执行 N 条低级语句就会发生的事件,这个代码段需要通过declare来指定。

常用的PCNTL函数
    pcntl_alarm ( int $seconds )设置一个$seconds秒后发送SIGALRM信号的计数器
    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 SIGALRMn"; 
            pcntl_alarm(5); 
        } 

        pcntl_signal(SIGALRM, "signal_handler", true); 
        pcntl_alarm(5); 

        for(;;) { 
        } 
?>
 

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';    //不会执行到该行 
?>

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
    }
?>

    pcntl_wait ( int &$status [, int $options ] )阻塞当前进程,只到当前进程的一个子进程退出或者收到一个结束当前进程的信号。使用$status返回子进程的状态码,并可以指定第二个参数来说明是否以阻塞状态调用:
    1. 阻塞方式调用的,函数返回值为子进程的pid,如果没有子进程返回值为-1;
    2. 非阻塞方式调用,函数还可以在有子进程在运行但没有结束的子进程时返回0。
    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秒才结束,而父进程阻塞着直到子进程退出之后才继续运行。
pcntl_getpriority ([ int $pid [, int $process_identifier ]] )取得进程的优先级,即nice值,默认为0,在我的测试环境的linux中(centos release 5.2 (Final)),优先级为-20到19,-20为优先级最高,19为最低。(手册中为-20到20)。

pcntl_setpriority ( int $priority [, int $pid [, int $process_identifier ]] )设置进程的优先级。
    posix_kill可以给进程发送信号
    pcntl_singal用来设置信号的回调函数

当父进程退出时,子进程如何得知父进程的退出
当父进程退出时,子进程一般可以通过下面这两个比较简单的方法得知父进程已经退出这个消息:

    当父进程退出时,会有一个INIT进程来领养这个子进程。这个INIT进程的进程号为1,所以子进程可以通过使用getppid()来取得当前父进程的pid。如果返回的是1,表明父进程已经变为INIT进程,则原进程已经推出。
    使用kill函数,向原有的父进程发送空信号(kill(pid, 0))。使用这个方法对某个进程的存在性进行检查,而不会真的发送信号。所以,如果这个函数返回-1表示父进程已经退出。

除了上面的这两个方法外,还有一些实现上比较复杂的方法,比如建立管道或socket来进行时时的监控等等。
PHP多进程采集数据的例子
 

复制代码 代码示例:

<?php
/**
* Project: Signfork: php多线程库
* File:    Signfork.class.php
* modify by www.jb200.com
*/

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;
    }
  }
}
?>