了解php的扩展在liunx下实现多进程

虽然PHP本身不支持多进程,但基于LINUX的PHP扩展PCNTL却可以提供多进程编程。网络上很多同类文章,但笔者进行多次尝试后发现,不是难以控制进程数量,就是有潜在产生僵尸进程或孤儿进程的危险,或者父进程阻塞难以获得更大的并发效果,且大多没有介绍FORK的原理,使得PHP程序员学习PCNTL并发编程尤为困难。本文力求解决这个问题。

FORK编程的大概原理是,每次调用fork函数,操作系统就会产生一个子进程,儿子进程所有的堆栈信息都是原封不动复制父进程的,而在fork之后,父进程与子进程实际上是相互独立的,父子进程不会相互影响。也就是说,fork调用位置之前的所有变量,父进程和子进程是一样的,但fork之后则取决于各自的动作,且数据也是独立的;因为数据已经完整的复制给了子进程。而唯一能够区分父子进程的方法就是判断fork的返回值。如果为0,表示是子进程,如果为正数,表示为父进程,且该正数为子进程的PID(进程号),而如果是-1,表示子进程创建失败。

需要大量的并发进程为同时为我们处理事情。这时,我们就需要fork多次,而产生的子进程数量需要在我们的控制之中,否则无限制的fork只会拖垮服务器。笔者曾经有过经历,几秒钟服务器负载从0.3左右飙到800多,吓的一身冷汗。

而子进程的使用通常会涉及到两种:子进程执行完任务直接退出;子进程常驻内存,等待任务。以上两种方式适用于不同情况。第一种情况大多我们不需要考虑太多,除非子进程的创建是循环进行的。而第二种则需要考虑进程间通信。

无论哪一种,无可避免的一个问题就是僵尸进程。僵尸进程就是子进程退出后,父进程没有及时回收,系统仍然保留子进程的执行信息(例如PID,退出状态等),留待其他程序读取。如果僵尸进程数量很少,我们可以忽略掉。但如果是在一个循环中fork(并发编程中常见的死循环),这个问题就不能无视了,父进程必须定期回收已经退出的子进程。子进程的回收我们采用pcnt_wait函数来完成。

父进程必须等待一个子进程退出后,再创建另外一个。额,这还是串行执行的不是吗?是的,解决办法就是将pcntl_wait函数替换成pcntl_waitpid()并添加WNOHANG参数。该函数可以在没有子进程退出的情况下立刻跳出执行后续代码。

$max = 800000;

$workers = 20;
 
$pids = array();
for($i = 0; $i < $workers; $i++){
    $pids[$i] = pcntl_fork();
    switch ($pids[$i]) {
        case -1:
            echo "fork error : {$i} \r\n";
            exit;
        case 0:
            $param = array(
                'lastid' => $max / $workers * $i,
                'maxid' => $max / $workers * ($i+1),
            );
            $this->executeWorker($input, $output, $param);
            exit;
        default:
            break;
    }
}
 
foreach ($pids as $i => $pid) {
    if($pid) {
        pcntl_waitpid($pid, $status);
    }
}

循环创建子进程是一件非常浪费操作系统资源的事情。既然使用了死循环来处理任务,那么就说明任务是一个可以队列化的数据结构。我们可以采用进程间的通信,解决子进程退出重建的问题。而通信的机制主要有信号量、管道、共享内存等。然后我们需要一个生产者和消费者的模型。而基于fork的这种代码编写方式,非常不利于我们编写复杂的业务逻辑。所以建议将进程控制与业务处理的代码进程抽象隔离。进程间通信本文暂不涉及,如果读者有需要可以阅读关于管道、共享内存和信号量的文章。

根据上面所说,循环创建子进程会造成系统资源的浪费,而循环创建往往意味着任务可以队列化。我们可以创建子进程后,让子进程常驻内存,持续执行等待任务到达。而这类模型往往可以用生产-消费模型来实现。生产者负责将任务写入队列,而子进程从队列中取出任务并执行。队列的实现最好采用本身支持互斥的方式,这样可以降低代码的复杂度,管道是个不错的选择。

基于fork方式实现的多进程,由于我们只能使用Pid来做代码隔离,所以进程控制中会充斥的各种if、else或者switch。这对实生产者和消费者模型造成一定难度。以下是一个生产者消费者的模型:

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
<?php
/**
  * @author:Jenner
  * @date 2014-01-14
  */
class JetMultiProcess {
 
     //最大队列长度
     private $size ;
 
     private $curSize ;
 
     //生产者
     private $producer ;
 
     //消费者
     private $worker ;
 
     private $queueName ;
 
     private $httpsqs ;
 
     /**
      * 构造函数
      * @param string $worker 需要创建的消费者类名
      * @param int $size 最大子进程数量
      * @param $producer 需要创建的消费者类名
      */
     public function __construct( $producer $worker $size =10){
         $this ->producer =  new $producer ;
         $this ->worker =  $worker ;
         $this ->size =  $size ;
         $this ->curSize = 0;
     }
 
