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();
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章