龙空技术网

PHP进阶教程-进程是个啥?通过一个Master-Worker案例搞定多进程

IT不是挨踢 140

前言:

而今我们对“phpworker”大体比较注重,小伙伴们都想要学习一些“phpworker”的相关内容。那么小编在网摘上搜集了一些有关“phpworker””的相关资讯,希望你们能喜欢,朋友们一起来学习一下吧!

​在人类刚发明计算机的时候,只能同时处理一件事情。我们通常说程序的设计来自于现实的人类世界,这个进程同样也是。

是早期计算机的多程序执行流程,我们可以看到在一个CPU的生命周期里面,多个程序是顺序执行的。后面的程序什么时候执行取决于前面的程序什么之后执行完。如果当前执行的程序进行IO时,CPU也只能等待,CPU的使用率极低。如果当前执行的程序占用CPU时间很长,其他程序得不到CPU的执行权,只能卡着不动。

基于上面说的单进程有很多的缺点,所以出现了多道程序技术,使得程序可以并发执行。从上图可以看出来多道程序设计把CPU分成了时间片,可以在单CPU的情况下实现并发。CPU以人无法感知的速度在不停切换执行不同的程序,这样让我们看起来像是多个程序同时在执行。

我们可以看到了每个进程有自己独立的内存、地址空间、数据栈等等,每个进程都跟其他进程互不干扰,这就是为什么有些人在写代码都时候,说定义了一个全局的属性,但是一个进程改变了,另一个进程读取却没有变化,这就是原因,每个进程有自己的独立堆栈,内存资源等。

进程状态

图中可以看到进程有不同的状态,当进程在不同状态进程处于就绪态的时候就是等待操作系统进行调度。前面我们提到过进程的切换,CPU不断的切换到不同的进程来执行,当CPU调度别的进程当前执行的进程怎么办呢?下次再调度会这个进程的之后怎么知道代码执行到哪里,都有什么数据呢?其实当CPU调度别的进程的时候,会保存当前执行进程的信息、状态,这种操作叫做上下文切换。一个进程从运行态转到休眠状态的时候,进程的现场会保存到该进程的内核栈,当这进程再次进入就运行状态的时候,CPU从这个进程的PCB内核栈读取这个现场信息恢复进程。

多道程序设计把CPU执行的时间分割成了很小的时间片,由操作系统在不同状态的进程进行调度。但是在一个核心CPU一次只能处理一件事情,只是因为每次切换的时间太快了,让我们认为是一起执行的。

上下文切换

当一个进程遇到阻塞的之后CPU就会切换到另一个进程执行,避免在在进程阻塞的时候浪费CPU资源。

进程不是越多越好,进程过多在上下文切换过程中浪费大量的CPU可执行时间

并发与并行的区别:并发是某一个时间段做多少事情,并行是同一时刻同时做多件事情

孤儿进程

前面说过了,计算机模拟的是人类世界,那么这个孤儿进程跟我们人类世界一样,没有父母没有监护人的为孤儿。孤儿进程产生的原因是父进程终止执行了,但是子进程依然在执行的子进程就被叫做孤儿进程。

僵尸进程

僵尸进程产生的原因是因为子进程终止之后父进程没有对其进行回收,子进程的进程描述符依然保留在系统当中,所以产生了僵尸进程,僵尸进程对于操作系统是有资源浪费的。所以务必避免出现僵尸进程,解决办法可以在父进程调用wait或者waitpid解决。

守护进程

在Linux操作系统里面我们想要实现关闭终端之后还可以运行,我们通常采用

* nohup

* setsid

* 命令后面加 &

这几种方式,我们想想在前面说过的孤儿进程好像就可以是现在这样的功能。我们只要父进程退出执行,子进程还是依然会执行的。这就是我们通过故意产生孤儿进程来实现守护进程的功能。

通过前面的知识我们了解了进程之后,我们接下来做一个关于进程的实战功能。

实现一个Master、Worker的进程 希望通过这个案例大家能对PHP实现进程有更深的了解。

背景:在编写服务端程序的时候单进程的消费速度跟不上,这时候可以选择多进程来操作,选择多进程为了防止进程死掉导致任务无法执行,所以采用Master、Worker进程模型实现一个进程管理。

