本系列文章讨论Perl的三个关键功能:进程(Processes)、管道(Pipes)和信号(Signals)。通过建立一个新进程,Perl程序可以运行另一个程序甚至是它自己的拷贝。管道允许Perl脚本刚其它进行交换数据,而信号使Perl脚本监视和控制其它进程成为可能。本文讨论的是其中的:信号。 信号( signal )正如文件句柄,理解信号是网络编程的基础。信号是通过操作系统发送给你的程序的一个消息,告诉它发生了重要的事情。信号可以指示程序自身的一个错误,比如尝试除0。事件要求立刻反应,例如用户尝试终止这个程序,或者一个非关键信息,如程序启动后,终止一个子进程。 常见信号POSIX标准定义了19个信号。每一个信号都拥有一个小的整数和一个符号名。我们在下面的表格显示它们。
按照惯例, 在一个进程中使用TERM 和 KILL 来结束另一个进程。默认的, TERM让程序直接结束,但程序可以搭配一个信号句柄给 TERM,用来拦截结束请求以及可能在退出执行一些清理工作。 相比之下,KILL 信号是无法捕获的,它会强制进程立刻结束。比如,当UINX系统关机时,shutdown(关机)进程首先发送 TERM 给所有正在运行的进程,给机会给它们进行清理。如果少数进程在几秒后依然在运行,那么它将发送 KILL。 捕获信号你可以通过在全局哈希 %SIG 中添加一个信号句柄来捕获一个信号。使用你想捕获的信号名作为哈希的键。例如,使用 $SIG{INT} 来获取或设置 INT 信号句柄。使用引用作为值:一个匿名函数或指向已命名函数的引用。例如,下面的例子是一个设定 INT 信号句柄的小脚本。当我们按下中断键的时候,它打印一条短信息并增加计数器。脚本如此运行下去,直到计数到三次中断,说明真正要结束了。在下面的例子中,当我们按下Ctrl+C时,打印一条“别打断我!”的信息。 #!/usr/bin/perl #文件:interrupt.pl #1 ############################################## # (c) 2011 LoRui(i@, www.) # ############################################## use strict; #2 use warnings; my $interruptions = 0; #3 $SIG{INT} = \&hanlde_interruptions; #4 while($interruptions < 3) { #5 print "休息一下……\n"; #6 sleep 5; #7 } #8 sub hanlde_interruptions { #9 $interruptions++; #10 warn "别打断我!你已经打断我 $interruption 次了!\n"; #11 } #12 来看看这个脚本的详细信息。
对于很短的信号处理程序,你可以使用匿名函数进行处理。比如,下面的代码片段和刚刚的行将,但我们不需要给信号处理程序命名: $SIG{INT} = sub { $interruptions++; warn "别打断我!你已经打断我 $interruption 次了!\n"; }; 除了引用代码之外, %SIG 接受两个特例。字符串“DEFAULT”恢复默认信号的行为。例如,将 $SIG{INT} 设定为“DEFAULT”,将让 INT 信号再次结束当前脚本。字符串“IGNORE”将让该信号完全被忽略。 $SIG{TERM} = $SIG{HUP} = $SIG{INT} = \&handler sub handler { my $sig = shift; warn "处理 $sig 信号.\n"; } 处理 PIPE 异常现在我们拥有了处理PIPE异常所需要的知识。回想《Perl进程、管道和信号之二:管道》里的示例代码 write_ten.pl 和 read_three.pl中那恐怖的PIPE错误。write_ten.pl打开一个到read_three.pl的管道并尝试向其写入10行文本,但read_three.pl只期望接受3行之后就退出并结束管道。write_ten.pl 并不知道到对方的连接已经被关闭,尝试写入第4行的时候, PIPE 信号产生了。 #!/usr/bin/perl #文件:write_ten_ph.pl #1 ############################################## # (c) 2011 LoRui(i@, www.) # ############################################## use strict; #2 use warnings; my $ok = 1; #3 $SIG{PIPE} = sub { undef $ok }; open(PIPE, "| read_three.pl") or die ("无法打开管道:$!"); select PIPE; $| = 1; select STDOUT; my $count = 0; for($_ = 1; $ok && $_ <= 10; $_++) { warn "写入第 $_ 行\n"; print PIPE "这是第 $_ 行\n" and $count++; sleep 1; } close PIPE or die "无法关闭管道:$!"; print "共写入 $count 行文本\n"; $SIG{PIPE} = sub { undef $ok }; 当接受到一个PIPE信号时,该处理程序将取消$ok的定义,让其为假。 % write_ten_ph.pl 写入第 1 行 read_three.pl 获取:这是第 1 行 写入第 2 行 read_three.pl 获取:这是第 2 行 写入第 3 行 read_three.pl 获取:这是第 3 行 写入第 4 行 共写入 3 行文本 另一种常用方法是设置 $SIG{INT} 为“IGNORE”,以完整的忽略 PIPE信号。现在,我们的职责是检查出了什么错,我们可以通过 print() 的返回值进行检测。如果 print() 返回假值,我们退出循环。 #!/usr/bin/perl #文件:write_ten_i.pl #1 ############################################## # (c) 2011 LoRui(i@, www.) # ############################################## use strict; #2 use warnings; $SIG{PIPE} = 'IGNORE'; #3 open(PIPE, "| read_three.pl") or die "无法打开管道:$!"; #4 select PIPE; $| = 1; select STDOUT; #5 my $count = 0; #6 for(1..10) { #7 warn "写入第 $_ 行\n"; #8 if(print PIPE "这是第 $_ 行\n") { #9 $count++; #10 } else { #11 warn "写入数据时有错误发生:$!\n"; #12 last; #13 } #14 sleep 1; #15 } #16 close PIPE or die "无法关闭管道:$!"; #17 print "共写入 $count 行文本\n"; #18 运行结果: % write_ten_i.pl 写入第 1 行 read_three.pl 获取:这是第 1 行 写入第 2 行 read_three.pl 获取:这是第 2 行 写入第 3 行 read_three.pl 获取:这是第 3 行 写入第 4 行 写入数据时有错误发生:Broken pipe 共写入 3 行文本 注意,如果失败,错误信息中的 $! 处将显示 “Broken pipe”。如果你希望将这个错误与其它I/O错误分别处理,我们可以通过正则表达式来明确地测试它的值。或用更好的方法:通过它的数字值与EPIPE的错误常量进行比对,例如: use Errno ':POSIX'; ... unless (print PIPE "这是第 $_ 行\n") { # 处理写入错误 last if $! == EPIPE; # PIPE错误,终止循环 die "I/O 错误: $!"; # 其它错误,打印错误信息 } 发送信号Perl脚本可以使用 kill() 函数发送一个信号到其它进程。 $count = kill($signal, @processes)kill() 函数发送信号$signal给一个或多个进程。你可以通过数字(比如:2)或符号名(比如: INT)来指定要发送的信号。@processes是将信号发送过去的一个或多个进程的PID列表。成功执行信号的进程数将作为 kill() 函数的结果返回。 一个进程只能发送一个信号给其它进程,并且需要对应的权限。一般而言,进程以普通用户权限运行,那么该进程也只能给普通用户权限及普通用户以下权限运行的进程发送信号。是的,以root或超级用户权限运行的进程可以给任何进程发送信号。 kill INT => $$; # 等效于 kill('INT',$$) 让慢的系统调用超时当Perl执行系统调用时,信号可能产生。大多数情况下,Perl自动重启并严密监控调用。 $slept = sleep([$seconds])根据指定的秒数暂停,或一直暂停直到接收到一个信号。如果没有给定参数,该函数将永远暂停。 sleep() 将返回其实际暂停的秒数。 另一个例外是四个参数的 select(),它可用于定时等待,直到一个或多个设定的I/O文件句柄准备就绪。该函数将在以后文章中描述。 有时候,自动重启系统调用不是你想要的。比如,一个应用程序提示用户输入密码,并尝试从标准输入读取用户输入。你可能希望,读取工作在一段时间后超时退出,以避免用户已离开,程序却还在等待输入。下面的代码片段看上去好像能胜任这个工作: my $timed_out = 0; $SIG{ALRM} = sub { $timed_out = 1 }; print STDERR "输入密码: "; alarm (5); # 5秒超时 my $password = <STDIN>; alarm (0); print STDERR "操作已超时\n" if $timed_out; 这里我们使用 alarm() 函数来设计定时器。当定时器过期,操作系统生成一个 ALRM信号,我们拦截这个信号并进行处理:设置全局变量 $timed_out 为真。在这个代码里,我们用5秒超时来调用 alarm() 函数,然后从标准输入读取一行。读取完成之后,我们以零为参数再次调用 alarm(),以关闭定时器。就是说,用户要在5秒钟的时间内输入密码,否则定时器将失效,我们也不再重启该程序。 $seconds_lef = alarm($seconds)为把 ALRM 信号在$seconds秒之后传递给进程做准备。如果参数为零,将使定时器失效。 Perl自动重启使用系统调用变慢的问题中,包括 <>。即使闹钟停止了,我们还停留在<>调用,等待用户的键盘输入。 print STDERR "输入密码: "; my $password = eval { local $SIG{ALRM} = sub { die "超时\n" }; alarm (5); # 5秒超时 return <STDIN>; }; alarm (0); print STDERR "操作已超时\n" if $@ =~ /timeout/; 这个程序中,我们命名 eval{} 块让 ALRM处理程序局部化(localize)。eval{} 块设定闹钟,跟前面一样,尝试从 STDIN 读取。如果在 <> 返回之前已超时,用户输入将从eval{} 块返回,并赋值给 $password。 |
|