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