PHP平滑關閉/重啟的實現方法
前言
寫過 CLI 常駐進程的老司機肯定遇到過這麼一個問題:在需要更新程序的時候,我要怎樣才能安全關閉老進程?你可能會想到 NGINX、php-fpm 之類的平滑重啟是給進程發送 USR2 信號,然後它就會將當前請求處理完再退出。
但進程是怎樣接收信號、處理信號,估計就不是很多人能說清楚瞭。
原理
要實現平滑關閉/重啟不難,這裡先講解兩個知識點:
阻塞信號
當我們的程序正在處理一個任務的時候,你肯定不希望它中途被終止,比如說你在執行一個數據庫事務,肯定不希望事務還沒被提交進程就被終止瞭。
<?php echo "開始執行事務" . PHP_EOL; // 模擬一些耗時的操作 $finish_time = time() + 5; while (time() < $finish_time) { } echo "事務執行完畢" . PHP_EOL;
上面這段代碼,如果你在第二個 echo 之前用 kill 命令去殺死這個進程,那麼第二個 echo 就不會被執行瞭。那能不能做到在事務過程中暫時先忽略 kill 信號呢?
能。我們可以使用 pcntl_sigprocmask() 來阻塞信號,讓事務完成之後再響應 kill 信號。
<?php // 阻塞信號 $sig_set = array(SIGINT, SIGTERM); // 要阻塞的信號集合 pcntl_sigprocmask(SIG_BLOCK, $sig_set); // SIG_BLOCK: 把信號加入到當前阻塞信號中 echo date("[Y-m-d H:i:s]") . " 開始執行事務" . PHP_EOL; $finish_time = time() + 5; while (time() < $finish_time) { } echo date("[Y-m-d H:i:s]") . "事務執行完畢" . PHP_EOL; pcntl_sigprocmask(SIG_UNBLOCK, $sig_set); // SIG_UNBLOCK: 從當前阻塞信號中移出信號
同樣的,在第二個 echo 之前按下 Ctrl + C 或者用 kill 命令去殺這個進程,你會發現第二個 echo 正常執行瞭,並且兩條輸出的時間間隔是 5 秒。
我們的常駐進程通常是在一個 while(true) 循環中去執行重復的任務,如果這麼寫的話:
<?php while (true) { pcntl_sigprocmask(SIG_BLOCK, $sig_set); // ... pcntl_sigprocmask(SIG_UNBLOCK, $sig_set); }
我們是可以保證一個事務不會被打斷,但是我們的程序還不知道是不是已經接收到信號瞭,並且把阻塞信號移除之後進程立刻就退出瞭,沒辦法去做一些收尾工作(比如關閉文件)。
處理信號
為瞭解決上面提到的問題,我們需要在信號發生的時候去做收尾工作,然後再退出進程。
pcntl 擴展提供瞭一些信號相關的函數,我們可以使用 pcntl_signal() 和 pcntl_signal_dispatch() 來註冊信號處理器和分發信號。
<?php $sig_handler = function ($signo) { echo "收到信號 {$signo}" . PHP_EOL; }; pcntl_signal(SIGINT, $sig_handler); // 給 SIGINT 信號註冊一個處理器 // 模擬耗時操作 echo "開始執行事務" . PHP_EOL; $finish_time = time() + 5; while(true) { if (time() > $finish_time) { echo "事務執行完畢" . PHP_EOL; break; } } pcntl_signal_dispatch(); // 分發信號
執行上面這段代碼並在 5 秒內按下 Ctrl + C,你會看到 sig_handler 被執行瞭;而如果不按下 Ctrl + C,那麼 sig_handler 就不會被執行。
到這裡你應該已經理解瞭 pcntl_signal() 和 pcntl_signal_dispatch() 的用法瞭,把它放到到剛剛的代碼試試
<?php $sig_handler = function ($signo) { echo "收到信號 {$signo}" . PHP_EOL; }; $sig_set = array(SIGINT, SIGTERM); foreach ($sig_set as $sig) { pcntl_signal($sig, $sig_handler); // 註冊多個信號 } // [1] while (true) { // [2-1] pcntl_sigprocmask(SIG_BLOCK, $sig_set); // [2-2] // ... // [2-3] pcntl_sigprocmask(SIG_UNBLOCK, $sig_set); // [2-4] } // [3]
pcntl_signal_dispatch() 該放哪裡呢?是 [1] [2] 還是 [3]?先動手試一下
然後你會發現,隻有放在 [2] 才能讓信號處理器執行。同時這個實驗也告訴我們 pcntl_signal_dispatch() 要在信號發生後才會使處理器執行:放在 [1] 時,除非你手速足夠快,不然在你按下 Ctrl + C 或者是 kill 之前就已經執行過瞭;而放在 [3] 它就永遠沒機會執行。
至於放在 [2] 的哪個位置,我建議是放在 [2-4],因為這個時候已經處理完任務瞭。
拼起來
到這裡你已經瞭解平滑關閉/重啟的原理瞭,我們把上面的半成品代碼(因為在收到信號後可能還會進入下一層循環)整理一下:
<?php $running = true; $sig_handler = function ($signo) use (&$running) { echo "收到信號 {$signo}" . PHP_EOL; // 做收尾工作 $running = false; }; $sig_set = array(SIGINT, SIGTERM, SIGUSR2 /* 熟悉的 USR2 信號不能漏 */); foreach ($sig_set as $sig) { pcntl_signal($sig, $sig_handler); // 註冊多個信號 } while ($running) { pcntl_sigprocmask(SIG_BLOCK, $sig_set); // ... 業務邏輯 pcntl_sigprocmask(SIG_UNBLOCK, $sig_set); pcntl_signal_dispatch(); }
我們就得到瞭一個可以平滑程序的常駐進程框架,你也可以把它封裝成一個類。
思考
細心的你可能會發現,上面這段代碼如果業務邏輯出現瞭死循環,還是沒辦法退出,那麼我們能不能設置個超時強制開始處理收尾工作然後退出進程呢?
到此這篇關於PHP平滑關閉/重啟實現的文章就介紹到這瞭,更多相關PHP平滑關閉重啟內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!