Linux-PHP 多进程编程

前言

刚刚确认这个 Chat 主题的时候,周围就有同事和同学质疑,有的说 多进程没有前途,有的说多进程就是神经病。虽然这些说法过于武断,但是不可否认的,是 PHP 在多进程方面确实不擅长。

既然如此,我还为什么要用 PHP 去实现多进程呢?原因有二。

第一,在开发过程中,我们需要学习许多关于 Linux 进程的一些知识,比如什么是进程,什么是写时复制,什么是信号,如何守护进程化,什么叫僵尸进程等。这些知识是 Linux 进程的知识,与 PHP 语言本身没有什么关系。我们如果能够使用 PHP 进行多进程编程,那么换成 C++ 语言也一样能够搞的定,当然前提是你会 C++。而且 PHP 和 C++ 的多进程相关的 api 也十分类似,例如 PHP 创建一个子进程用的是pcntl_fork() ,而 C++ 用的是 fork() 。

既然如此,为什么我不直接用 C++ 呢?效率比 PHP 更高。理由很简单,因为我是 PHPer,对 C++ 不熟。哈哈。

第二,作为一个 PHP 开发人员,在工作中难免遇到一些需要使用多进程的时候,虽然有一些著名的框架(swoole,workerman)可以用,但是我们必须知其然,且知其所以然。知其所以然的最好方式莫过于自己动手实践一番。

本人也使用多进程开发了两个项目,一是公司使用的推送系统,二是定时任务管理系统(个人开源项目)

环境准备

项目环境:

  1. 代码运行环境是 Linux,如果你使用 Windows 编程,建议你使用 vagrant,或者使用虚拟机或云服务器(用 ftp 同步代码)。
  2. 安装好 PHP
  3. 安装好 pcntl 扩展和 posix 扩展。如果你使用 lnmp 一键安装,那么这两个扩展应该是默认安装好的。否则请自行安装扩展,这里不对此进行详述。

建议学习方式(仅供参考):

  1. 建议使用 PC 看本文,因为有不少代码需要你去实践
  2. 先跳到文章最后下载完整代码,然后一边看代码一边看文章内容。最后参照我的源码自己写一遍。

hello world

按照惯例,上来就是一个通俗易懂的 hello world 一定能取得大部分开发人员的好感。

pcntl 扩展让 PHP 拥有进程创建,信号处理等能力。我们使用 pcntl_fork() 创建子进程:

<?php

//其他代码

$pid = pcntl_fork(); //fork进程

echo '父子进程都会执行的代码'.PHP_EOL;

if($pid > 0){
    echo "我是父进程,我创建的子进程id为 {$pid}".PHP_EOL;
}else if($pid == 0){
    echo '我是子进程'.PHP_EOL;
}else{
    echo 'fork进程失败'.PHP_EOL;
}

运行上面的代码,可以得到如下结果:

[root@vagrant-centos65 default]# php test.php
父子进程都会执行的代码
我是父进程,我创建的子进程id为 8560
父子进程都会执行的代码
我是子进程

这段代码执行过程中,执行到 pcntl_fork() 时,将产生一个一模一样的脚本,即子进程,然后父进程和子进程分别继续执行之后的代码,互不干扰。

实际开发中我们需要父进程和子进程执行不一样的代码怎么办?

很简单,pcntl_fork() 为我们提供了一个返回值 pid,这个返回值有些特殊。在父进程脚本中,这个值是子进程的进程 id,而在子进程脚本中,这个值是 0,我们就可以用这个值区分父子进程。

运行过程示意图如下:enter image description here

看到这里应该对多进程编程有了个大概的概念。

信号

多进程编程不是上面几行代码就搞定了的,我们还有许多周边问题需要解决,例如如何在进程间进行通讯,如何让进程常驻,并且不会随随便便的挂掉。

玩过 Linux 的肯定都用过这个命令 kill -9 进程号 来杀死一个进程,许多新手就误以为 kill 命令是杀死进程的命令,其实并不是,kill 命令是向进程发送一个信号,我们可以简单的将信号理解为是一个指令,9 才是真正的杀死进程的罪魁祸首。

