perl 信号控制
转载1: http://www.php-oa.com/2009/06/09/perl_signal.html转载2: http://blog.163.com/zhuyu_blog/blog/static/26735153200743072258824/
参考:http://search.cpan.org/~dlux/Parallel-ForkManager-0.7.9/lib/Parallel/ForkManager.pm
==========================================================================
linux中的信号
先了解在linux中的信号,信号其实就是编程里俗称的中断,它使监视与控制其他进程变为有可能。中断信号(signal,又简称为信号)用来通知进程发 生了异步事件。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。注意,信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。
首先看看linux中的常用信号,见如下列表:
========================================================================
信号名 值 标注 解释
————————————————————————
HUP 1 A 检测到挂起
INT 2 A 来自键盘的中断
QUIT 3 A 来自键盘的停止
ILL 4 A 非法指令
ABRT 6 C 失败
FPE 8 C 浮点异常
KILL 9 AF 终端信号
USR1 10 A 用户定义的信号1
SEGV 11 C 非法内存访问
USR2 12 A 用户定义的信号2
PIPE 13 A 写往没有读取者的管道
ALRM 14 A 来自闹钟的定时器信号
TERM 15 A 终端信号
CHLD 17 B 子进程终止
CONT 18 E 如果被停止则继续
STOP 19 DF 停止进程
TSTP 20 D tty键入的停止命令
TTIN 21 D 对后台进程的tty输入
TTOU 22 D 对后台进程的tty输出
————————————————————————
著明:上表中‘值’列下没有列出的值所对应的信号为系统调用的非标准信号。上表中的第三列‘标注’定义了当进程接受到信号后的默认的操作
A—–终止进程
B—–忽略进程信号
C—–终止进程并卸下内核
D—–停止进程
E—–恢复进程
F—–不能截取或忽略进程信号
==========================================================================
Perl中命令信号的原理
Perl 提供了%SIG这个特殊的默认HASH.调用需要使用到系统保留全局HASH数组%SIG,即使用’$SIG{信号名}’截取信号,相当于,在perl程序中出现这个信号时,执行我们自己定义某段代码(子函数)的地址值(定义信号响应函数),这代码就是截取这个信息后要执行的结果了。
例如:
1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/perl
$SIG{TERM}=$SIG{INT}=\&yoursub;
my$i=1;
while(1){
sleep1;
$i=$i+1;
print$i."\n";
}
sub yoursub{
print" exit ... \n";
exit 0;
}
最程序运行前,我们给$SIG{TERM}和$SIG{INT}二个hash放一个子函数的引用(地址),当有终端信号或来自键盘的中断时,上面的while中的就不在运行,就开始运行yoursub这个函数.
可以使用的地方
象对信息的处理,我们常用到的地方,可以捕捉die及一些warning的信息,然后打印出来,我们也可以让程序在退出来之前,就是按下Ctrl+c时,进行一些任务(如删除tmp文件),需要注意的地方是.为了尽可能早的加载这些代码,这样就能保证程序一执行就能先得到信号。这样用处非常大,比如我们写的perl的CGI.可以用信号来捕捉CGI程序Internal 500 错误,不然出现了问题,大多数都必须查看Webserver的日志才能知道程序哪里出了错误,页面只一个500服务器错误,象php因为是mod,直接就显示在网页上。可以使用如下的方法.比如将信号捕捉代码放到BEGIN块中
1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/perl
use strict;
BEGIN{
# fatal handler setting.
$SIG{__DIE__}=$SIG{__WARN__}=\&handler_fatal;
}
sub handler_fatal {
print"Content-type: text/html\n";
print"@_&";
}
当然perl的CGI的500错误,用下面的模块CGI::Carp来处理会更加容易些
1
2
use CGI::Carpqw(fatalsToBrowser);
die'Bad error here';
另一个常用的用法使用$SIG{ALRM},设置等待超时一般都这样做:
1
2
3
4
5
6
7
local$SIG{ALRM}=sub{alarm0;die "TIMEOUT";};#超时处理过程
eval{
alarm(10);#设定10秒钟后如果下面的代码没处理完,则进入超时处理过程
$input=;#处理过程
alarm(0);#如果处理完了,取消超时处理设置
};
if($@=~/TIMEOUT/){...}
注意这里alarm(10)一定要放在eval内。否则,万一程序执行完alarm后发生任务切换,而程序再次获得时间片时,ALRM信号已经发生, 这时程序还没有执行到eval内就产生die,程序就会退出
有时我们要杀死所有的子进程,需要用到向进程组发送信息
在perl中,进程组的ID就是$$.如果程序想给所有由它启动的所有子进程发送一个挂起的信号号,现实的方法,先调用setpgrp(0,0),使自己成为新的进程组的领头.这样不管是fork还是open,还是system都无所谓.
当然先排除自己
1
2
3
4
{
local$SIG{HUP}='IGNORE';#排除自己
kill(HUP,-$$);#通知自己的进程组
}
Fork所做的事情
父进程将代码段,堆栈段,数据段完全复制一份给子进程。也就是说,在子进程运行之初,它拥有父进程的一切变量和句柄。例如,父进程申明了某个hash表,那这个hash表也会被子进程拥有。
然而,一旦子进程开始运行,它的数据段和堆栈段就在内存里完全和父进程分离开了。也就是说,两个进程间不再共享任何数据。例如前面所说的hash表,虽然子进程从父进程处继承了这个数据结构,但子进程写往hash里的数据,不会被父进程访问到。
Fork出来的僵死进程
如果进程退出时,会向父发送一个CHLD的信号后,就会变成僵死的进程,需要父进程使用wait和waitpid来收割.当然,也可以设置$SIG{CHLD}为IGNORE
不错的Fork的模块
1
2
3
4
5
6
7
8
use Parallel::ForkManager;
$pm=new Parallel::ForkManager($MAX_PROCESSES)#标明最大进程数。0是不 fork .
foreach$data(@all_data){
my$pid=$pm->startandnext;#$pm->start 来开始 fork.$pm 是子进程时返回 0 ,父进程时返回子进程的进程.and next; 用来跳过父进程。
...子进程中...
$pm->finish;#结束子程序
}
$pm->wait_all_children;#回收资源
注意:当你使用在子进程中时,就不能使用 $pm->start。 必须再初始化一个.
在 Unix 系统上守护进程化
对于 Fork出来的程序,我们常常需要给进程推到后台,不然在前台程序会一直占用,当关掉当前 shell 时,程序就退出了。如下有个 中一个非常合适的例子:
sub daemonStart {
die "Can’t fork:$!" unless defined (my $child = fork);
exit 0 if $child;
open STDIN,"/dev/null" or die"Can’t write to /dev/null: $!";
#open STDERR, ">&STDOUT" || die ("open STDERR failed");
# 分离终端和进程组
setsid() or die "Can’t start a new session.";
chdir ‘/’;#修改工作目录
umask(0); #设置文件的 umask
$ENV{PATH} = "/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin";
return $$;
}
建议使用 Proc::Daemon 模块
==========================================================================
Perl 信号
Perl 使用一种简单的信号处理模型:在 %SIG散列里包含指向用户定义信号句柄的引用(符号或者硬引用)。某些事件促使操作系统发送一个信号给相关的进程。这时候对应该事件的句柄就会被调用,给该句柄的参数中就有一个包含触发它的信号名字。要想给另外一个进程发送一个信号,你可以用 kill函数。把这个过程想象成是一个给其他进程发送一个二进制位信息的动作。(注:实际上,更有可能是五或者六个二进制位,取决于你的 OS定义的信号数目以及其他进程是否利用了你不发送别的信号的这个情况。)。如果另外一个进程安装了一个处理该信号的信号句柄,那么如果收到该信号,它就能够执行代码。不过发送进程没有任何办法获取任何形式的返回,它只能知道该信号已经合法发送出去了。发送者也接收不到任何接收进程对该信号做的处理的信息。
我们把这个设施归类为 IPC的一种,但实际上信号可以来自许多源头,而不仅仅是其他进程。一个信号也可能来自你自己的进程,或者是用户在键盘上敲入了某种特定键盘序列,比如Control-C 或者 Control-Z造成的,也可能是内核在处理某些特殊事件的时候产生的,比如子进程退出或者你的进程用光堆栈或者达到了文件尺寸或内存的极限等。不过你自己的进程可以很容易区别这些场合。信号就好象一个送到你家门口的没有返回地址的神秘包裹。你打开的时候最好小心一点。
因为在 %SIG 里的记录可能是硬链接,所以通常把匿名函数用做信号句柄:
$SIG{INT} = sub {die "\nOutta here1\n"}; $SIG{ALRM} = sub { die "Your alarm clock went off" }; 或者你可以创建一个命名函数,并且把它的名字或者引用放在散列里的合适的槽位里。比如,要截获中断和退出信号(通常和你的键盘的 Control-C 和 Control-\ 绑在一起),你可以这样设置句柄:
%CODE{"perl"}%
sub catch_zap {
my $signame = shift;
our $shucks++;
die "Somebody sent me a SIG$signame!";
}
$shucks = 0;
$SIG{INT} = 'catch_zap'; # 意思总是 &main::catch_zap
$SIG{INT} = \&catch_zap; # 最好的方法
$SIG{QUIT} = \&catch_zap; # 把另外一个信号也捕获上
%ENDCODE%
注意,我们在信号句柄里做的所有事情 就是设置一个全局变量然后用 die抛出一个例外。如果可能,请力争避免处理比这更复杂的东西,因为在大多数系统上,C 库都是不可再入的。信号是异步传送的(注:与 Perl层操作码同步的信号传递安排在以后的版本发布,那样应该能解决信号和核心转储的问题。),所以,如果信号传递后你已经在一个相关的 C库过程里面了,那么调用任何 print 函数(或者只是任何需要 malloc(3)分配更多内存的函数)在理论上都可能触发内存错误并导致内核转储。(甚至可能 die 过程也有点有点不安全——除非该进程是在一个eval 里执行的,因为那样会消除来自 die 的 I/O,于是就让它无法调用 C 库。)
一个更简单的捕获信号的方法是使用 sigtrap 用法安装简单的缺省信号句柄:
use sigtrap qw(die INT QUIT);
use sigtrap qw(die untrapped normal-signals stack-trace any error-signals);
如果你嫌写自己的句柄麻烦,那就可以用这个用法,不过你仍然会希望捕获危险的信号并且执行一个正常的关闭动作。缺省时,这些信号中的一部分对你的进程是致 命 的,当的程序收到这样的信号时只能停止。糟糕的是,这也意味着不会调用任何用做退出控制的 END 函数和用于对象终止的 DESTROY方法。但是它们在正常的 Perl 例外中的确是被调用的(比如你调用 die的时候),所以,你可以用这个用法无痛地把信号转换成例外。甚至在你没有自己处理这些信号的情况下,你的程序仍然能够表现正确。参阅第三十一章,用法模块,里 usesigtrap 的描述获取这个用法的更详细的特性。
你还可以把 %SIG 句柄设置为字串“IGNORE”或者“DEFAULT”,这样,Perl就会试图丢弃该信号或者允许用缺省动作处理该信号(不过有些信号既不能捕获,也不能忽略,比如 KILL 和 STOP信号;如果手边有资料,你可以参阅 signal(3),看看你的系统可以用的信号列表和它们的缺省行为。)
操作系统认为信号是一个数字,而不是一个名字,但是 Perl 和大多数人一样,喜好符号名字,而讨厌神秘的数字。如果想找出信号的名字,你可以把%SIG 散列里的键字都列出来,或者如果你的系统里有 kill 命令,你可以用 kill -l 把它们列出来。你还可以使用 Perl 标准的Config 模块来检查你的操作系统的信号名字和信号数字之间的映射。参阅 Config(3) 获取例子。
因为 %SIG是一个全局的散列,所以给它赋值将影响你的整个程序。如果你把信号捕获局限于某个范围,可能对你的程序的其他部分更有好处。实现这个目的的方法是用一个local 信号句柄赋值,这样,一旦退出了闭合的语句块,那么该句柄就失去作用了。(但是要记住,local变量对那些语句块中调用的函数是可见的。)
{
local $SIG{INT} = 'IGNORE'; ... # 处理你自己的业务,忽略所有的信号
fn(); # 在 fn() 里也忽略信号! ... # 这里也忽略。
} # 语句块退出后恢复原来的 $SIG{INT} 值。
fn(); # 在(假设的) fn() 里没有忽略 SIGINT
1 给进程组发信号
(至 少在 Unix 里,)进程是组织成进程组的,一起对应一个完整的任务。比如,如果你运行了单个 shell命令,这条命令是有一系列过滤器命令组成,相互之间用管道传递数据,这些进程(以及它们的子进程)都属于同一个进程组。该进程组有一个数字对应这个进程组 的领头进程的进程号。如果你给一个正数的进程号发送信号,该信号只发送给该进程,而如果你给一个负数进程号发送信号,那么该信号将发送给对应的进程组的所有进程,该进程组的进程组号就是这个负数的绝对值,也就是该进程组领头进程的进程号。(为了方便进程组领头进程,进程组 > 假 设你的程序想给由它直接启动的所有子进程(以及由那些子进程启动的孙子进程和曾孙进程等)发送一个挂起信号。实现这个目的的方法是:你的程序首先调用setpgrp(0,0),使自己成为新的进程组的领头进程,这样任何它创建的进程都将成为新进程组的一部分。不管那些进程是通过 fork手工启动的还是通过管道 open 打开的或是用 system("cmd &")启动的后台进程。即使那些进程有自己的子进程也无所谓,只要你给你的整个进程组发送挂起信号,那么就会把它们都找出来(除了那些设置了自己的进程组或者改变了自己的UID 的进程——它们对你的信号有外交豁免权。)
{
local $SIG{HUP} = 'IGNORE'; # 排除自己
kill(HUP, -$$); # 通知自己的进程组
}
另外一个有趣的信号是信号数 0。它实际上不影响目标进程,只是检查一下,看看那个进程是否还活着或者是是否改变了 UID。也就是说,它判断给目标进程发送信号是否合法,而实际上并不真正发送信号。
unless ( kill 0 => $kid_pid ) { warn "something wicked happened to $kid_pid"; } 信 号 0 是唯一的一个在 Unix 上和 Windows 上的 Perl 移植作用一样的信号。在 Microsoft 系统里,kill实际上并不发送信号。相反,它强迫目标进程退出,而退出状态由信号数标明。这些东西以后都会修改。但是,神奇的 0信号将依然如故,表现出非破坏性的特性。
2 收割僵死进程 当一个进程退出的时候,内核向它的父进程发送一个 CHLD 信号然后该进程就成为一个僵死进程(zombie,注:这是一个技术术语),直到父进程调用wait 或者 waitpid。如果你在 Perl 里面启动新进程用的不是 fork,那么 Perl就会替你收割这些僵死进程,但是如果你用的是一个 fork,那么就得自己做清理工作。在许多(但不是全部)内核上,自动收割的最简单办法就是把$SIG{CHLD} 设置为'IGNORE'。另一个更简单(但也更乏味)的方法是你自己收割它们。因为在你开始处理的时候,可能有不止一个子进程已经完蛋了,所以,你必须在一个循环里收割你的子进程直到没有更多为止:
use POSIX ":sys_wait_h";
sub REAPER {
1 until waitpid(-1, WNOHANG) == -1)
}
想根据需要运行这些代码,你要么可以给它设置 CHLD 信号:
$SIG{CHLD} =\&REAPER; 或 者如果你的程序是在一个循环里运行,那你只需要循环调用收割器就行了。这个方法最好,因为它避免了那些信号可能触发的在 C库里偶然的核心转储。但是,如果在一个很快速的循环里调用,这样做的开销是巨大的,所以一种合理的折衷是用一种混合的方法:你在句柄里尽可能少做处理,把风险降到最低,同时在外部循环中等待收割僵死进程:
%CODE{"perl"}%
our $zombies = 0;
$SIG{CHLD} = sub { $zombies++};
sub reaper {
my $zombie;
our %Kid_Status; # 存储每个退出状态
$zombies = 0;
while (($zombie = waitpid( -1, WNOHANG)) = -1) {
$Kid_Status{$zombie} = $?;
}
}
while(1) {
reaper() if $zombies;
...
}
%ENDCODE%
这段代码假设你的内核支持可靠信号。老的 Sys V 风格的信号是不可靠的,那样的话,想写正确的信号句柄几乎是不可能的。甚至早在 Perl 版本5.003,只要可能,我们就开始使用sigaction(2)系统调用了,因为它更可靠些。这意味着除非你在一个古老的操作系统上运行或者跑的是一个古老的 Perl,你用不着重新安装你的句柄,也不会冒丢失信号的危险。幸运的是,所有带 BSD 风格的系统(包括 Linux,Solaris,和 Mac OSX)以及所有 POSIX 兼容的系统都提供可靠的信号,所以那些老旧的Sys V 问题更多是历史遗留问题,而不是目前我们要关心的问题。
在新内核上,还有许多其他东西也会运行得更好些。比如,“慢的”系统调用(那种可以阻塞的,就象 read,wait,和accept)如果被一个信号中断后将自动重新启动。在那些灰暗的旧社会里,用户代码必须记得明确地检查每个慢的系统调用是否带着 $!($ERRNO) 为 EINTR 失败的,而且如果是这样,那么重起。而且这样的情况不光对 INT 信号,而且对有些无辜的信号,比如TSTP(来自 Control-Z)或者 CONT (来自把任务放到前台)也会退出系统调用。如果操作系统允许,现在 Perl自动为你重新启动系统调用。我们通常认为这是一个特性。
你可以检查一下,看看你的系统是否有更严格的 POSIX 风格的信号,方法是装载 Config 模块然后检查$Config{d_sigaction} 是否为真。要检查慢的系统调用是否可以可以重起,检查你的系统的文档:sigaction(2) 或者sigvec(3),或者在你的 C sys/signal.h 里查找 SV_INTERRUPT 或者SA_RESTART。如果找到两个或者其中之一,你可能就拥有可重起的系统调用。
3 给慢速操作调速 信号的一个用途就是给长时间运行的操作设置一个时间限制。如果你用的是一种 Unix 系统(或者任何 POSIX 兼容的支持 ALRM 信号的系统),你就可以让内核在未来的某时刻给你进程发送一个 ALRM 信号:
%CODE{"perl"}%
use Fcntl ':flock';
eval {
local $SIG{ALRM} = sub { die "alarm clock restart" };
alarm 10; # 安排10秒后报警
eval {
flock(FH, LOCK_EX) # 一个阻塞的,排它锁
or die "can't flock:$!";
};
alarm 0; # 取消报警
};
alarm 0; # 避免冲突条件
die if $@ && $@ !~ /alarm clock restart/; # 重新启动
%ENDCODE%
如果你在等待锁的时候报警,你只是把信号缓冲起来然后返回,你会直接回到 flock,因为 Perl在可能的情况下会自动重起系统调用。跳出去的唯一方法是用 die 抛出一个例外并且让 eval 捕获之。(这样做是可行的,因为例外会退回到调用库的longjmp(3) 函数,而 longjmp(3) 是真正把你带出重起系统调用的东西。)
我们使用了嵌套的例外陷阱是因为如果 flock 在你的平台上没有实现的话,那么调用 flock会抛出一个例外,因此你必须确保清理了警告信号。第二个 alarm 0 用于处理这样的情况:信号到达时是在调用 flock 之后但是在到达第一个alarm 0 之前。没有第二个alarm,你可能会面对一个很小的冲突条件——不过冲突条件可不会管你的冲突条件是大是小;它们是黑白分明的:要么有,要么无。而我们更希望没有。
4 阻塞信号 有 时候,你可能想在一些关键的代码段里推迟接收信号。你并不想简单地忽略这些信号,只是你做的事情太关键了,因而不能中断。Perl 的 %SIG散列并不实现信号阻塞,但是 POSIX 模块通过它的调用 sigprocmask(2) 系统调用的接口实现了信号阻塞:
%CODE{"perl"}%
use POSIX qw(:signal_h);
$sigset = POSXI::SigSet->new;
$blockset = POSIX::SigSet->new(SIGINT, SIGQUIT, SIGCHLD);
sigprocmask(SIG_BLOCK, $blockset, $sigset) or die "Could not block INT, QUIT, CHLD signals: $! \n";
%ENDCODE%
一旦上面三个信号都被阻塞了,你就可以毫不担心地执行你的任务了。在你处理完你的关键业务以后,用恢复旧的信号掩码的方法取消信号的阻塞:
sigprocmask( SIG_SETMASK, $sigset) or die "Could not restore INT, QUIT, CHLD signals: $!\n"; 如果阻塞的时候有三个信号中的任何信号到达,那么这时它们会被立即发送。如果有两种或者更多的不同信号在等待,那么它们的发送顺序并没有定义。另外,在阻 塞过程中,收到某个信号一次和收到多次是没有区别的。(注:通常是这样。根据最新的规范,可计数信号可能在一些实时系统上有实现,但是我们还没有看到那些系统。)比如,如果你在阻塞 CHLD信号期间有九个子进程退出,那么你的信号句柄(如果存在)在退出阻塞后仍然只会被调用一次。这就是为什么当你在收割僵死进程的时候,你应该循环到所有的僵死进程都消失
页:
[1]