PHP8 编写cli运行的进程,实现热重启和多进程(pcntl与信号的使用)(用到pcntl_signal和inotify相关)

需要一个一直可以在后台跑的进程来处理业务逻辑,又担心因为代码上执行错误导致进程意外退出。

首先需要一个主进程,主进程监控所有子进程的状态,以及接收信号并对各个子进程发送信号

一个监控子进程,监控子进程加载的逻辑处理文件,如果发生了变化告知主进程需要重启子进程来重新加载逻辑文件。

若干个业务子进程,处理业务逻辑并接收信号。

过程中遇到的疑问:

1.在控制台运行代码后按ctrl+c时,主进程和所有的子进程都会接收SIGINT信号,如果没有对此信号进行处理,会默认退出

2.信号进入先后关系,第一个信号进来的时候正在处理(sleep),第二个信号又进来,会在第一个信号处理完成之后下一个信号触发的时候会执行第二个信号的处理逻辑

3.kill -9 主进程id,子进程时不会被关闭的

4.

$this->watch_files = inotify_init();
$files = get_included_files();
foreach ($files as $file){
    inotify_add_watch($this->watch_files,$file,IN_MODIFY);
}
$event = inotify_read($this->watch_files);

这段代码,在php8.0.12下,A文件发生变化后inotify_read可以读取到,下次A文件发生变化inotify_read任然可以读取到,

但是,在php7.3.21下,A文件第一次发生变化inotify_read可以读取到,下一次发生变化的时候inotify_read就读取不到了……,

采取的解决办法就是,每一次文件更新后,都会把监控子进程也一起重启。

5.注意父进程拉起子进程的时候,子进程会复制父进程的上下文,以下代码中有注册showdown事件,原本是写给父进程的,第一次初始化拉起子进程的时候子进程并没有这样的showdown事件,但是,一次文件修改后,会重新拉起子进程,这个时候子进程就有了showdown注册的事件,这里就容易出问题需要注意。

6.依然是要注意上下文,如果再父进程中已经加载了某个文件,那么这个文件即使发生修改,对之后的操作也不会有任何影响,因为已经加载到内存中了,重新拉起子进程的时候,父进程内已经有了这个文件的内容,子进程不会去自动加载,导致逻辑文件更新,但是子进程没有效果,这个时候需要注意,父进程中是不能加载那个逻辑文件的,要在子进程中加载,这样每一次重启子进程都会重新加载这个逻辑文件。

<?php
class Worker{
    protected $pid;
    protected $children=[];
    protected $woker_num = 2;
    protected $this_child_id;//当前子进程的id
    protected $deal_child_id;//监控文件状态的id
    public $watch_files;
    protected $is_reload = false;
    public $redis;
    protected $dd;
    public function __construct()
    {
        //引入逻辑处理代码
        $this->pid = posix_getpid();
//        $this->redis = new \Redis();
//        $this->redis->connect('127.0.0.1',6379);
    }

    /****************主进程相关s******************/
    /**
     * 主进程关闭后,给所有子进程发信号,关闭进程
     */
    public function kill(){
        echo '主进程被关闭'.PHP_EOL;
        //关闭监控文件的子进程
        posix_kill($this->deal_child_id, SIGKILL);
        //关闭逻辑子进程
        if($this->children)foreach ($this->children as $child){
            posix_kill($this->deal_child_id, SIGINT);//用户自定义信号,这里关闭子进程时要在子进程逻辑运行完后再去关闭
        }
    }
    public function run(){
        $this->forkSon($this->woker_num);
        $this->forkMon();
        register_shutdown_function(function () {//只给父进程注册关闭子进程的方法
            $this->kill();
        });
        $this->watchProcess();
    }
    /**
     * 监控子进程以及信号,如果有异常退出的就拉起
     */
    protected function watchProcess(){
        pcntl_signal(SIGUSR1,array($this,'signalHandler'),false);
        pcntl_signal(SIGUSR2,array($this,'signalHandler'),false);
        pcntl_signal(SIGINT,array($this,'signalHandler'),false);
        $status = 0;
        while (1){
            pcntl_signal_dispatch();
            $pid = pcntl_wait($status,WNOHANG);//pcntl_wait 此方法没有第二个参数WNOHANG时阻塞,设置了第二个参数WNOHANG后子进程未退出会直接返回0
            $index=array_search($pid,$this->children);
            if($pid>1 && $pid != $this->pid){//pcntl_wifexited检查是否是一个正常的退出 && ($index!==false)
                //检测到有子进程退出
                if($index!==false){
                    //业务子进程被关闭
                    $this->forkSon(1);
                    unset($this->children[$index]);

                }
                if($pid == $this->deal_child_id){
                    //监视子进程
                    $this->forkMon();
                }
            }
            pcntl_signal_dispatch();
            sleep(1);
        }
    }

    /**
     * 关闭业务子进程
     */
    protected function stopSon(){
        foreach ($this->children as $child){
            posix_kill($child,SIGUSR1);
        }
    }