除了 9 以为,还有 1、2、3 等几十种信号,可以使用 kill -l 查看信号列表:

[root@vagrant-centos65 default]# kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

有了信号,我们就能对进程发出指令,让进程执行退出,显示状态等动作。

每个信号都有一个默认响应动作,我们可以使用 pcntl_signal 来修改响应动作。我们来做个实验,先执行以下代码 php test.php

<?php

//注册SIGINT和SIGUSR2信号的响应
pcntl_signal(SIGINT, 'signalHandler', false);  //SIGINT : 2
pcntl_signal(SIGUSR2, 'signalHandler', false); //SIGINT : 12

function signalHandler($signal){

    echo "收到了信号:".$signal;

    if($signal == SIGUSR2){
        echo "进行业务操作1" . PHP_EOL;
    }elseif($signal == SIGINT){
        echo "进行业务操作2" . PHP_EOL;
    }
    // else if
}

while(true){

    sleep(1);
    //调用该方法,信号处理函数才会被执行
    pcntl_signal_dispatch();

}

while 循环将使进程一直运行,不退出,然后我们另外开一个终端,先用 ps aux 命令找到它的进程 id,然后发信号:

[root@vagrant-centos65 default]# kill -2 8600
[root@vagrant-centos65 default]# kill -12 8600

回到第一个终端,就可以看到当前进程收到了信号,并执行了信号处理函数,结果如下:

[root@vagrant-centos65 default]# php test.php
收到了信号:2进行业务操作2
收到了信号:12进行业务操作1

PS:上面脚本无法使用ctrl+c退出,因为 ctrl+c 就是信号 2,响应动作被我们修改掉了。请使用 kill -9 进程号 退出。

理论上来说,我们可以修改任何一个信号的响应动作,但是不建议随便修改,例如 9(SIGKILL),这个信号一般都是用来强制杀死进程,如果被改掉了,将对运维人员造成一定的困扰。

SIGUSR1 和 SIGUSR2 是留给用户使用的。

在 PHP 中,使用 posix_kill 来向一个进程发出信号,后面代码会使用到。

守护进程化

守护进程是不受终端控制,在后台运行的进程。我们必须使用守护进程,因为你无法保证你的终端永远不被关闭。

守护进程化的步骤如下:

  1. 创建子进程,父进程自杀
  2. 子进程创建新会话,以此摆脱终端控制

代码如下:

<?php

//设置文件掩码
umask(0);

$pid = pcntl_fork();

if($pid > 0){
    //父进程自杀
    exit(0); 
}elseif($pid == 0){
    //子进程创建新会话,摆脱终端控制
    if( -1 === posix_setsid() ){
        throw new Exception("setsid fail");
    }
    //此处可以结合上面的信号代码
    while(1){
        sleep(1);
        //pcntl_signal_dispatch();
    }
}else{
    throw new Exception("fork fail");
}

执行上面脚本后我们可以使用 ps aux 命令查看到该脚本在后台默默运行

master-worker 进程模型

master-worker 进程模型是一个比较经典且常用的进程模式,nginx 也是用这个模型:

[root@vagrant-centos65 default]# ps -ef | grep nginx
root      1096     1  0 May04 ?        00:00:00 nginx: master process /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
www       1097  1096  0 May04 ?        00:02:20 nginx: worker process
www       1098  1096  0 May04 ?        00:00:00 nginx: worker process

模型示意图:

enter image description here

worker 进程是 master 进程的子进程,worker进程负责具体的工作,而 master 进程仅负责监控 worker 进程的状态,一旦发现 worker 进程挂掉了,就立刻 fork 一个新的 worker 进程,以此来保证程序的高可用。

到这里你可能会有疑问,如果 master 也挂掉了怎么办?其实不用担心,worker 进程会挂掉基本上都是因为业务上抛异常导致的,而 master 进程仅负责监控,不做任何业务处理,只要你不主动杀死它,它挂掉的概率几乎为零。

