信号处理
操作系统可以通过信号(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!";
请发表评论