Perl訊號處理
訊號處理
作業系統可以通過訊號(signal)處理機制來實現一些功能:程式註冊好待監視的訊號處理機制,在程式執行過程中如果產生了對應的訊號,則會按照註冊好的處理方式進行處理。
signal基礎
每個程序都記錄了一個訊號(signal)索引表,並註冊了各種訊號的處理方式,每當收到訊號的時候,會立即停止執行操作並處理對應的訊號。
絕大多數訊號都有預設處理機制,但Perl支援使用者自己重新定義接收到訊號時的處理方式。在Perl中,訊號處理的方式註冊在一個hash變數%SIG
中,key為訊號的名稱,value有幾種可能的值:
- DEFAULT或undef :表示採取所接收訊號的預設處理方式
- IGNORE :表示忽略接收到的該訊號
-
子程式引用
:如
\&subref
或匿名子程式sub { codeblock }
,表示接收到該訊號時,執行該子程式 - 子程式:強烈建議不使用該類值
要想檢視支援的訊號,可以遍歷一下%SIG
,或者直接在Linux下使用kill -l
命令:
$ perl -le 'print join qq/ /, sort keys %SIG'
要檢視訊號對應的數值,可以去Config的sig_name裡查詢:
#!/usr/bin/perl use strict; use warnings; use Config; my @signals = split ' ', $Config{sig_name}; for (0..$#signals){ print "$_ $signals \n" unless $signals[$_] =~ /^NUM/; }
記住幾個常見的即可(數值|KEY|NAME):
-
0 | ZERO | SIGZERO
:檢查程序是否存在 -
1 | HUP | SIGHUP
:傳送HUP訊號給終端來終止終端上的所有程序(終端的子程序),對daemon類程式還常重新定義該訊號用來重新載入配置檔案並reload服務 -
2 | INT | SIGINT
:中斷程序,可被捕捉和忽略,幾乎等同於sigterm,所以也會盡可能的釋放執行clean-up,釋放資源,儲存狀態等(CTRL+C) -
3 | QUIT | SIGQUIT
:從鍵盤發出殺死(終止)程序的訊號,優先順序較高,可能還會發出core dump行為 -
9 | KILL | SIGKILL
:強制終止程序,該訊號不可被捕捉。該訊號是人為強制終止,而不是讓作業系統核心去終止程序,所以程序收到該訊號後不會執行任何clean-up行為,所以資源不會釋放,狀態不會儲存 -
10 | USR1 | SIGUSR1
:使用者自定義訊號1 -
12 | USR2 | SIGUSR2
:使用者自定義訊號2 -
13 | PIPE | SIGPIPE
:已關閉的管道。當正在讀的、或正在寫入的管道已被對方關閉時,將觸發該訊號 -
14 | ALRM | SIGALRM
:alarm訊號,噹噹前程序的alarm計時器(alarm定時器即一個定時器)到期了,將觸發該訊號。在Microsoft系統上未實現該訊號 -
15 | TERM | SIGTERM
:殺死(終止)程序,可被捕捉和忽略,幾乎等同於sigint訊號,會盡可能的釋放執行clean-up,釋放資源,儲存狀態等,優先順序高於INT,但低於QUIT和KILL -
17 | CHLD | SIGCHLD
:當子程序中斷或退出時,傳送該訊號告知父程序自己已完成,父程序收到訊號將告知核心清理程序列表。所以該訊號可以解除殭屍程序,也可以讓非正常退出的程序工作得以正常的clean-up,釋放資源,儲存狀態等 -
18 | CONT | SIGCONT
:傳送此訊號使得stopped程序進入running,該訊號主要用於jobs,例如bg & fg 都會發送該訊號。可以直接傳送此訊號給stopped程序使其執行起來 -
19 | STOP | SIGSTOP
:該訊號是不可被捕捉和忽略的程序停止資訊,收到訊號後會進入stopped狀態,直到接收到CONT訊號後才繼續執行 -
20 | TSTP | SIGTSTP
:該訊號是可被忽略的程序停止訊號(CTRL+Z) -
28 | WINCH | SIGWINCH
:程序所在的控制終端或控制視窗大小發生了改變(例如拉大拉小圖形介面程式的框框)會發送該訊號。對於後臺程序,由於沒有視窗的概念,常常重新定義該訊號用來實現graceful stop -
29 | IO | SIGIO
:非同步IO事件。如果檔案控制代碼設定為非同步IO(即O_ASYNC),當該檔案控制代碼中產生了任何事件(例如可寫事件)時都會發送該訊號
安全的訊號
需要注意的是,對於具有安全訊號處理機制的語言(不止是Perl),需要保證在執行一條語句(嚴格地說是opcode)的時候不會被作業系統的訊號處理機制中斷,只有在當前正在處理的語句結束後,才會中斷 。
例如,在Perl進行IO的時候,訊號不會終止正在進行的IO操作,而是在這次IO完成後再終止。再例如,正在執行排序操作的時候,不會在排序的過程中終止,而是當前排序過程完成後再終止。
安全的訊號機制優點很明顯,它可以讓程式更加健壯。但是缺點也很明顯,因為有些操作可能會花費比較長的時間,然後才終止程序。當然,大多數時候這個缺點並不是什麼大問題,但是有些情況下對時間長短的控制要求非常精確(比如反導彈系統,必須在一個很短的時間內計算出一些資料,這種程式很可能會直接定製作業系統實現特殊的功能),這樣的情況就不適合使用這種安全的訊號處理機制。
從Perl 5.8開始,Perl就預設使用safe模式的訊號處理機制。如果想要在Perl上使用非安全的訊號處理機制,需要設定環境變數PERL_SIGNALS=unsafe
。
訊號處理
前面說過,要想定製訊號處理方式,只需在%SIG
中註冊對應的value即可。其中value有幾種可能的值:
- DEFAULT或undef :表示採取所接收訊號的預設處理方式
- IGNORE :表示忽略接收到的該訊號
-
子程式引用
:如
\&subref
或匿名子程式sub { codeblock }
,表示接收到該訊號時,執行該子程式 - 子程式:強烈建議不使用該類值
注意,自定義訊號處理方式,對於無法捕獲的訊號無影響,如SIGKILL訊號是不可被捕捉的訊號。
例如,忽略INT訊號,使得CTRL+C無效:
$SIG{INT}='IGNORE';
以下是一個完整的perl示例:
#!/usr/bin/env perl use strict; use warnings; $SIG{INT} = 'IGNORE'; for (1..3){ print "hello $_\n"; sleep 2; }
執行這個perl程式的時候,按下ctrl + c將無法終止程式,而是正常執行完。
再例如,設定alarm訊號為預設值'DEFAULT',alarm訊號的預設處理機制是終止呼叫alarm的程序。
$SIG{ALRM} = 'DEFAULT';
設定訊號的處理方式為一個自定義的子程式:
$SIG{USR1} = \&usr1handler;
注意使用的是子程式引用,不要直接使用子程式
。實際上,如果%SIG
的value部分,如果不是子程式引用,也不是'DEFAULT'或IGNORE
,其它字串都表示以main包(不是當前包)的該子程式作為訊號處理方式。例如:
$SIG{USR1} = 'DEFLT';
等價於:
$SIG{USR1} = \&main::DEFLT;
而很多時候,這個子程式是不存在的。所以,請注意value部分的拼寫。
還可以直接定義一個匿名子程式作為訊號處理的值。例如,收到INT訊號時,清理一些臨時檔案(如pid檔案):
$SIG{INT} = sub { warn "received SIGINT, removing PID file and exiting.\n"; unlink "/var/run/perlapp.pid"; exit 0; };
正常的%SIG
寫法註冊訊號時,一次只能註冊一個訊號:
$SIG{INT} = \&handler;
但可以通過下面的方式一次性註冊多個訊號處理方式:
%SIG = (%SIG, INT => IGNORE, PIPE => \&handler, HUP => \&handler);
之所以能這麼展開,是因為Perl在列表上下文會將列表、陣列、hash(它們本質上都是列表)壓扁展開,所以括號中的%SIG
會展開成一個列表,然後重新定義了INT、PIPE、HUP訊號的值,由於hash型別的key必須是唯一的,所以重新定義的key的值會覆蓋已有的值。
die和warn的訊號處理
Perl除了支援訊號處理機制,還支援錯誤處理,特別是die和warn這兩個行為(以及Carp模組中對應的crap和croak)。
$SIG{__WARN__} = \&yoursub; $SIG{__DIE__} = \&yoursub;
這些並不是真的訊號,而是偽訊號,Perl提供偽訊號處理機制讓我們定製一些事件的處理方式。在%SIG
中並沒有為這些偽訊號設定預設值,所以如果需要設定偽訊號的事件處理,需要手動設定,正如上面設定的方式。
上面的字首和字尾雙下劃線是可選的,只是為了讓偽訊號和真訊號進行區分。當然,Perl並不允許我們在%SIG
中隨意建立訊號名。
寫一個訊號處理子程式
如果某個訊號的所註冊的是一個子程式引用,那麼在接收到這個訊號的時候,會呼叫這個子程式,並傳遞訊號的名稱作為引數給子程式。
例如:
#!/usr/bin/perl use strict; use warnings; sub handler { my $sig = shift; print "Caught SIGNAL: $sig\n"; } $SIG{INT} = \&handler; for (1..3){ sleep 2; }
有些作業系統(特別是BSD系統)會在呼叫一次子程式後登出訊號處理子程式,所以要想繼續註冊該訊號的處理方式,可以在子程式中的開頭(在開頭加是為了避免訊號觸發後子程式呼叫過程中有新的訊號進來)加上重新安裝子程式的語句:
sub handler{ $sig = shift; # reinstall handler $SIG{$sig} = \&handler; ... ...其它程式碼... ... }
很多時候,並不希望正在處理某個訊號的時候再次接收該訊號(因為這個時候接收同樣的訊號是多餘的行為),這時可以在子程式的開頭將訊號處理設定為"IGNORE"來忽略可能的新訊號,再在子程式的結尾設定回原來的訊號處理方式。
下面的程式碼展示了這種處理邏輯:
sub handler { $SIG{$_[0]} = 'IGNORE'; ... do something ... $SIG{$_[0]} = \&handler; }
或者,更簡便的方式是使用local
關鍵字來修飾%SIG
中對應的訊號:
sub handler { local $SIG{$_[0]} = 'IGNORE'; ... do something ... }
local關鍵字是在區域性範圍內操作全域性變數,在退出範圍時恢復全域性變數。所以,上面的程式碼中,只有在handler函式內部臨時設定了訊號處理方式為"IGNORE",退出子程式後又恢復原來的訊號處理方式。
糟糕的訊號處理子程式
其實訊號處理機制中隱含了一個關鍵點:強烈建議不要在訊號處理程式中分配新記憶體 。例如,新建一個變數儲存某個值。
例如,下面的示例中,就在每次訊號處理的過程中,新建一個元素空間儲存每個被觸發的訊號計數器的值:
my %sigcount; sub allocatinghandler { $sigcount{$_[0]}++; }
上面是不太好的程式設計方式,而下面修改後的程式碼則更好,因為在第一次呼叫子程式的時候,就分配好了一些空間(每個訊號預設值都為0),在每次自增計數器計數的時候不會再新分配記憶體:
%sigcount = map { $_ => 0 } keys %SIG; sub nonallocatinghandler { $sigcount{$_[0]}++; }
傳送訊號(解釋HUP訊號和0訊號)
在Unix系統中,使用kill
命令傳送訊號。在Perl中,也可以使用kill函式來發送訊號。
Perl kill函式至少兩個引數,第一個引數是要傳送的訊號名,第二個或者後面的引數是待發送訊號的PID。Perl kill的返回值為成功交付訊號的程序數量 (因為有些訊號忽略的程序沒必要計算是否接收了訊號,所以忽略的訊號不計數):
# 傳送INT訊號給多個程序 kill 'INT', @mychildren; # 更易讀的方式 kill INT => @mychildren, $grandpatoo; # 程序自殺 kill KILL => $$; kill (9, $$);# 使用數值格式的訊號 # 傳送訊號給父程序 kill USR1 => getppid;
其中getppid函式用來獲取父程序的PID。
向一個負數的PID傳送訊號,表示將訊號傳送給該PID所在程序組(包括子程序、兄弟程序,甚至可能會包括父程序)。例如,下面的語句表示傳送HUP訊號給當前程序自身所在的程序組:
kill HUP => -$$;
HUP訊號經常會發送給父程序,然後父程序會發送給其所有子程序來終止它們,並重新初始化它們。例如apache httpd可以傳送一個HUP訊號給main程序,來重新fork子程序。當然,在這過程中,父程序自身可能並不希望被HUP終止,所以這時常為父程序設定訊號忽略。如下:
sub huphandler{ local $SIG{HUP} = 'IGNORE'; kill HUP => -$$; }
訊號0是特殊的訊號,它不會有任何操作,僅僅用來檢查程序是否存在。因為kill返回值是正確接收訊號的程序數量,如果程序存在,0訊號就會被接收但卻不會做任何處理,但kill的返回值卻為1。例如,檢查某個子程序是否存在:
kill (0 => $child) or warn "Child $child is dead!";