worker 进程的数量建议等于 CPU 核心数,或整数倍,这样可以最有效合理地利用 CPU 资源。

master 和 worker 之间则可以使用信号进行通讯。假设我们现在要 stop 这个程序,我们只需要向 master 发出一个信号,然后 master 分别向各个 worker 发出信号让 worker 停止,最后 master 自己也停止。

有了上面的基础,我们就可以正式开始开发。

开发步骤一:环境检查

首先我们分别创建 Worker.php 和 test.php :

worker.php

<?php

class Worker{


    public static function runAll(){
        static::checkEnv();
    }

    public static function checkEnv(){

        if (php_sapi_name() != 'cli') {
            exit('请使用命令行模式运行!');
        }

        if(!function_exists('posix_kill')){
            exit('请先安装posix扩展'."\n");
        }

        if(!function_exists('pcntl_fork')){
            exit('请先安装pcntl扩展'."\n");
        }

    }

}

test.php

<?php

require 'Worker.php';

Worker::runAll();

php test.php 运行

下面所有代码都写在Worker类中,重复代码不再贴出

开发步骤二:初始化

在项目中,我们还需要一些必备的初始化操作,例如配置日志文件路径,目录权限检查等:

    public static $log_file = '';

    //将master进程id保存到这个文件中
    public static $pid_file = '';

    //保存worker进程的状态
    public static $status_file = '';


    public static function runAll(){
        static::checkEnv();
        static::init();
    }

    public static function init(){

        //$temp_dir = sys_get_temp_dir() . '/myworker/';
        $temp_dir = __DIR__.'/tmp/';

        if (!is_dir($temp_dir) && !mkdir($temp_dir)) {
            exit('mkdir runtime fail');
        }
        $test_file = $temp_dir . 'test';
        //尝试创建文件
        if(touch($test_file)){
            @unlink($test_file);
        }else{
            exit('permission denied: dir('.$temp_dir.')');
        }

        if (empty(static::$status_file)) {
            static::$status_file = $temp_dir . 'status_file.status';
        }

        if (empty(self::$pid_file)) {
            static::$pid_file = $temp_dir . 'master.pid';
        }

        if (empty(self::$log_file)) {
            static::$log_file = $temp_dir . 'worker.log';
        }

        static::log('初始化完成');
    }

    public static function log($message)
    {
        $message = '['.date('Y-m-d H:i:s') .']['. $message . "]\n";
        file_put_contents((string)self::$log_file, $message, FILE_APPEND | LOCK_EX);
    }

开发步骤三:解析命令

启动和停止项目都是使用命令行操作,在 PHP 中,使用 global $argv 可以获取我们输入的命令,如:

<?php 

global $argv;

var_dump($argv);

测试:

[root@vagrant-centos65 myworker]# php test_arvg.php a b c
array(4) {
  [0]=>
  string(13) "test_arvg.php"
  [1]=>
  string(1) "a"
  [2]=>
  string(1) "b"
  [3]=>
  string(1) "c"
}

我们启动多进程项目的命令一般格式是:php test.php start -d ,-d 是可选选项,表示使用守护进程模式启动,适合项目正式运行时使用。没有 -d 则是调试模式,所有信息都会输出到终端,适合在开发阶段使用。

