PHP进程退出信号_一文吃透 PHP 进程信号处理
背景
前兩周老大給安排了一個(gè)任務(wù),寫一個(gè)監(jiān)聽信號(hào)的包。因?yàn)槲宜镜捻?xiàng)目是運(yùn)行在容器里邊的,每次上線,需要重新打包鏡像,然后啟動(dòng)。在重新打包之前,Dokcer會(huì)先給容器發(fā)送一個(gè)信號(hào),然后等待一段超時(shí)時(shí)間(默認(rèn)10s)后,再發(fā)送SIGKILL信號(hào)來終止容器
現(xiàn)在有一種情況,容器中有一個(gè)常駐進(jìn)程,該常駐進(jìn)程的任務(wù)是不斷的消費(fèi)隊(duì)列里的消息。假設(shè)現(xiàn)在要上線,需要關(guān)殺掉容器,Docker給容器里跑的常駐進(jìn)程發(fā)送一個(gè)信號(hào),告訴它我10s后會(huì)將你關(guān)閉,假設(shè)現(xiàn)在已經(jīng)過了9秒,常駐進(jìn)程剛從隊(duì)列中取出一條消息,1s內(nèi)還沒將后續(xù)邏輯執(zhí)行完,進(jìn)程就已經(jīng)被殺了,此時(shí)這條消息就丟失了,且可能會(huì)產(chǎn)生臟數(shù)據(jù)
上邊就是這次任務(wù)的背景,需要通過監(jiān)聽信號(hào)來決定后續(xù)如何操作。對(duì)于上邊這種情況,當(dāng)常駐進(jìn)程收到Docker發(fā)送的關(guān)閉信號(hào)時(shí),將該進(jìn)程阻塞即可,一直sleep,直到殺掉容器。OK,清楚背景之后,下邊就介紹一下PHP中的信號(hào)(后邊會(huì)再整理一篇這個(gè)包如何寫,并將包發(fā)布到https://packagist.org/,供需要的小伙伴使用)
一、在Linux操作系統(tǒng)中有哪些信號(hào)
1、簡單介紹信號(hào)
信號(hào)是事件發(fā)生時(shí)對(duì)進(jìn)程的通知機(jī)制,有時(shí)又稱為軟件中斷。一個(gè)進(jìn)程可以向另一個(gè)進(jìn)程發(fā)送信號(hào),比如子進(jìn)程結(jié)束時(shí)都會(huì)向父進(jìn)程發(fā)送一個(gè)SIGCHLD(17號(hào)信號(hào))來通知父進(jìn)程,所以有時(shí)信號(hào)也被當(dāng)作一種進(jìn)程間通信的機(jī)制。
在linux系統(tǒng)下,通常我們使用 kill -9 XXPID來結(jié)束一個(gè)進(jìn)程,其實(shí)這個(gè)命令的實(shí)質(zhì)就是向某進(jìn)程發(fā)送SIGKILL(9號(hào)信號(hào)),對(duì)于在前臺(tái)進(jìn)程我們通常用Ctrl+c快捷鍵來結(jié)束運(yùn)行,該快捷鍵的實(shí)質(zhì)是向當(dāng)前進(jìn)程發(fā)送SIGINT(2號(hào)信號(hào)),而進(jìn)程收到該信號(hào)的默認(rèn)行為是結(jié)束運(yùn)行
2、常用信號(hào)
下邊這些信號(hào),可以使用kill -l命令進(jìn)行查看
下邊介紹幾個(gè)比較重要且常用的信號(hào):
信號(hào)名
信號(hào)值
信號(hào)類型
信號(hào)描述
SIGHUP
1
終止進(jìn)程(終端線路掛斷)
本信號(hào)在用戶終端連接(正常或非正常、結(jié)束時(shí)發(fā)出, 通常是在終端的控制進(jìn)程結(jié)束時(shí), 通知同一session內(nèi)的各個(gè)作業(yè), 這時(shí)它們與控制終端不再關(guān)聯(lián)
SIGQUIT
2
終止進(jìn)程(中斷進(jìn)程)
程序終止(interrupt、信號(hào), 在用戶鍵入INTR字符(通常是Ctrl-C、時(shí)發(fā)出
SIGQUIT
3
建立CORE文件終止進(jìn)程,并且生成CORE文件
進(jìn)程,并且生成CORE文件SIGQUIT 和SIGINT類似, 但由QUIT字符(通常是Ctrl-、來控制. 進(jìn)程在因收到SIGQUIT退出時(shí)會(huì)產(chǎn)生core文件, 在這個(gè)意義上類似于一個(gè)程序錯(cuò)誤信 號(hào)
SIGFPE
8
建立CORE文件(浮點(diǎn)異常)
SIGFPE 在發(fā)生致命的算術(shù)運(yùn)算錯(cuò)誤時(shí)發(fā)出. 不僅包括浮點(diǎn)運(yùn)算錯(cuò)誤, 還包括溢 出及除數(shù)為0等其它所有的算術(shù)的錯(cuò)誤
SIGKILL
9
終止進(jìn)程(殺死進(jìn)程)
SIGKILL 用來立即結(jié)束程序的運(yùn)行. 本信號(hào)不能被阻塞, 處理和忽略
SIGSEGV
11
SIGSEGV 試圖訪問未分配給自己的內(nèi)存, 或試圖往沒有寫權(quán)限的內(nèi)存地址寫數(shù)據(jù)
SIGALRM
14
終止進(jìn)程(計(jì)時(shí)器到時(shí))
SIGALRM 時(shí)鐘定時(shí)信號(hào), 計(jì)算的是實(shí)際的時(shí)間或時(shí)鐘時(shí)間. alarm函數(shù)使用該信號(hào)
SIGTERM
15
終止進(jìn)程(軟件終止信號(hào))
SIGTERM 程序結(jié)束(terminate、信號(hào), 與SIGKILL不同的是該信號(hào)可以被阻塞和處理. 通常用來要求程序自己正常退出. shell命令kill缺省產(chǎn)生這個(gè)信號(hào)
SIGCHLD
17
忽略信號(hào)(當(dāng)子進(jìn)程停止或退出時(shí)通知父進(jìn)程)
SIGCHLD 子進(jìn)程結(jié)束時(shí), 父進(jìn)程會(huì)收到這個(gè)信號(hào)
SIGVTALRM
26
終止進(jìn)程(虛擬計(jì)時(shí)器到時(shí))
SIGVTALRM 虛擬時(shí)鐘信號(hào). 類似于SIGALRM, 但是計(jì)算的是該進(jìn)程占用的CPU時(shí)間
SIGIO
29
忽略信號(hào)(描述符上可以進(jìn)行I/O)
SIGIO 文件描述符準(zhǔn)備就緒, 可以開始進(jìn)行輸入/輸出操作
二、PHP中處理信號(hào)相關(guān)函數(shù)
PHP的pcntl擴(kuò)展以及posix擴(kuò)展為我們提供了若干操作信號(hào)的方法(若想使用這些函數(shù),需要先安裝這幾個(gè)擴(kuò)展)
下邊具體介紹幾個(gè)我在本次任務(wù)中用到的方法:
declare
declare結(jié)構(gòu)用來設(shè)定一段代碼的執(zhí)行指令。declare的語法和其它流程控制結(jié)構(gòu)相似
declare?(directive)
statement
復(fù)制代碼
directive部分允許設(shè)定declare代碼段的行為。目前只認(rèn)識(shí)兩個(gè)指令:ticks和encoding。declare代碼段中的 statement部分將被執(zhí)行——怎樣執(zhí)行以及執(zhí)行中有什么副作用出現(xiàn)取決于directive中設(shè)定的指令
Ticks
Tick(時(shí)鐘周期)是一個(gè)在declare代碼段中解釋器每執(zhí)行N條可計(jì)時(shí)的低級(jí)語句就會(huì)發(fā)生的事件N的值是在declare 中的directive部分用ticks=N來指定的。不是所有語句都可計(jì)時(shí)。通常條件表達(dá)式和參數(shù)表達(dá)式都不可計(jì)時(shí)。在每個(gè)tick中出現(xiàn)的事件是由register_tick_function()來指定的,注意每個(gè) tick 中可以出現(xiàn)多個(gè)事件
更詳細(xì)的內(nèi)容,可查看官方文檔:https://www.php.net/manual/zh/control-structures.declare.php
declare(ticks=1);//每執(zhí)行一條時(shí),觸發(fā)register_tick_function()注冊(cè)的函數(shù)
$a=1;//在注冊(cè)之前,不算
function?test(){//定義一個(gè)函數(shù)
echo?"執(zhí)行\(zhòng)n";
}
register_tick_function('test');//該條注冊(cè)函數(shù)會(huì)被當(dāng)成低級(jí)語句被執(zhí)行
for($i=0;$i<=2;$i++){//for算一條低級(jí)語句
$i=$i;//賦值算一條
}
輸出:六個(gè)“執(zhí)行”
復(fù)制代碼
pcntl_signal
pcntl_signal,安裝一個(gè)信號(hào)處理器
pcntl_signal?(?int?$signo?,?callback?$handler?[,?bool?$restart_syscalls?=?true?]?)?:?bool
復(fù)制代碼
函數(shù)pcntl_signal()為signo指定的信號(hào)安裝一個(gè)新的信號(hào)處理器
declare(ticks?=?1);
pcntl_signal(SIGINT,function(){
echo?"你按了Ctrl+C".PHP_EOL;
});
while(1){
sleep(1);//死循環(huán)運(yùn)行低級(jí)語句
}
輸出:當(dāng)按Ctrl+C之后,會(huì)輸出“你按了Ctrl+C”
復(fù)制代碼
posix_kill
posix_kill,向進(jìn)程發(fā)送一個(gè)信號(hào)
posix_kill?(?int?$pid?,?int?$sig?)?:?bool
復(fù)制代碼
第一個(gè)參數(shù)為進(jìn)程ID,第二個(gè)參數(shù)為你要發(fā)送的信號(hào)
a.php
declare(ticks?=?1);
echo?getmypid();//獲取當(dāng)前進(jìn)程id
pcntl_signal(SIGINT,function(){
echo?"你給我發(fā)了SIGINT信號(hào)";
});
while(1){
sleep(1);
}
b.php
posix_kill(執(zhí)行1.php時(shí)輸出的進(jìn)程id,?SIGINT);
復(fù)制代碼
pcntl_signal_dispatch
pcntl_signal_dispatch,調(diào)用等待信號(hào)的處理器
pcntl_signal_dispatch?(?void?)?:?bool
復(fù)制代碼
函數(shù)pcntl_signal_dispatch()調(diào)用每個(gè)等待信號(hào)通過pcntl_signal()安裝的處理器
echo?"安裝信號(hào)處理器...\n";
pcntl_signal(SIGHUP,??function($signo)?{
echo?"信號(hào)處理器被調(diào)用\n";
});
echo?"為自己生成SIGHUP信號(hào)...\n";
posix_kill(posix_getpid(),?SIGHUP);
echo?"分發(fā)...\n";
pcntl_signal_dispatch();
echo?"完成\n";
?>
輸出:
安裝信號(hào)處理器...
為自己生成SIGHUP信號(hào)...
分發(fā)...
信號(hào)處理器被調(diào)用
完成
復(fù)制代碼
pcntl_async_signals()
異步信號(hào)處理,用于啟用無需 ticks (這會(huì)帶來很多額外的開銷)的異步信號(hào)處理。(PHP>=7.1)
pcntl_async_signals(true);?//?turn?on?async?signals
pcntl_signal(SIGHUP,??function($sig)?{
echo?"SIGHUP\n";
});
posix_kill(posix_getpid(),?SIGHUP);
輸出:
SIGHUP
復(fù)制代碼
三、PHP中處理信號(hào)量的方式
前邊我們知道我們可以通過declare(ticks=1)和pcntl_signal組合的方式監(jiān)聽信號(hào),即每一條PHP低級(jí)語句,就會(huì)檢查一次當(dāng)前進(jìn)程是否有未處理的信號(hào),這其實(shí)是十分耗性能的。
pcntl_signal的實(shí)現(xiàn)原理是,觸發(fā)信號(hào)后先將信號(hào)加入一個(gè)隊(duì)列中。然后在PHP的ticks回調(diào)函數(shù)中不斷檢查是否有信號(hào),如果有信號(hào)就執(zhí)行PHP中指定的回調(diào)函數(shù),如果沒有則跳出函數(shù)。
PHP_MINIT_FUNCTION(pcntl)
{
php_register_signal_constants(INIT_FUNC_ARGS_PASSTHRU);
php_pcntl_register_errno_constants(INIT_FUNC_ARGS_PASSTHRU);
php_add_tick_function(pcntl_signal_dispatch?TSRMLS_CC);
return?SUCCESS;
}
復(fù)制代碼
在PHP5.3之后,有了pcntl_signal_dispatch函數(shù)。這個(gè)時(shí)候?qū)⒉辉谛枰猟eclare,只需要在循環(huán)中增加該函數(shù),就可以調(diào)用信號(hào)通過了:
echo?getmypid();//獲取當(dāng)前進(jìn)程id
pcntl_signal(SIGUSR1,function(){
echo?"觸發(fā)信號(hào)用戶自定義信號(hào)1";
});
while(1){
pcntl_signal_dispatch();
sleep(1);//死循環(huán)運(yùn)行低級(jí)語句
}
復(fù)制代碼
大家都知道PHP的ticks=1表示每執(zhí)行1行PHP代碼就回調(diào)此函數(shù)。實(shí)際上大部分時(shí)間都沒有信號(hào)產(chǎn)生,但ticks的函數(shù)一直會(huì)執(zhí)行。如果一個(gè)服務(wù)器程序1秒中接收1000次請(qǐng)求,平均每個(gè)請(qǐng)求要執(zhí)行1000行PHP代碼。那么PHP的pcntl_signal,就帶來了額外的 1000 * 1000,也就是100萬次空的函數(shù)調(diào)用。這樣會(huì)浪費(fèi)大量的CPU資源。比較好的做法是去掉ticks,轉(zhuǎn)而使用pcntl_signal_dispatch,在代碼循環(huán)中自行處理信號(hào)。
pcntl_signal_dispatch 函數(shù)的實(shí)現(xiàn):
void?pcntl_signal_dispatch()
{
//....?這里略去一部分代碼,queue即是信號(hào)隊(duì)列
while?(queue)?{
if?((handle?=?zend_hash_index_find(&PCNTL_G(php_signal_table),?queue->signo))?!=?NULL)?{
ZVAL_NULL(&retval);
ZVAL_LONG(¶m,?queue->signo);
/*?Call?php?signal?handler?-?Note?that?we?do?not?report?errors,?and?we?ignore?the?return?value?*/
/*?FIXME:?this?is?probably?broken?when?multiple?signals?are?handled?in?this?while?loop?(retval)?*/
call_user_function(EG(function_table),?NULL,?handle,?&retval,?1,?¶m?TSRMLS_CC);
zval_ptr_dtor(¶m);
zval_ptr_dtor(&retval);
}
next?=?queue->next;
queue->next?=?PCNTL_G(spares);
PCNTL_G(spares)?=?queue;
queue?=?next;
}
}
復(fù)制代碼
但是上邊這種,也有個(gè)惡心的地方就是,它得放在死循環(huán)中。PHP7.1之后出來了一個(gè)完成異步的信號(hào)接收并處理的函數(shù):
pcntl_async_signals
//a.php
echo?getmypid();
pcntl_async_signals(true);//開啟異步監(jiān)聽信號(hào)
pcntl_signal(SIGUSR1,function(){
echo?"觸發(fā)信號(hào)";
posix_kill(getmypid(),SIGSTOP);
});
posix_kill(getmypid(),SIGSTOP);//給進(jìn)程發(fā)送暫停信號(hào)
//b.php
posix_kill(文件1進(jìn)程,?SIGCONT);//給進(jìn)程發(fā)送繼續(xù)信號(hào)
posix_kill(文件1進(jìn)程,?SIGUSR1);//給進(jìn)程發(fā)送user1信號(hào)
復(fù)制代碼
通過pcntl_async_signals方法,就不用再寫死循環(huán)了。
監(jiān)聽信號(hào)的包:
https://github.com/Rain-Life/monitorSignal
復(fù)制代碼
總結(jié)
以上是生活随笔為你收集整理的PHP进程退出信号_一文吃透 PHP 进程信号处理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: php 字符串进行计算_怎么在php中利
- 下一篇: php fpm工作原理,什么是phpfp