本文关于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
:用户自定义信号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完成后再终止。再例如,正在执行排序操作的时候,不会在排序的过程中终止,而是当前排序过程完成后再终止。
安全的信号机制优点很明显,它可以让程序更加健壮。但是缺点也很明显,因为有些操作可能会花费比较长的时间,然后才终止进程。当然,大多数时候这个缺点并不是什么大问题,但是有些情况下对时间长短的控制要求非常精确(比如反导弹系统,必须在一个很短的时间内计算出一些数据,这种程序很可能会直接定制操作系统实现特殊的功能),这样的情况就不适合使用这种安全的信号处理机制。
从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来取消计时器。但在稍微大一点的程序中,取消计时器是很有必要的,因为我们不知道什么时候程序结束。