代码如下:

    //是否使用守护进程模式启动
    public static $deamonize = false;

    public static function runAll(){
        static::checkEnv();
        static::init();
        static::parseCommand();
    }

    public static function parseCommand(){
        global $argv;

        if(!isset($argv[1]) || !in_array($argv[1],['start','stop','status'])){
            exit('usage: php your.php start | stop | status !' . PHP_EOL);
        }

        $command1 = $argv[1]; //start , stop , status
        $command2 = $argv[2]; // -d

        //检测master是否正在运行
        $master_id = @file_get_contents(static::$pid_file);
        //向master进程发送0信号,0信号比较特殊,进程不会响应,但是可以用来检测进程是否存活
        $master_alive = $master_id && posix_kill($master_id,0);

        if($master_alive){
            //不能重复启动
            if($command1 == 'start' && posix_getpid() != $master_id){
                exit('worker is already running !'.PHP_EOL);
            }
        }else{
            //项目未启动的情况下,只有start命令有效
            if ($command1 != 'start') {
                exit('worker not run!' . PHP_EOL);
            }
        }

        switch($command1){
            case 'start':
                if($command2 == '-d'){
                    static::$deamonize = true;
                }
                break;
            case 'stop':
                //停止进程
                //必须exit退出
                exit(0);
                break;
            case 'status':
                //查看状态
                //必须exit退出
                exit(0);
                break;
            default:
                exit('usage: php your.php start | stop | status !' . PHP_EOL);
                break;
        }

    }

stop和status 功能后面再说,我们先实现 start 功能,在 parseComman() 方法中,start 不需要进行任何实质性的操作。

开发步骤四:守护进程化

解析完命令,如果是 start 操作,立即进行守护进程化操作:

    public static function runAll(){
        static::checkEnv();
        static::init();
        static::parseCommand();
        static::deamonize();
    }
    public static function deamonize(){

        if(static::$deamonize == false){
            return;
        }

        umask(0);

        $pid = pcntl_fork();

        if($pid > 0){
            exit(0);
        }elseif($pid == 0){
            if(-1 === posix_setsid()){
                throw new Exception("setsid fail");
            }
            static::setProcessTitle('myworker: master');
        }else{
            throw new Exception("fork fail");
        }
    }

    public static function setProcessTitle($title){
        //设置进程名
        if (function_exists('cli_set_process_title')) {
            @cli_set_process_title($title);
        }
    }

守护进程化在前面就说过了,不再重复。

cli_set_process_title 是设置进程的名称,方便以后查看进程,就像用 ps 命令查看 nginx,下图红框中就是进程名:

enter image description here

开发步骤五:保存 master 进程号

代码运行到这里,当前进程就是 master 进程了,我们要将 master 进程 id 保存到文件中:

    public static $master_pid = 0;

    public static function runAll(){
        static::checkEnv();
        static::init();
        static::parseCommand();
        static::deamonize();
        static::saveMasterPid();
    }
    public static function saveMasterPid(){
        static::$master_pid = posix_getpid();
        if(false === @file_put_contents(static::$pid_file, static::$master_pid)){
            throw new Exception('fail to save master pid ');
        }
    }

开发步骤六:注册信号处理

上面已经解释过信号,现在我们要注册几个信号处理函数,分别对应 stop,status 操作

    public static function runAll(){
        static::checkEnv();
        static::init();
        static::parseCommand();
        static::deamonize();
        static::saveMasterPid();
        static::installSignal();
    }

    public static function installSignal()
    {
        pcntl_signal(SIGINT, array(__CLASS__, 'signalHandler'), false);
        pcntl_signal(SIGUSR2, array(__CLASS__, 'signalHandler'), false);
        //SIG_IGN表示忽略该信号,不做任何处理。SIGPIPE默认会使进程退出
        pcntl_signal(SIGPIPE, SIG_IGN, false);
    }

    public static function signalHandler($signal){
        switch ($signal) {
            case SIGINT: // Stop.
                //static::stopAll();
                break;
            case SIGUSR2: // Show status.
                //static::writeStatus();
                break;
        }
    }

stop 和 status 的具体实现后面再说

开发步骤七:重定向输入输出

如果是使用守护进程的模式启动项目,我们要求系统代码中的 echo、var_dump 等方法输出的内容不能显示到终端,因此需要将标准输出给重定向到 /dev/null 中。

