我已经读了几遍这个问题,我认为我有点明白您想做什么。您有一个控制脚本。这个脚本生成子进程来做一些事情,这些子进程生成孙子进程来实际完成工作。问题是孙子进程可能太慢(等待STDIN之类的),您想杀死它们。此外,如果有一个缓慢的孙子进程,您希望整个子进程都死掉(如果可能的话杀死其他孙子进程)。
因此,我尝试用两种方法实现这个功能。第一种方法是让父进程在一个新的UNIX会话中生成一个子进程,设置一个几秒钟的定时器,当定时器响起时杀死整个子进程。这使得父进程对子进程和孙子进程都负责。但这并没有正常工作。
下一个策略是让父进程生成子进程,然后让子进程负责管理孙子进程。它将为每个孙子进程设置一个定时器,并在过期时间内如果该进程未退出则杀死它。这个方法非常好,所以这里是代码。
我们将使用EV来管理子进程和定时器,使用AnyEvent来进行API调用。(您可以尝试另一个AnyEvent事件循环,如Event或POE。但我知道EV正确处理了子进程在您告诉循环监视它之前退出的情况,这消除了其他循环容易受到的烦人的竞态条件。)
use strict;
use warnings;
use feature ':5.10';
use AnyEvent;
use EV;
我们需要跟踪子监视器:
# active child watchers
my %children;
接下来我们需要编写一个函数来启动子进程。父进程创建的子进程被称为“children”,而子进程创建的任务被称为“jobs”。
sub start_child($$@) {
my ($on_success, $on_error, @jobs) = @_;
这些参数包括:一个回调函数,用于在子进程成功完成时被调用(这意味着它的工作也是成功的),一个回调函数,用于在子进程未能成功完成时被调用,以及要运行的coderef任务列表。
在此函数中,我们需要进行分叉。在父进程中,我们设置了一个子进程监视器来监视子进程:
if(my $pid = fork){
say "$$: Starting child process $pid";
$children{$pid} = AnyEvent->child( pid => $pid, cb => sub {
my ($pid, $status) = @_;
delete $children{$pid};
say "$$: Child $pid exited with status $status";
if($status == 0){
$on_success->($pid);
}
else {
$on_error->($pid);
}
});
}
在子进程中,我们实际上运行任务。但这需要一些设置。
首先,我们忽略父进程的子进程监视器,因为让子进程知道其兄弟进程退出是没有意义的。(fork很有趣,因为即使这完全没有意义,你也会继承父进程的所有状态。)
else { # child
# kill the inherited child watchers
%children = ();
my %timers;
我们还需知道所有任务何时完成,以及它们是否全部成功。我们使用计数条件变量来确定何时所有任务都已退出。我们在启动时增加计数,在退出时减少计数,当计数为0时,我们就知道所有任务都已完成。
我还保留了一个布尔值来指示错误状态。如果进程以非零状态退出,则错误状态为1。否则,它保持为0。您可能希望保留比这更多的状态 :)
# then start the kids
my $done = AnyEvent->condvar;
my $error = 0;
$done->begin;
我们从1开始计数,这样如果没有作业,我们的进程仍然会退出。
现在我们需要为每个作业分叉并运行该作业。在父进程中,我们做了一些事情。我们增加条件变量。我们设置一个定时器来杀死孩子,如果它太慢了。并且我们设置了一个子监视器,以便我们可以被告知作业的退出状态。
for my $job (@jobs) {
if(my $pid = fork){
say "[c] $$: starting job $job in $pid";
$done->begin;
$timers{$pid} = AnyEvent->timer( after => 3, interval => 0, cb => sub {
delete $timers{$pid};
say "[c] $$: Killing $pid: too slow";
kill 9, $pid;
});
$children{$pid} = AnyEvent->child( pid => $pid, cb => sub {
my ($pid, $status) = @_;
delete $timers{$pid};
delete $children{$pid};
say "[c] [j] $$: job $pid exited with status $status";
$error ||= ($status != 0);
$done->end;
});
}
使用计时器比闹钟稍微容易一些,因为它带有状态。每个计时器都知道要杀死哪个进程,当进程成功退出时取消计时器也很容易——我们只需从哈希表中删除它。
这是父进程(的子进程)。子进程(或作业)非常简单:
else {
# run kid
$job->();
exit 0; # just in case
}
如果你想关闭stdin,你也可以在这里关闭。
现在,当所有进程都被生成后,我们通过等待condvar来等待它们全部退出。事件循环将监视子进程和定时器,并为我们执行正确的操作:
} # this is the end of the for @jobs loop
$done->end;
# block until all children have exited
$done->recv;
然后,当所有孩子都离开时,我们可以做任何我们想要的清理工作,比如:
if($error){
say "[c] $$: One of your children died.";
exit 1;
}
else {
say "[c] $$: All jobs completed successfully.";
exit 0;
}
} # end of "else { # child"
} # end of start_child
好的,现在我们需要翻译的是关于IT技术方面的内容。这段文字讲述了如何编写父进程,相较于子进程和孙子进程/任务,编写父进程要容易得多。
和子进程一样,我们将使用计数条件变量来等待子进程完成。
# main program
my $all_done = AnyEvent->condvar;
我们需要完成一些工作。这里有一个任务总是成功的,还有一个任务如果您按回车键将会成功,但如果您让它被定时器杀死,那么它将失败。
my $good_grandchild = sub {
exit 0;
};
my $bad_grandchild = sub {
my $line = <STDIN>;
exit 0;
};
那么我们只需要启动子作业。如果你还记得在 start_child
顶部的位置,它需要两个回调函数,一个错误回调和一个成功回调。我们将设置这些回调函数; 错误回调将打印“not ok”并将 condvar 减去,而成功回调将打印“ok”并执行相同的操作。非常简单。
my $ok = sub { $all_done->end; say "$$: $_[0] ok" };
my $nok = sub { $all_done->end; say "$$: $_[0] not ok" };
然后我们可以启动一系列的工作,包括更多的子任务和孙任务:
say "starting...";
$all_done->begin for 1..4;
start_child $ok, $nok, ($good_grandchild, $good_grandchild, $good_grandchild);
start_child $ok, $nok, ($good_grandchild, $good_grandchild, $bad_grandchild);
start_child $ok, $nok, ($bad_grandchild, $bad_grandchild, $bad_grandchild);
start_child $ok, $nok, ($good_grandchild, $good_grandchild, $good_grandchild, $good_grandchild);
其中两个将超时,两个将成功。如果您在它们运行时按下回车键,则它们可能全部成功。
无论如何,一旦它们开始,我们只需要等待它们完成:
$all_done->recv;
say "...done";
exit 0;
这就是整个程序的内容。
我们没有像Parallel::ForkManager那样对fork进行“速率限制”,以使得每次只有n
个子进程在运行。不过,手动实现这一点也很容易:
use Coro;
use AnyEvent::Subprocess;
use Coro::Semaphore;
my $job = AnyEvent::Subprocess->new(
on_completion => sub {},
code => sub { the child process };
)
my $rate_limit = Coro::Semaphore->new(3);
my @coros = map { async {
my $guard = $rate_limit->guard;
$job->clone( on_completion => Coro::rouse_cb )->run($_);
Coro::rouse_wait;
}} ({ args => 'for first job' }, { args => 'for second job' }, ... );
my @results = map { $_->join } @coros;
这里的优势是您可以在子进程运行时做其他事情 - 只需要使用
async
生成更多的线程,然后进行阻塞连接。使用AnyEvent::Subprocess,您可以对子进程进行更多控制 - 您可以在Pty中运行子进程并提供stdin(如Expect),并且可以捕获其stdin、stdout和stderr,或者可以忽略这些内容,或者任何其他想法。您可以自行决定,而不是一些试图使事情 "简单化 "的模块作者。希望这可以帮到您。
system "perl -le '<STDIN>'"
超时吗?我发现它会立即挂起生成它的进程,直到进程唤醒后 SIGALRM 才会被发送。 - mob