本文关于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来取消计时器。但在稍微大一点的程序中,取消计时器是很有必要的,因为我们不知道什么时候程序结束。

友情链接
KaDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:[email protected]
QQ群:637538335
关注微信