如果是调试模式,则不需要该步骤中提供了 global $STDOUT, $STDERR; 让我们能够获取到标准输出。

    public static $stdoutFile = '/dev/null';

    public static function runAll(){
        static::checkEnv();
        static::init();
        static::parseCommand();
        static::deamonize();
        static::saveMasterPid();
        static::installSignal();
        static::resetStd();
    }
    public static function resetStd(){
        if(static::$deamonize == false){
            return;
        }
        global $STDOUT, $STDERR;
        $handle = fopen(self::$stdoutFile, "a");
        if ($handle) {
            unset($handle);
            @fclose(STDOUT);
            @fclose(STDERR);
            $STDOUT = fopen(self::$stdoutFile, "a");
            $STDERR = fopen(self::$stdoutFile, "a");
        } else {
            throw new Exception('can not open stdoutFile ' . self::$stdoutFile);
        }
    }

Linux 中的 /dev/null 是一个黑洞,丢进去的东西都将消失不见,相当于垃圾桶。

开发步骤八:fork 子进程

重头戏来了,这一步是核心步骤,首先我们需要确定我们要几个 worker 进程,然后循环 fork 出来,值得注意的是,fork 完成后,master 进程和 worker 进程都要使用 while 死循环保持运行状态,否则代码执行完毕就会自动退出了。

    public static $workers = [];

    //worker实例
    public static $instance = null;

    //worker数量
    public $count = 2;

    //worker启动时的回调方法
    public $onWorkerStart = null;

    public function __construct(){
        static::$instance = $this;
    }
    public static function runAll(){
        static::checkEnv();
        static::init();
        static::parseCommand();
        static::deamonize();
        static::saveMasterPid();
        static::installSignal();
        static::resetStd();
        static::forkWorkers();
    }
    public static function forkWorkers(){

        $worker_count = static::$instance->count;

        while(count(static::$workers) < $worker_count ){
            static::forkOneWorker(static::$instance);
        }
    }

    public static function forkOneWorker($instance){
        $pid = pcntl_fork();
        if($pid > 0){
            static::$workers[$pid] = $pid;
        }elseif($pid == 0){
            static::log('创建了一个worker');
            static::setProcessTitle('myworker process');
            //运行
            $instance->run();
        }else{
            throw new Exception('fork one worker fail');
        }
    }

    public function run(){
        if($this->onWorkerStart){
            try {
                //worker启动,调用onWorkerStart回调
                call_user_func($this->onWorkerStart, $this);
            } catch (\Exception $e) {
                static::log($e);
                sleep(1);
                exit(250);
            } catch (\Error $e) {
                static::log($e);
                sleep(1);
                exit(250);
            }
        }
        //死循环,保持worker运行,并且一有信号来了就调用信号处理函数
        while (1) {
            pcntl_signal_dispatch();
            sleep(1);
        }
    }

修改 test.php:

<?php

require 'Worker.php';

$worker = new Worker();

$worker->count = 2;

$worker->onWorkerStart = function($worker){
    echo 'onWorkerStart' . PHP_EOL;
};

Worker::runAll();

业务代码一般写在onWorkerStart ,worker启动时会调用该方法。比如可以开一个socket,阻塞等待用户连接。

调试模式运行:

[root@vagrant-centos65 myworker]# php test.php start
onWorkerStart
onWorkerStart
[root@vagrant-centos65 myworker]# ps aux |grep myworker
root      8813  0.0  0.1 154664  5532 pts/3    S    14:43   0:00 myworker process
root      8814  0.0  0.1 154664  5532 pts/3    S    14:43   0:00 myworker process

运行起来后,发现 onWorkerStart 都正常输出了,但是 master 进程却不见了,这是咋回事?因为上面代码中我们没有让 master 死循环,它执行完代码就退出了。下面我们就来做这件事。

开发步骤九:监控 worker 进程

