Perl信號處理

本文關於Perl信號處理的內容主體來自於《Pro Perl》的第21章。apache

信號處理

操做系統能夠經過信號(signal)處理機制來實現一些功能:程序註冊好待監視的信號處理機制,在程序運行過程當中若是產生了對應的信號,則會按照註冊好的處理方式進行處理。編程

signal基礎

每一個進程都記錄了一個信號(signal)索引表,並註冊了各類信號的處理方式,每當收到信號的時候,會當即中止執行操做並處理對應的信號。數組

絕大多數信號都有默認處理機制,但Perl支持用戶本身從新定義接收到信號時的處理方式。在Perl中,信號處理的方式註冊在一個hash變量%SIG中,key爲信號的名稱,value有幾種可能的值:安全

  • DEFAULT或undef:表示採起所接收信號的默認處理方式
  • IGNORE:表示忽略接收到的該信號
  • 子程序引用:如\&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:用戶自定義信號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完成後再終止。再例如,正在執行排序操做的時候,不會在排序的過程當中終止,而是當前排序過程完成後再終止。操作系統

安全的信號機制優勢很明顯,它可讓程序更加健壯。可是缺點也很明顯,由於有些操做可能會花費比較長的時間,而後才終止進程。固然,大多數時候這個缺點並非什麼大問題,可是有些狀況下對時間長短的控制要求很是精確(好比反導彈系統,必須在一個很短的時間內計算出一些數據,這種程序極可能會直接定製操做系統實現特殊的功能),這樣的狀況就不適合使用這種安全的信號處理機制。rest

從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來取消計時器。但在稍微大一點的程序中,取消計時器是頗有必要的,由於咱們不知道何時程序結束。

相關文章
相關標籤/搜索