PHP多进程编程

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

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

经常使用的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”的例子:编程

1数组

2服务器

3数据结构

4多线程

5并发

6异步

7

8

9

10

11

12

13

14

15

<?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族函数。所谓当前空间,即载入指定程序的代码覆盖掉当前进程的空间,执行完该程序进程即结束。

1

2

3

4

5

6

7

8

9

10

11

<?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间最好不要有其它语句,例如:

1

2

3

4

5

6

7

8

9

10

11

12

<?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返回子进程的状态码,并能够指定第二个参数来讲明是否以阻塞状态调用:
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这些函数。
例如:

1

2

3

4

5

6

7

8

9

10

11

12

<?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
用来设置信号的回调函数

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

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

除了上面的这两个方法外,还有一些实现上比较复杂的方法,好比创建管道或socket来进行时时的监控等等。

PHP多进程采集数据的例子

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

<?php

/**

* Project: Signfork: php多线程库

* File:    Signfork.class.php

*/

 

class Signfork{

  /**

   * 设置子进程通讯文件所在目录

   * @var string

   */

  private $tmp_path='/tmp/';

 

/**

  * Signfork引擎主启动方法

  * 一、判断$arg类型,类型为数组时将值传递给每一个子进程;类型为数值型时,表明要建立的进程数.

  * @param object $obj 执行对象

  * @param string&#124;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主进程控制方法

   * 一、$tmpfile 判断子进程文件是否存在,存在则子进程执行完毕,并读取内容

   * 二、$data收集子进程运行结果及数据,并用于最终返回

   * 三、删除子进程文件

   * 四、轮询一次0.03秒,直到全部子进程执行完毕,清理子进程资源

   * @param  string&#124;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子进程执行方法

  * 一、pcntl_fork 生成子进程

  * 二、file_put_contents 将'$obj->__fork($val)'的执行结果存入特定序列命名的文本

  * 三、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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

<?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)语句,或写成:

1

2

3

4

5

<?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)通知内核,本身对子进程的结束不感兴趣,那么子进程结束后,内核会回收,并再也不给父进程发送信号,例如:

1

2

3

4

5

<?php

pcntl_signal(SIGCHLD, SIG_IGN);

$pid = pcntl_fork();

//....code

?>

4. 还有一个技巧,就是fork两次,父进程fork一个子进程,而后继续工做,子进程再fork一个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收还要本身作。下面是一个例子:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

#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回收清理这些父进程已退出的僵尸子进程。

因此,上面的示例能够改为:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

<?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文件,而后在之后台形式来运行它,例如:

1

2

3

4

5

6

7

8

9

<?php

//Action代码

public function createAction(){

    //....

    //将args替换成要传给insertLargeData.php的参数,参数间用空格间隔

    system('php -f insertLargeData.php ' . ' args ' . '&');

    $this->redirect('/');

}

?>

而后在insertLargeData.php文件中作数据库操做。也能够用cronjob + php的方式实现大数据量的处理。

若是是在终端运行php命令,当终端关闭后,刚刚执行的命令也会被强制关闭,若是你想让其不受终端关闭的影响,可使用nohup命令实现:

1

2

3

4

5

6

7

8

9

<?php

//Action代码

public function createAction(){

    //....

    //将args替换成要传给insertLargeData.php的参数,参数间用空格间隔

    system('nohup php -f insertLargeData.php ' . ' args ' . '&');

    $this->redirect('/');

}

?>

你还可使用screen命令代替nohup命令。

相关文章
相关标签/搜索