为什么master 死循环的操作不在上面代码中直接实现,而要单独出来讲,因为master 进程不仅要保持运行,它还肩负着监控 worker 进程的重任,一旦发现 worker 进程意外退出了,就立刻重新 fork 一个 worker 进程,做到高可用。

    //记录当前进程的状态
    public static $status = 0;

    //运行中
    const STATUS_RUNNING = 1;
    //停止
    const STATUS_SHUTDOWN = 2;

    public static function runAll(){
        static::checkEnv();
        static::init();
        static::parseCommand();
        static::deamonize();
        static::saveMasterPid();
        static::installSignal();
        static::resetStd();
        static::forkWorkers();
        static::monitorWorkers();
    }
    public static function monitorWorkers(){
        //设置当前状态为运行中
        static::$status = static::STATUS_RUNNING;
        while (1) {
            pcntl_signal_dispatch();
            $status = 0;
            //阻塞,等待子进程退出
            $pid = pcntl_wait($status, WUNTRACED);

            self::log("worker[ $pid ] exit with signal:".pcntl_wstopsig($status));

            pcntl_signal_dispatch();
            //child exit
            if ($pid > 0) {
                //意外退出时才重新fork,如果是我们想让worker退出,status = STATUS_SHUTDOWN
                if (static::$status != static::STATUS_SHUTDOWN) {
                    unset(static::$workers[$pid]);
                    static::forkOneWorker(static::$instance);
                }
            }
        }
    }

上面代码中最关键的就是 pcntl_wait ,该方法的作用是等待子进程退出,一旦有子进程退出,就会返回退出的进程的 id,然后就重新 fork 一个 worker 进程。

注意:到了这一步,要先删除前面测试产生的 master.pid 文件并且 kill -9 前面运行起来的 worker 进程。否则会影响下面的实验

我们来实验一下,先使用守护进程模式启动:

[root@vagrant-centos65 myworker]# php test.php start -d
[root@vagrant-centos65 myworker]# ps aux | grep myworker
root      8844  0.0  0.1 154660  5528 ?        Ss   15:20   0:00 myworker: master
root      8845  0.0  0.1 154660  5508 ?        S    15:20   0:00 myworker process
root      8846  0.0  0.1 154660  5508 ?        S    15:20   0:00 myworker process

可以看到三个进程都正常运行,进程 id 分别为 8844、8845、8846。

然后我们用kill命令杀死 8845,再次查看进程状态:

[root@vagrant-centos65 myworker]# kill -9 8845
[root@vagrant-centos65 myworker]# ps aux | grep myworker
root      8844  0.0  0.1 154660  5636 ?        Ss   15:20   0:00 myworker: master
root      8846  0.0  0.1 154660  5508 ?        S    15:20   0:00 myworker process
root      8849  0.0  0.1 154660  5508 ?        S    15:22   0:00 myworker process

根据上面结果,8845 确实被我们杀掉了,并且重新启动了一个新的 worker,进程 id 为 8849。

到此,多进程的启动已经全部实现,并且整个项目已经完成了四分之三。

PS:由于 stop 功能还没实现,所以需要手动 kill master 和 worker 进程,注意要先 kill master,再 kill worker,然后手动删除 master.pid 文件

小知识:pcntlwait() 方法不仅可以监控子进程退出,而且有回收资源的作用。fork 出来的每一个子进程最终都要使用 pcntlwait 进行资源回收,否则就会产生僵尸进程。僵尸进程就是代码执行完了却没有被回收的进程,会白白占用 CPU 等资源,造成系统资源浪费。

开发步骤十:查看进程状态

查看进程状态这一步并不是必须的,你可以直接用 ps aux | grep myworker 查看即可。

但如果你并不满足,想要查看更多的状态,例如查看一些业务状态,那么就必须要开发这个功能。

原理其实很简单,我们给 master 发一个信号,然后 master 分别给各个 worker 发一个信号。worker 收到这个信号,就往一个文件里写入一些信息(状态信息),然后我们再读取这个文件的内容并显示出来就可以了。

这一步,我们需要修改前面的 parseCommand() 和 signalHandler() 方法的代码,如果不明白,请先回头看一看。

