PHP平滑關(guān)閉/重啟的實(shí)現(xiàn)方法
前言
寫(xiě)過(guò) CLI 常駐進(jìn)程的老司機(jī)肯定遇到過(guò)這么一個(gè)問(wèn)題:在需要更新程序的時(shí)候,我要怎樣才能安全關(guān)閉老進(jìn)程?你可能會(huì)想到 NGINX、php-fpm 之類(lèi)的平滑重啟是給進(jìn)程發(fā)送 USR2 信號(hào),然后它就會(huì)將當(dāng)前請(qǐng)求處理完再退出。
但進(jìn)程是怎樣接收信號(hào)、處理信號(hào),估計(jì)就不是很多人能說(shuō)清楚了。
原理
要實(shí)現(xiàn)平滑關(guān)閉/重啟不難,這里先講解兩個(gè)知識(shí)點(diǎn):
阻塞信號(hào)
當(dāng)我們的程序正在處理一個(gè)任務(wù)的時(shí)候,你肯定不希望它中途被終止,比如說(shuō)你在執(zhí)行一個(gè)數(shù)據(jù)庫(kù)事務(wù),肯定不希望事務(wù)還沒(méi)被提交進(jìn)程就被終止了。
<?php
echo "開(kāi)始執(zhí)行事務(wù)" . PHP_EOL;
// 模擬一些耗時(shí)的操作
$finish_time = time() + 5;
while (time() < $finish_time) {
}
echo "事務(wù)執(zhí)行完畢" . PHP_EOL;上面這段代碼,如果你在第二個(gè) echo 之前用 kill 命令去殺死這個(gè)進(jìn)程,那么第二個(gè) echo 就不會(huì)被執(zhí)行了。那能不能做到在事務(wù)過(guò)程中暫時(shí)先忽略 kill 信號(hào)呢?
能。我們可以使用 pcntl_sigprocmask() 來(lái)阻塞信號(hào),讓事務(wù)完成之后再響應(yīng) kill 信號(hào)。
<?php
// 阻塞信號(hào)
$sig_set = array(SIGINT, SIGTERM); // 要阻塞的信號(hào)集合
pcntl_sigprocmask(SIG_BLOCK, $sig_set); // SIG_BLOCK: 把信號(hào)加入到當(dāng)前阻塞信號(hào)中
echo date("[Y-m-d H:i:s]") . " 開(kāi)始執(zhí)行事務(wù)" . PHP_EOL;
$finish_time = time() + 5;
while (time() < $finish_time) {
}
echo date("[Y-m-d H:i:s]") . "事務(wù)執(zhí)行完畢" . PHP_EOL;
pcntl_sigprocmask(SIG_UNBLOCK, $sig_set); // SIG_UNBLOCK: 從當(dāng)前阻塞信號(hào)中移出信號(hào)同樣的,在第二個(gè) echo 之前按下 Ctrl + C 或者用 kill 命令去殺這個(gè)進(jìn)程,你會(huì)發(fā)現(xiàn)第二個(gè) echo 正常執(zhí)行了,并且兩條輸出的時(shí)間間隔是 5 秒。
我們的常駐進(jìn)程通常是在一個(gè) while(true) 循環(huán)中去執(zhí)行重復(fù)的任務(wù),如果這么寫(xiě)的話(huà):
<?php
while (true) {
pcntl_sigprocmask(SIG_BLOCK, $sig_set);
// ...
pcntl_sigprocmask(SIG_UNBLOCK, $sig_set);
}我們是可以保證一個(gè)事務(wù)不會(huì)被打斷,但是我們的程序還不知道是不是已經(jīng)接收到信號(hào)了,并且把阻塞信號(hào)移除之后進(jìn)程立刻就退出了,沒(méi)辦法去做一些收尾工作(比如關(guān)閉文件)。
處理信號(hào)
為了解決上面提到的問(wèn)題,我們需要在信號(hào)發(fā)生的時(shí)候去做收尾工作,然后再退出進(jìn)程。
pcntl 擴(kuò)展提供了一些信號(hào)相關(guān)的函數(shù),我們可以使用 pcntl_signal() 和 pcntl_signal_dispatch() 來(lái)注冊(cè)信號(hào)處理器和分發(fā)信號(hào)。
<?php
$sig_handler = function ($signo) {
echo "收到信號(hào) {$signo}" . PHP_EOL;
};
pcntl_signal(SIGINT, $sig_handler); // 給 SIGINT 信號(hào)注冊(cè)一個(gè)處理器
// 模擬耗時(shí)操作
echo "開(kāi)始執(zhí)行事務(wù)" . PHP_EOL;
$finish_time = time() + 5;
while(true) {
if (time() > $finish_time) {
echo "事務(wù)執(zhí)行完畢" . PHP_EOL;
break;
}
}
pcntl_signal_dispatch(); // 分發(fā)信號(hào)執(zhí)行上面這段代碼并在 5 秒內(nèi)按下 Ctrl + C,你會(huì)看到 sig_handler 被執(zhí)行了;而如果不按下 Ctrl + C,那么 sig_handler 就不會(huì)被執(zhí)行。
到這里你應(yīng)該已經(jīng)理解了 pcntl_signal() 和 pcntl_signal_dispatch() 的用法了,把它放到到剛剛的代碼試試
<?php
$sig_handler = function ($signo) {
echo "收到信號(hào) {$signo}" . PHP_EOL;
};
$sig_set = array(SIGINT, SIGTERM);
foreach ($sig_set as $sig) {
pcntl_signal($sig, $sig_handler); // 注冊(cè)多個(gè)信號(hào)
}
// [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]?先動(dòng)手試一下
然后你會(huì)發(fā)現(xiàn),只有放在 [2] 才能讓信號(hào)處理器執(zhí)行。同時(shí)這個(gè)實(shí)驗(yàn)也告訴我們 pcntl_signal_dispatch() 要在信號(hào)發(fā)生后才會(huì)使處理器執(zhí)行:放在 [1] 時(shí),除非你手速足夠快,不然在你按下 Ctrl + C 或者是 kill 之前就已經(jīng)執(zhí)行過(guò)了;而放在 [3] 它就永遠(yuǎn)沒(méi)機(jī)會(huì)執(zhí)行。
至于放在 [2] 的哪個(gè)位置,我建議是放在 [2-4],因?yàn)檫@個(gè)時(shí)候已經(jīng)處理完任務(wù)了。
拼起來(lái)
到這里你已經(jīng)了解平滑關(guān)閉/重啟的原理了,我們把上面的半成品代碼(因?yàn)樵谑盏叫盘?hào)后可能還會(huì)進(jìn)入下一層循環(huán))整理一下:
<?php
$running = true;
$sig_handler = function ($signo) use (&$running) {
echo "收到信號(hào) {$signo}" . PHP_EOL;
// 做收尾工作
$running = false;
};
$sig_set = array(SIGINT, SIGTERM, SIGUSR2 /* 熟悉的 USR2 信號(hào)不能漏 */);
foreach ($sig_set as $sig) {
pcntl_signal($sig, $sig_handler); // 注冊(cè)多個(gè)信號(hào)
}
while ($running) {
pcntl_sigprocmask(SIG_BLOCK, $sig_set);
// ... 業(yè)務(wù)邏輯
pcntl_sigprocmask(SIG_UNBLOCK, $sig_set);
pcntl_signal_dispatch();
}我們就得到了一個(gè)可以平滑程序的常駐進(jìn)程框架,你也可以把它封裝成一個(gè)類(lèi)。
思考
細(xì)心的你可能會(huì)發(fā)現(xiàn),上面這段代碼如果業(yè)務(wù)邏輯出現(xiàn)了死循環(huán),還是沒(méi)辦法退出,那么我們能不能設(shè)置個(gè)超時(shí)強(qiáng)制開(kāi)始處理收尾工作然后退出進(jìn)程呢?
到此這篇關(guān)于PHP平滑關(guān)閉/重啟實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)PHP平滑關(guān)閉重啟內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
深入解讀php中關(guān)于抽象(abstract)類(lèi)和抽象方法的問(wèn)題分析
這篇文章主要介紹了php中關(guān)于抽象(abstract)類(lèi)和抽象方法的問(wèn)題分析,有需要的朋友可以參考一下2014-01-01
php實(shí)現(xiàn)將字符串按照指定距離進(jìn)行分割的方法
這篇文章主要介紹了php實(shí)現(xiàn)將字符串按照指定距離進(jìn)行分割的方法,涉及字符串操作的技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-03-03
PHP中將一個(gè)字符串部分字符用星號(hào)*替代隱藏的實(shí)現(xiàn)代碼
這篇文章主要介紹了PHP中將一個(gè)字符串部分字符用星號(hào)*替代隱藏的實(shí)現(xiàn)代碼,有時(shí)候我們需要將部分內(nèi)容隱藏那么就可能需要下面的代碼了,需要的朋友可以參考下2019-09-09
PHP中使用break跳出多重循環(huán)代碼實(shí)例
這篇文章主要介紹了PHP中使用break跳出多重循環(huán)代碼實(shí)例,本文直接給出代碼,代碼簡(jiǎn)潔易一懂,一看就明白了,需要的朋友可以參考下2015-01-01
PHP數(shù)組游標(biāo)實(shí)現(xiàn)對(duì)數(shù)組的各種操作詳解
這篇文章主要介紹了PHP數(shù)組游標(biāo)實(shí)現(xiàn)對(duì)數(shù)組的各種操作,結(jié)合實(shí)例形式較為詳細(xì)的分析了PHP數(shù)組操作中current與next方法控制數(shù)組游標(biāo)移動(dòng)實(shí)現(xiàn)數(shù)組遍歷的技巧,需要的朋友可以參考下2016-01-01

