信號處理 操作系統可以通過信號(signal)處理機制來實現一些功能:程式註冊好待監視的信號處理機制,在程式運行過程中如果產生了對應的信號,則會按照註冊好的處理方式進行處理。 signal基礎 每個進程都記錄了一個信號(signal)索引表,並註冊了各種信號的處理方式,每當收到信號的時候,會立即停止 ...
本文關於Perl信號處理的內容主體來自於《Pro Perl》的第21章。
信號處理
操作系統可以通過信號(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 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!";
SIGALRM信號:ALARM
alarm常用來做一個計時器,計時到了就發送ALRM信號來終止計時器所在進程。
可以通過alarm函數設置一個計時器,它的參數是0或正數,正數表示計時多少秒,0表示取消當前已有的計時器。每個進程只能有一個alarm計時器。
# 30秒的計時器
alarm 30;
計時器計時到了,就會立即發送ALRM信號,該信號預設行為是終止當前進程,除非設置了ALRM信號的處理方式。例如,下麵定義了一個2秒的計時器,後面還睡眠5秒:
$ perl -le 'alarm 2;sleep 5;'
在睡眠5秒的過程中,大概在第二秒後就直接終止進程了,而不是等到5秒都睡眠完。
需要註意的是,前面說過安全的信號處理機制會等待當前正在執行的opcode執行完再處理信號,所以alarm定義的計時器可能並不那麼精確,出現一點點的誤差是經常性的。
重新設置計時器會覆蓋之前已有的計時器。例如:
alarm 30; # 30秒的計時器
... do something ...
alarm 5; # 覆蓋前面的定時器,重新定義一個5秒的計時器
alarm函數的參數設置為0表示取消已有的alarm計時器,但註意取消計時器不會發送SIGALRM信號。
alarm 0;
計時器有時候非常好用,它是非阻塞模式的sleep,可以讓我們回到交互模式下並計時。例如,下麵的示例中要求在5秒內輸入一個字元,如果沒輸入就一直提示"Hurry UP:",並繼續設置5秒的計時器等待輸入,由於ReadKey是阻塞的,只要一輸入就不再阻塞,於是進入後續語句並很快到達程式的尾部並正常結束。
#!/usr/bin/perl
use strict;
use warnings;
use Term::ReadKey;
# Make read blocking until a key is pressed, and turn on autoflushing (no
# buffered IO)
ReadMode 'cbreak';
$| = 1;
sub alarmhandler {
print "\nHurry up!: ";
alarm 5;
}
$SIG{ALRM} = \&alarmhandler;
alarm 5;
print "Hit a key: ";
my $key = ReadKey 0;
print "\n You typed '$key' \n";
# cancel alarm
alarm 0;
# reset readmode
ReadMode 'restore';
上面的alarm 0其實是多餘的,因為只要輸入了字元後,基本上立即就到達了程式的結尾而正常結束,所以不需要alarm 0來取消計時器。但在稍微大一點的程式中,取消計時器是很有必要的,因為我們不知道什麼時候程式結束。