pubilc static function parseCommand(){
    //其他代码,重复的不贴出来了
    switch($command1){
        // case start 和 case stop的代码就不贴出来了
        case 'status':
            //查看状态
            if(is_file(static::$status_file)){
                //先删除就得status文件
                @unlink(static::$status_file);
            }
            //给master发送信号
            posix_kill($master_id,SIGUSR2);
            //等待worker进程往status文件里写入状态
            usleep(300000);
            @readfile(static::$status_file);
            exit(0);
            break;
        default:
            exit('usage: php your.php start | stop | status !' . PHP_EOL);
               break;
    }
}

public static function signalHandler($signal){
    switch ($signal) {
         case SIGINT: // Stop.
             //static::stopAll();
             break;
         case SIGUSR2: // Show status.
    //master和worker都执行
             static::writeStatus();
             break;
       }
}

public static function writeStatus(){
    $pid = posix_getpid();

    if($pid == static::$master_pid){ //master进程

        $master_alive = static::$master_pid&& posix_kill(static::$master_pid,0);
        $master_alive = $master_alive ? 'is running' : 'die';
           $result = file_put_contents(static::$status_file, 'master[' . static::$master_pid . '] ' . $master_alive . PHP_EOL, FILE_APPEND | LOCK_EX);
        //给worker进程发信号
        foreach(static::$workers as $pid){
            posix_kill($pid,SIGUSR2);
        }
    }else{ //worker进程

        $name = 'worker[' . $pid . ']';
        $alive = $pid && posix_kill($pid, 0);
        $alive = $alive ? 'is running' : 'die';
        file_put_contents(static::$status_file, $name . ' ' . $alive . PHP_EOL, FILE_APPEND | LOCK_EX);
    }
}

测试:

[root@vagrant-centos65 myworker]# php test.php start -d
[root@vagrant-centos65 myworker]# php test.php status
master[8905] is running
worker[8906] is running
worker[8907] is running

PS:Worker 往文件写状态信息时,可以根据具体的业务写入更多的数据,这点可以自由发挥。

开发步骤十一:停止进程

停止进程的和查看进程状态的原理是一样的,向 master 进程发一个信号叫他go die,然后 master 向 worker 进程发信号叫他们 go die,然后大家一起 go die。

同样的修改 parseCommand() 和 signalHandler() 方法:

    public static function signalHandler($signal){
        switch ($signal) {
            case SIGINT: // Stop.
                static::stopAll();
                break;
            case SIGUSR2: // Show status.
                //master和worker都执行
                static::writeStatus();
                break;
        }
    }
    pubilc static function parseCommand(){
        //其他代码,重复的不贴出来了
        switch($command1){
            // case start 和 case status的代码就不贴出来了
            case 'stop':
                //停止进程
                $master_id && posix_kill($master_id, SIGINT);
                //只要还没杀死master,就一直杀
                while ($master_id && posix_kill($master_id, 0)) {
                    usleep(300000);
                }
                exit(0);
                break;
            default:
                exit('usage: php your.php start | stop | status !' . PHP_EOL);
                   break;
        }
    }
    public static function stopAll(){

        $pid = posix_getpid();

        if($pid == static::$master_pid){ //master进程
            //将当前状态设为停止,否则子进程一退出master重新fork
            static::$status = static::STATUS_SHUTDOWN;
            //通知子进程退出
            foreach(static::$workers as $pid){
                posix_kill($pid,SIGINT);
            }
            //删除pid文件
            @unlink(static::$pid_file);
            exit(0);
        }else{ //worker进程
            static::log('worker[' . $pid .'] stop');
            exit(0);
        }
    }

测试:

[root@vagrant-centos65 myworker]# php test.php start -d
[root@vagrant-centos65 myworker]# php test.php status
master[8920] is running
worker[8921] is running
worker[8922] is running
[root@vagrant-centos65 myworker]# php test.php stop

到此,多进程模型全部开发了。根据 status 和 stop 的原理,大家可以自行开发 reload,restart 等功能。

除此之外,我们在 worker 启动时调用了 onWorkerStart 回调,同理的,我们也可以开发 onWorkerStop 等回调。

扩展:定时器 timer