     public function start(){
 
         $producerPid = pcntl_fork();
         if ( $producerPid == -1) {
             die ( "could not fork" );
         else if ( $producerPid ) { // parent
 
             while (true){
                 $pid = pcntl_fork();
                 if ( $pid == -1) {
                     die ( "could not fork" );
                 else if ( $pid ) { // parent
 
                     $this ->curSize++;
                     if ( $this ->curSize>= $this ->size){
                         $sunPid = pcntl_wait( $status );
                     }
 
                 else { // worker
 
                     $worker new $this ->worker;
                     $worker ->run();
                     exit ();
                 }
             }
 
         else { // producer
             $this ->producer->run();
             exit ();
         }
     }
}

以上代码,通过size控制多进程数量,通过构造函数传入生产者和消费者的类型。父进程第一次fork产生一个子进程生产者,然后再进行size次fork创建多个消费者。类似方法可以创建多个生产者和多个消费者协同工作。生产者和消费者都必须实现run方法,并在run方法中创建死循环。循环写入和读取队列进行协同工作。该类没有提供进程间通信的功能。通信需要在生产者和消费者类中实现。这样能够使得进程控制的代码看起来更加简洁

参数 option 可以为 0 或下面的 OR 组合:
1.WNOHANG 如果没有任何已经结束的子进程则马上返回,不予以
等待。
2.WUNTRACED 如果子进程进入暂停执行情况则马上返回,但结束
状态不予以理会。
子进程的结束状态返回后存于 status,底下有几个宏可判别结束情
况:
3.WIFEXITED(status)如果子进程正常结束则为非 0 值。
4.WEXITSTATUS(status)取得子进程 exit()返回的结束代码,一
般会先用 WIFEXITED 来判断是否正常结束才能使用此宏。
5.WIFSIGNALED(status)如果子进程是因为信号而结束则此宏值为
6.WTERMSIG(status) 取得子进程因信号而中止的信号代码,一般
会先用 WIFSIGNALED 来判断后才使用此宏。
7.WIFSTOPPED(status) 如果子进程处于暂停执行情况则此宏值为
真。一般只有使用 WUNTRACED 时才会有此情况。
8.WSTOPSIG(status) 取得引发子进程暂停的信号代码,一般会先
用 WIFSTOPPED 来判断后才使用此宏。
  1. #include <iostream>
  2.  
    #include <sys/types.h>
  3.  
    #include <sys/wait.h>
  4.  
    #include <stdlib.h>
  5.  
    #include <unistd.h>
  6.  
     
  7.  
     
  8.  
    using namespace std;
  9.  
     
  10.  
    #define FORK_NUM 5
  11.  
     
  12.  
    int
  13.  
    main(int argc, char* argv[])
  14.  
    {
  15.  
            pid_t pid, rmpid;
  16.  
            int i;
  17.  
     
  18.  
            for(i=0; i<FORK_NUM; i++){
  19.  
                    pid = fork();
  20.  
                    if(pid == 0){
  21.  
                            break;
  22.  
                    }else if(pid > 0){
  23.  
                            printf("creat %dth son \n", i+1);
  24.  
                            if(i == 2){
  25.  
                                    rmpid = pid;
  26.  
                            }
  27.  
                    }else{
  28.  
                            printf("creat %dth son error\n", i+1);
  29.  
                    }
  30.  
            }
  31.  
     
  32.  
            sleep(i+1);
  33.  
     
  34.  
            if(i==FORK_NUM){
  35.  
                    int status;
  36.  
                    printf("I am parent\n");
  37.  
            #if 0   // wait one son progress
  38.  
                    if(waitpid(rmpid, &status, 0) == -1){// only wait 3th son progress
  39.  
                            perror("waipid error");
  40.  
                            exit(1);
  41.  
                    }
  42.  
                    if(WIFEXITED(status)!=0 && WIFSIGNALED(status)==0){ 
  43.  
                            printf("son progress normal exit\n");
  44.  
                            printf("son progress exit status : %d \n", WEXITSTATUS(status));
  45.  
                    }else{// 0   
  46.  
      printf("son expretion exit,signal ID:%d \n",  WTERMSIG(status));
  47.  
                    }
  48.  
                    printf("the %dth is waitpided by me\n", i+1);
  49.  
                    while(1)sleep(1); 
  50.  
            #else  // wait all of son progress
  51.  
                    do{
  52.  
                            rmpid = waitpid(-1, &status, WNOHANG);
  53.  
                            if(rmpid == 0){
  54.  
                                    sleep(1);
  55.  
                                    continue;
  56.  
                            }
  57.  
                            else if(rmpid == -1)break;
  58.  
                            else{
  59.  
                                    i--;
  60.  
                                    printf("has %d son progress wll to be wait\n", i);
  61.  
                            }
  62.  
     
  63.  
                    }while(i > 0);
  64.  
     
  65.  
                    while(1) sleep(1);
  66.  
     
  67.  
            #endif
  68.  
            }else{
  69.  
                    printf("I am child %d \n", i+1);
  70.  
            }
  71.  
     
  72.  
     
  73.  
            return 0;
  74.  
    }

     

posted @ 2020-12-01 10:42  行驶中大神  阅读(110)  评论(0编辑  收藏  举报