<?phpclass SunnyProcess{    // Master进程id    protected static $masterPid;    // Worker进程id集合    protected static $workerPid = [];        /**     * 设置进程名称     * @String $name 进程名称     */    public static function setProcessTitle($name){        if (extension_loaded('proctitle') && function_exists('setproctitle')) {            @setproctitle($name);        } elseif (version_compare(phpversion(), "5.5", "ge") && function_exists('cli_set_process_title')) {            @cli_set_process_title($name);        }    }    /**     * 创建Worker进程     * @Int $num Worker进程数量     */    public static function forkWorker($num = 2){        for($i=0;$i<$num;$i++){            $pid = pcntl_fork();            if($pid>0){                // 父进程,把子进程id存到进程集合                static::$workerPid[$pid] = $pid;                print_r(static::$workerPid);            }elseif($pid===0){                // 子进程                static::setProcessTitle("php:worker");                while(1){                    // 获取当前进程id                    $pids = posix_getpid();                    $time = microtime(true);                    $masterpid = static::$masterPid;                    echo "主进程:{$masterpid},当前进程:{$pids},当前时间:{$time}\n";                    sleep(1);                }            }else{                exit('进程启动失败\n');            }        }    }    /**     * Master进程监控Worker进程     */    public static function monitor(){        while(true){            // 挂起当前进程的执行直到一个子进程退出或接收到一个信号            $status = 0;            // 使用pcntl_wait挂起当前进程,挂起之后会阻塞不执行pcntl_wait之后的代码            // 直到pcntl_wait返回pid            $pid = pcntl_wait($status, WUNTRACED);            var_dump($pid);//这段代码往后的代码只有在pcntl_wait有触发的时候才会调用            pcntl_signal_dispatch();            if ($pid >= 0) {                // worker健康检查                unset(static::$workerPid[$pid]);                echo "worker检查:{$pid}\n";                static::forkWorker(1);                print_r(static::$workerPid);echo "\n";            }        }    }    public static function runAll(){        // 设置主进程名字        static::setProcessTitle("php:master");        // 获取主进程id        static::$masterPid = posix_getpid();        // 创建3个子进程        static::forkWorker(3);        // 监控worker进程,当子进程死掉重新创建        static::monitor();    }}SunnyProcess::runAll();

这样可以通过PHP的创建一个较为健壮的Master-Worker进程,Master负责管理子进程,具体工作让进程去工作。

在上面的代码中启动了一个Master进程和3个Worker进程,Master进程的工作很简单。

public static function runAll(){  // 设置主进程名字  static::setProcessTitle("php:master");  // 获取主进程id  static::$masterPid = posix_getpid();  // 创建3个子进程  static::forkWorker(3);  // 监控worker进程,当子进程死掉重新创建  static::monitor();}

runAll 方法里面就是Master所做的事情

设置一个主进程的名字,也就是master进程的名字获取master主进程的id保存起来创建子进程监控子进程

public static function setProcessTitle($name){    if (extension_loaded('proctitle') && function_exists('setproctitle')) {        @setproctitle($name);    } elseif (version_compare(phpversion(), "5.5", "ge") && function_exists('cli_set_process_title')) {        @cli_set_process_title($name);    }}

setProcessTitle 方法负责设置进程的名字

public static function forkWorker($num = 2){  for($i=0;$i<$num;$i++){    $pid = pcntl_fork();    if($pid>0){      // 父进程,把子进程id存到进程集合      static::$workerPid[$pid] = $pid;      print_r(static::$workerPid);    }elseif($pid===0){      // 子进程      static::setProcessTitle("php:worker");      while(1){        // 获取当前进程id        $pids = posix_getpid();        $time = microtime(true);        $masterpid = static::$masterPid;        echo "主进程:{$masterpid},当前进程:{$pids},当前时间:{$time}\n";        sleep(1);      }    }else{      exit('进程启动失败\n');    }  }}

forkWorker 方法负责创建子进程,因为在 runAll 方法中调用 forkWorker 方法的时候传递了一个参数值为 3 因此创建3个子进程,所以循环3次。

调用php的pcntl扩展函数 pcntl_fork 创建一个子进程,pcntl_fork 函数返回一个进程ID,如果该值大于0则为父进程,等于0则为子进程,否则失败。

真正的业务就在在子进程去工作的,在pid===0的时候去负责编写我们的业务代码。这里示例代码只是简单的输出一下进程id和时间。

public static function monitor(){  while(true){    // 挂起当前进程的执行直到一个子进程退出或接收到一个信号    $status = 0;    // 使用pcntl_wait挂起当前进程,挂起之后会阻塞不执行pcntl_wait之后的代码    // 直到pcntl_wait返回pid    $pid = pcntl_wait($status, WUNTRACED);    var_dump($pid);//这段代码往后的代码只有在pcntl_wait有触发的时候才会调用    pcntl_signal_dispatch();    if ($pid >= 0) {      // worker健康检查      unset(static::$workerPid[$pid]);      echo "worker检查:{$pid}\n";      static::forkWorker(1);      print_r(static::$workerPid);echo "\n";    }  }}

monitor 方法负责监控子进程是否正常工作,当进程死掉之后Master进程会接收到一个信号,调用 pcntl_wait 等待会阻塞等待信号发生,信号发生的时候会返回子进程的ID,通过子进程的ID去进程ID集合删除掉老的ID,并且重新调用 forkWorker 方法重新创建一个新的进程。

效果演示

左边执行脚本,右边查看进程信息;可以看到启动了四个进程,分别是一个Master和三个Worker进程。

杀死进程

kill -9 35170

杀死进程ID为35169的Worker进程,再次查看进程信息

可以看到进程ID为35169的进程已经不存在了,被杀死了。新多出来一个35174的进程。左边的信息框信息也可以看到打印出来的进程信息。

再次杀死一个进程

kill -9 35170

杀死进程ID为35170的Worker进程,再次查看进程信息

可以看到进程ID为35170的进程已经不存在了,被杀死了。新多出来一个35197的进程。左边的信息框信息也可以看到打印出来的进程信息。

视频演示:

视频加载中...

通过上面的效果演示可以看到,我们的目的基本达到了,Worker死掉之后Master会帮我们重新创建一个进程保证我们的业务正常运行。

标签: #phpworker