本文關於Perl信號處理的內容主體來自於《Pro Perl》的第21章。apache
操做系統能夠經過信號(signal)處理機制來實現一些功能:程序註冊好待監視的信號處理機制,在程序運行過程當中若是產生了對應的信號,則會按照註冊好的處理方式進行處理。編程
每一個進程都記錄了一個信號(signal)索引表,並註冊了各類信號的處理方式,每當收到信號的時候,會當即中止執行操做並處理對應的信號。數組
絕大多數信號都有默認處理機制,但Perl支持用戶本身從新定義接收到信號時的處理方式。在Perl中,信號處理的方式註冊在一個hash變量%SIG
中,key爲信號的名稱,value有幾種可能的值:安全
\&subref
或匿名子程序sub { codeblock }
,表示接收到該信號時,執行該子程序要想查看支持的信號,能夠遍歷一下%SIG
,或者直接在Linux下使用kill -l
命令:app
$ perl -le 'print join qq/ /, sort keys %SIG'
要查看信號對應的數值,能夠去Config的sig_name裏查找:less
#!/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
:用戶自定義信號112 | USR2 | SIGUSR2
:用戶自定義信號213 | PIPE | SIGPIPE
:已關閉的管道。當正在讀的、或正在寫入的管道已被對方關閉時,將觸發該信號14 | ALRM | SIGALRM
:alarm信號,噹噹前進程的alarm計時器(alarm定時器即一個定時器)到期了,將觸發該信號。在Microsoft系統上未實現該信號15 | TERM | SIGTERM
:殺死(終止)進程,可被捕捉和忽略,幾乎等同於sigint信號,會盡量的釋放執行clean-up,釋放資源,保存狀態等,優先級高於INT,但低於QUIT和KILL17 | 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 stop29 | IO | SIGIO
:異步IO事件。若是文件句柄設置爲異步IO(即O_ASYNC),當該文件句柄中產生了任何事件(例如可寫事件)時都會發送該信號須要注意的是,對於具備安全信號處理機制的語言(不止是Perl),須要保證在運行一條語句(嚴格地說是opcode)的時候不會被操做系統的信號處理機制中斷,只有在當前正在處理的語句結束後,纔會中斷。函數
例如,在Perl進行IO的時候,信號不會終止正在進行的IO操做,而是在此次IO完成後再終止。再例如,正在執行排序操做的時候,不會在排序的過程當中終止,而是當前排序過程完成後再終止。操作系統
安全的信號機制優勢很明顯,它可讓程序更加健壯。可是缺點也很明顯,由於有些操做可能會花費比較長的時間,而後才終止進程。固然,大多數時候這個缺點並非什麼大問題,可是有些狀況下對時間長短的控制要求很是精確(好比反導彈系統,必須在一個很短的時間內計算出一些數據,這種程序極可能會直接定製操做系統實現特殊的功能),這樣的狀況就不適合使用這種安全的信號處理機制。rest
從Perl 5.8開始,Perl就默認使用safe模式的信號處理機制。若是想要在Perl上使用非安全的信號處理機制,須要設置環境變量PERL_SIGNALS=unsafe
。
前面說過,要想定製信號處理方式,只需在%SIG
中註冊對應的value便可。其中value有幾種可能的值:
\&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的值會覆蓋已有的值。
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]}++; }
在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!";
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來取消計時器。但在稍微大一點的程序中,取消計時器是頗有必要的,由於咱們不知道何時程序結束。