在多进程模型中,我们经常使用到定时器。其实定时器就是用了信号来实现。

pcntl 扩展为我们提供了pcntl_alarm() 方法,调用 pcntl_alarm(1) ,系统就会在 1 秒之后给当前进程发一个SIGALRM信号,收到信号时,我们再次调用 pcntl_alarm(1),如此不断循环,就会每秒执行一次信号处理函数,就这样形成了定时器。

了解了原理,代码其实很简单,仔细阅读一下就明白了。

Timer.php

<?php

class Timer
{
    public static $tasks = array();

    public static function init()
    {
        pcntl_signal(SIGALRM, array( __CLASS__, 'signalHandle'), false);
    }

    public static function signalHandle()
    {
        pcntl_alarm(1);

        if (empty(self::$tasks)) {
            return;
        }
        //执行任务
        foreach (self::$tasks as $run_time => $task) {
            $time_now = time();
            if ($time_now >= $run_time) {
                $func = $task[0];
                $args = $task[1];
                $interval = $task[2];
                $persistent = $task[3];
                call_user_func_array($func, $args);
                unset(self::$tasks[$run_time]);
                if($persistent){
                    Timer::add($interval, $func, $args,$persistent);
                }
            }
        }
    }

    /**
     * @param $interval 几秒后执行
     * @param $func 要执行的回调方法
     * @param array $args 参数
     * @param bool $persistent 是否持续执行
     * @return bool
     */
    public static function add($interval, $func, $args = array(),$persistent = true)
    {
        if ($interval <= 0) {
            echo new Exception('wrong interval');
            return false;
        }
        if (!is_callable($func)) {
            echo new Exception('not callable');
            return false;
        } else {
            $runtime = time() + $interval;
            self::$tasks[$runtime] = array($func, $args, $interval,$persistent);
            return true;
        }
    }

    public static function tick()
    {
        pcntl_alarm(1);
    }
}

测试:

<?php

require 'Worker.php';
require 'Timer.php';

$worker = new Worker();

$worker->count = 2;

$worker->onWorkerStart = function($worker){
    Timer::init();
    //2秒执行一次
    Timer::add(2,function(){
        $pid = posix_getpid();
        echo date('Y-m-d H:i:s') .' pid : ' . $pid.PHP_EOL;
    },[],true);

    //注意,一定要调用tick方法定时器才会运行
    Timer::tick();
};

Worker::runAll();

为了直观的看到效果,使用调试模式运行:

[root@vagrant-centos65 myworker]# php test.php start
2018-05-14 16:38:53 pid : 9181
2018-05-14 16:38:53 pid : 9182
2018-05-14 16:38:55 pid : 9181
2018-05-14 16:38:55 pid : 9182
2018-05-14 16:38:57 pid : 9182
2018-05-14 16:38:57 pid : 9181
2018-05-14 16:38:59 pid : 9182
2018-05-14 16:38:59 pid : 9181

总结

主要知识点:

  • 使用 pcntl_fork 创建子进程
  • 守护进程化:fork 一个进程并且杀死自己
  • 使用 pcntl_signal 注册信号处理方法
  • 使用 posix_kill 向指定进程发送信号
  • worker 进程 while 循环等待信号
  • master 进程 while 循环中持续监控 worker 进程状态
  • 在 onWorkerStart 回调中实现业务代码

完整代码下载:

  • CSDN 下载(需要 1 积分):https://download.csdn.net/download/u010837612/10413528
  • 网盘下载:链接: https://pan.baidu.com/s/1MI4E1Y521I5Q5UEyvCQvcg 密码: x8wn

建议学习方式:先下载完整代码,然后一边看代码一边看文章内容。最后参照我的源码自己写一遍。

其他学习资料:本人最开始学习这一块内容是通过研究 workerman 的源码,workerman 是非常好的学习资料,有兴趣的朋友可以看看。

最后,感谢大家的支持。

猜你喜欢

转载自blog.csdn.net/weixin_42075590/article/details/80740968
今日推荐