骏马金龙

网名骏马金龙,钟情于IT世界里的各种原理和实现机制,强迫症重症患者。爱研究、爱翻译、爱分享。特借此一亩三分田记录自己成长点滴!!!
我本问道人,道心不坚,必将与道无缘!

Perl信号处理

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

posted @ 2019-02-15 22:27 骏马金龙 阅读(...) 评论(...) 编辑 收藏