名称perlipc - Perl的进程间通讯(信号、fifos、管道、安全子进程、套接字与信号量)
描述Perl的基本的进程间通讯方法有旧式UNIX信号、命名管道、打开的管道、伯克利套接字以及SysV IPC调用。每一种都在不同的情形下有各自的应用。
信号Perl使用一种简单的信号处理机制:哈希%SIG包含了信号的名字和用户安装的信号处理句柄。这些句柄将带着触发它的信号的名字为参数被调用。一 个信号可能通常地从其它进程发送过来的一串键盘序列(如Control-C或者Control-Z)触发,或者自动地从相应的内核事件触发,比如一个子进 程退出,你的进程超过了堆栈空间,或者达到了文件大小限制。
比如,为了捕获一个交互地的信号,像这样安装一个信号处理句柄:
sub catch_zap {
my $signame = shift;
$shucks++;
die "Somebody sent me a SIG$signame";
}
$SIG{INT} = 'cat_zap'; # 可能失败噢
$SIG{INT} = \&catch_zap; # 更好的策略在Perl 5.7.3以前,在你的处理句柄中尽可能少的操作是必要的:你注意一下我们所做的所有操作,就是设置一下全局变量然后触发一个异常。那是因为,在很多系统里,库是不可重入的:尤其内存申请及IO处理不是。那意味着,在你的处理回调句柄里做任何事情,都可能触发一个内存错误或者接下来的崩溃-看安全信号。
信号名字可以在你的系统上由命令kill -l列出,或者从Config模块获取它们。设置一个以数字为索引的@signame列表来得到名字,一个以名字为索引的%signo哈希数组来得到数字。
use Config;
defined $Config{sig_name} || die "No sigs?";
foreach $name (split(' ', $Config{sig_name})) {
$signo{$name} = $i;
$signame[$i] = $name;
$i++;
}为了检查信号17与SIGALARM是否一样,这样做:
print "signal #17 = $signame[17]\n";
if ($signo{ALRM}) {
print "SIGALRM is $signo{ALRM}\n";
}你也可以选择字符串'IGNORE'或者'DEFAULT'做为句柄,这样,Perl将尝试忽略信号或者做默认处理。
在很多UNIX平台上,CHLD(有时为CLD)信号被赋与'IGNORE'值时有特别地行为。在这类系统上,设置$SIG{CHLD}为'IGNORE'具有当父进程wait()它的子进程失败时不创建僵尸进程的效果(或者说,子进程被自动地回收)。在这样的系统上,设置$SIG{CHLD}为'IGNORE'的进程调用wait()通常返回-1。
一些信号可以既不被捕获也不被忽略,比如KILL和STOP(但不是TSTP)信号。为了暂时忽略信号,一种策略是使用local()语句,这可以使得你的块结束的时候,信号被恢复。(记住:local()变量是会被块内调用的函数继承的。)
sub precious {
local $SIG{INT} = 'IGNORE';
&more_functions;
}
sub more_functions {
# 中断仍然被忽略着呢……
}发送一个信号给一个负的进程ID意味着你发送这个信号给整个Unix进程组。这块代码发送hang-up信号给自己所在的进程组的所有进程(设置$SIG{HUP}为IGNORE,所以它不会杀掉自身):
{
local $SIG{HUP} = 'IGNORE';
kill HUP => -$$;
# 比用:kill('HUP', -$$)更优美的书写
}另一个有趣的信号是数字0。这不会对子进程有任何操作,但是会检查它是否还存活着或者它的进程UID改变过。
unless (kill 0 => $kid_pid) {
warn "something wicked happened to $kid_pid";
}当直接在一个UID不是发送信号的UID的进程内发送信号0时会失败。因为你没有权限给它发信号,尽管这个进程还活着。你可以可以通过%!判断失败的原因。
unless (kill 0 => $pid or $!{EPERM}) {
warn "$pid looks dead";
}你也可能希望对简单的信号处理句柄注册匿名函数:
$SIG{INT} = sub { die "\nOutta here!\n" };但是对于用来重新注册它们的更加复杂的句柄将会有问题。因为Perl的信号机制,是基于C库的signal(3)函数,你在一些系统上可能有时会不幸地失败,即,它的行为是老的SysV式的而不是新的、更加合理的BSD和POSIX方式的。所以,你有时看到保守的人们像这样书写信号句柄:
sub REAPER {
$waitedpid = wait;
# 恶心的sysV: 它使得我们不能恢复
# 这个信号, 但是把它放在wait后面。
$SIG{CHLD} = \&REAPER;
}
$SIG{CHLD} = \&REAPER;
# 现在开始做创建后的事情……或者更加好:
use POSIX ":sys_wait_h";
sub REAPER {
my $child;
# 如果第1个子进程死亡的引起的信号处理,导致了
# 第2个子进程的结束,我们不会收到第2个信号。所以我们必须循环,或者
# 留下这个子进程做为一个僵尸进程。于是下一次
# 2个子进程死亡我们又得了一个僵尸进程。等等等等。 while (($child = waitpid(-1,WNOHANG)) > 0) {
$Kid_Status{$child} = $?;
}
$SIG{CHLD} = \&REAPER; # 仍然是讨厌的sysV
}
$SIG{CHLD} = \&REAPER;
# 开始做创建后的事情……信号处理也用在Unix的超时操作中,比如,你在一个被安全保护的eval{}块中设置一个信号来捕获超时信号来实现一些时间后调度特定的操作。 然后你阻塞你的操作,在你从你的eval{}块中退出前清除超时信号。如果它失控了,你将使用die()来跳出你的块,就像你在其它语言中使用longjmp()或者throw()。
看一个例子:
eval {
local $SIG{ALRM} = sub { die "alarm clock restart" };
alarm 10;
flock(FH, 2); # blocking write lock
alarm 0;
};
if ($@ and $@ !~ /alarm clock restart/) { die }如果这个超时了的操作是system()或者qx(),这个技巧可以避免创建僵尸进程。如果这符合你的需求,你将需要自己fork()然后exec(),然后杀掉重入的子进程。
对于更复杂的信号处理,你应该看标准的POSIX模块。抱歉,这几乎没有文档,但是Perl源发行包中的t/lib/posix.t文件中有一些例子。
处理后台程序的SIGHUP信号一个通常在系统启动时启动、在系统关闭时停止的进程叫做精灵进程(Disk And Execution MONitor)。如果一个精灵进程有一个配置文件在进程启动后被改变了,应该有一种方法来告诉这个进程在不停止进程的情况下重读它的配置文件。许多精灵进程用SIGHUP信号提供了这个机制。当你想告诉精灵重读文件时,你只需发送一个SIGHUP信号给它。
并不是所有的平台都会在一个信号被执行后重新安装它们内置的信号。这意味着这个机制只能在第一次信号发送时很好地工作。这个问题的解决方法是尽量使用POSIX信号处理,它们的行为更加精确。
下面的例子实现了一个简单的精灵,每次收到一个SIGHUP信号时它将重启自身。实际的代码在子过程<c13>code()里,它只为了表示它在工作以及它将被实际地代码替换打印一点调试信息。
#!/usr/bin/perl -w use POSIX ();
use FindBin ();
use File::Basename ();
use File::Spec::Functions; $|=1; # 为了使精灵跨平台运行, exec总是使用正确的路径调用这个脚本
# 自身, 不用关心这个脚本是如何被执行起来的。 my $script = File::Basename::basename($0);
my $SELF = catfile $FindBin::Bin, $script; # POSIX unmasks the sigprocmask properly
my $sigset = POSIX::SigSet->new();
my $action = POSIX::SigAction->new('sigHUP_handler',
$sigset,
&POSIX::SA_NODEFER);
POSIX::sigaction(&POSIX::SIGHUP, $action); sub sigHUP_handler {
print "got SIGHUP\n";
exec($SELF, @ARGV) or die "Couldn't restart: $!\n";
} code(); sub code {
print "PID: $$\n";
print "ARGV: @ARGV\n";
my $c = 0;
while (++$c) {
sleep 2;
print "$c\n";
}
}
__END__
命名管道命名管理(通常称为FIFO)是一种为了在本机进程间通信的古老的UNIX IPC机制。它工作起来就像通常连接起来的匿名管道,但是进程间通过一个文件名来共享它而不用进程相关。
为了创建命名管道,使用POSIX::mkfifo()函数。
use POSIX qw(mkfifo);
mkfifo($path, 0700) or die "mkfifo $path failed: $!";你也可以使用Unix命令mknod(1)或者其它系统的mkfifo(1)。这些可能不在你的常规的目录下。
# 失败时返回非0,所以得用&&而不是||
#
$ENV{PATH} .= ":/etc:/usr/etc";
if ( system('mknod', $path, 'p')
&& system('mkfifo', $path) )
{
die "mk{nod,fifo} $path failed";
}当你希望连接一个自己无关的进程时,一个fifo比较方便。当你使用fifo时,这个程序将阻塞,直到另一端有什么东西。
比如,你有你自己的.signature文件为命名管道,有一个Perl程序在另一端。现在每次有任何程序(像一个mailer、news reader、finger 程序……)尝试从这个文件读的时候,读程序将会阻塞直到你的程序提供新的signature。我们将使用管道检测参数-p来确定是否有人(或者其它)突然删除了我们的fifo。
chdir; # go home
$FIFO = '.signature'; while (1) {
unless (-p $FIFO) {
unlink $FIFO;
require POSIX;
POSIX::mkfifo($FIFO, 0700)
or die "can't mkfifo $FIFO: $!";
} # 下一行阻塞,直到有一个人来读
open (FIFO, "> $FIFO") || die "can't write $FIFO: $!";
print FIFO "John Smith (smith\@host.org)\n", `fortune -s`;
close FIFO;
sleep 2; # to avoid dup signals
}
延迟信号(安全信号)在Perl 5.7.3以前,使用Perl代码来处理信号。由于两点原因,把你自己放在了危险之中。首先,很少的系统库函数是可重入的。如果Perl正在执行着一个函数(如malloc(3)或者printf(3))时,信号打断了,然后你的信号处理函数调用了同样的函数,你可能得到难以料到的结果-通常,是一个崩溃。其次,在较底层上,Perl自身也不是可重入的。如果Perl正在改变着它内部的数据结构,信号打断了,结果一样难以料到。
有两种态度你可以选择,即:保守或者激进。保守是说在信号处理中,尽可能少执行操作。设置一个已经有值的变量一个值,然后返回。如果你在一个比较慢的可能会重试的系统调用中,这仍然帮不了你。这意味着你不得不使用die来longjmp(3)出你的处理函数。尽管这着实有一些过于保守了,但可以防止系统除去你而避免die在一个句柄中。激进是说,“我知道风险,但是我不管”,并且在信号处理中做任意操作,然后一次一次地清除崩溃文件。
在Perl 5.7.3以及更新的版本中,避免这些问题是“延迟”-当系统发送信号给进程时(给实现Perl的C代码)一个变量被设置,然后处理马上返回。然后在一个 Perl解释器安全的时机(比如,当它要解释一个新的字节码时)这个变量被检查,然后%SIG里的Perl级别的处理被执行。这种“延迟”机制允许在信号 处理的代码中有更复杂的处理,因为我们知道Perl解释器在一个安全的状态,当信号处理被调用时,我们没有在系统库里。Perl里的实现跟以前具有如下的 不同:
长运行的字节码当Perl解释器将要执行新的字节码时,它只查看当前的信号标志,一个长运行的字节码(比如在一个很长的字符串上执行一个正则表达式)将不会看到它直到当前的字节码执行完毕。
N.B. 如果一个信号在一个字节码执行间触发了多次,这个信号的处理只会在字节码执行完毕后被调用一