    /**
     * 关闭监控子进程
     */
    protected function stopMon(){
        posix_kill($this->deal_child_id,SIGUSR1);
    }
    protected function signalHandler($sigo){
        switch ($sigo){
            case SIGUSR1://接受到的信号,平滑关闭子进程,然后子进程在关闭后告诉父进程已关闭,可重新拉起
                //告诉子进程要重启了
                echo "告诉子进程要关闭了,热重启".PHP_EOL;
                $this->stopSon();
                echo '执行结束'.PHP_EOL;
                break;
            case SIGUSR2://接受到的信号,由子进程告知,现在重新拉起一个新的子进程
                //拉起子进程
                echo "拉起子进程".PHP_EOL;
                break;
            case SIGINT:
                echo "按下ctrl+c,关闭所有字进程".PHP_EOL;
                $this->stopSon();
                exit('主进程退出');
//                swoole_event_del($this->watch_fd);
                break;
        }
    }
    protected function forkSon($woker_num){
        for ($i=0 ; $i < $woker_num ; $i++){
            $pid = pcntl_fork();
            if ($pid == 0) {//子进程
                include 'index.php';
                $this->dd = new DD();
                $this->this_child_id = posix_getpid();//子进程id
                //业务子进程逻辑
                $this->forkBusiness();
                exit;
            } elseif($pid > 0) {
                $this->children[] = $pid;
            } else {
                die('fork fail!'.PHP_EOL);
            }
        }
    }
    protected function forkMon(){
        $pid = pcntl_fork();
        if ($pid == 0) {//子进程
            include_once 'index.php';
            $this->this_child_id = posix_getpid();//子进程id
            $this->deal_child_id = $this->this_child_id;
            //监控子进程逻辑
            $this->forkMonitor();
            exit;
        } elseif($pid > 0) {
            $this->deal_child_id = $pid;
        } else {
            die('fork fail!'.PHP_EOL);
        }
    }
    /****************主进程相关e******************/

    /****************业务子进程相关s******************/
    /**
     * 子进程重启
     */
    public function sonReload(){
        echo '业务子进程被关闭'.PHP_EOL;
//        if($this->is_reload){
//            //告知父进程,可以重新拉起子进程了
//            posix_kill($this->deal_child_id, SIGUSR2);
//        }

    }
    protected function sonSignalHandler($sigo){
        switch ($sigo){
            case SIGUSR1://关闭子进程
                echo $this->this_child_id."子进程接受SIGUSR1,关闭".PHP_EOL;
//                $this->is_reload = true;
                exit;
                break;
            case SIGUSR2://
                echo $this->this_child_id."子进程接受SIGUSR2".PHP_EOL;
//                $this->is_reload = false;
//                exit;
                break;
            case SIGINT:
                echo $this->this_child_id."子进程接受SIGINT,按下ctrl+c".PHP_EOL;
                exit;
//                swoole_event_del($this->watch_fd);
                break;
        }
    }
    /**
     * 拉起业务子进程
     */
    protected function forkBusiness(){
        register_shutdown_function(function () {//只给父进程注册关闭子进程的方法
            $this->sonReload();
        });
        pcntl_signal(SIGUSR1,array($this,'sonSignalHandler'),false);
        pcntl_signal(SIGUSR2,array($this,'sonSignalHandler'),false);
        pcntl_signal(SIGINT,array($this,'sonSignalHandler'),false);
        while (true) {
            //调用已安装的信号信号处理器,为了检测是否有新的信号等待dispatching
            echo $this->dd->ee(). " I am child: ".getmypid(). " and i am running !".PHP_EOL;
            sleep(rand(3,6));
            pcntl_signal_dispatch();
        }

    }
    /****************业务子进程相关e******************/

    /****************监控子进程相关s******************/
    protected function monSignalHandler($sigo){
        var_dump('信号'.$sigo);
        switch ($sigo){
            case SIGUSR1://重启子进程
                echo $this->this_child_id."监控进程接受SIGUSR1".PHP_EOL;
//                $this->is_reload = true;
//                exit;
                break;
            case SIGUSR2://关闭子进程
                echo $this->this_child_id."监控进程接受SIGUSR2".PHP_EOL;
//                $this->is_reload = false;
//                exit;
                break;
            case SIGINT:
                echo $this->this_child_id."监控进程接受SIGINT,按下ctrl+c".PHP_EOL;
//                exit;
//                swoole_event_del($this->watch_fd);
                break;
        }
    }

    protected function reloadMonitor(){
        echo '监控子进程关闭'.PHP_EOL;
    }


    /**
     * 拉起监控子进程
     */
    protected function forkMonitor(){
        register_shutdown_function(function () {//只给父进程注册关闭子进程的方法
            $this->reloadMonitor();
        });
        pcntl_signal(SIGINT,array($this,'monSignalHandler'),false);
        $this->watch_files = inotify_init();
        $files = get_included_files();
        foreach ($files as $file){
            inotify_add_watch($this->watch_files,$file,IN_MODIFY);
        }

        stream_set_blocking($this->watch_files,0);//设置为阻塞模式
        while(1){
            sleep(2);
            pcntl_signal_dispatch();
            $event = inotify_read($this->watch_files);
            if(!empty($event)){
                echo '文件发生了变化';//如果发生了变化就得告知父进程,可以重启子进程了
                posix_kill($this->pid,SIGUSR1);
            }
        }
//        register_shutdown_function(function () {
//            Swoole\Event::wait();
//        });
//        swoole_event_add($this->watch_files,function($fd){
//            $event = inotify_read($fd);
//            if(!empty($event)){
//                echo '文件发生了变化';//如果发生了变化就得告知父进程,可以重启子进程了
//                posix_kill($this->pid,SIGUSR1);
//            }
//        });
//        Swoole\Event::wait();

    }
    /****************监控子进程相关e******************/
}

$work = new Worker();
$work->run();
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章