Perl:当子进程被杀死时,子进程不会被终止

3

正在使用Windows进行此操作。

我遇到了错误:由于另一个进程正在使用该文件,因此无法访问该文件。似乎即使子进程退出(exit 0)并且父进程正在等待子进程完成(waitpid($lkpid, 0)),子进程的子进程也没有被杀死。因此,当下一次迭代(测试用例)运行时,它会发现进程已经在运行,因此会给出错误消息。

代码片段($bashexe和$ bePath已定义):

my $MSROO = "/home/abc";
if (my $fpid = fork()) {
  for (my $i=1; $i<=1200; $i++) {
     sleep 1;
     if (-e "$MSROO/logs/Complete") {
        last;
     }
}
elsif (defined ($fpid)) {
    &runAndMonitor (\@ForRun, "$MSROO/logs/Test.log");  ### @ForRun has the list of test cases
    system("touch $MSROO/logs/Complete");
    exit 0;
}


sub runAndMonitor {
    my @ForRunPerProduct =  @{$_[0]};
    my $logFile = $_[1];
        foreach my $TestVar (@ForRunPerProduct) {
            my $TestVarDirName = $TestVar;
            $TestVarDirName = dirname ($TestVarDirName);
            my $lkpid;
            my $filehandle;       
           if ( !($pid = open( $filehandle, "-|" , " $bashexe -c \" echo abc \; perl.exe reg_script.pl $TestVarDirName -t wint\" >> $logFile "))) {                
                die( "Failed to start process: $!" );
            }
            else {
                print "$pid is pid of shell running: $TestVar\n";   ### Issue (error message above) is coming here after piped open is launched for a new test
                my $taskInfo=`tasklist | grep "$pid"`;
                chomp ($taskInfo);
                print "$taskInfo is taskInfo\n";
            }
            if ($lkpid = fork()) {
                 sleep 1;
                 chomp ($lkpid);
                 LabelToCheck:
                 my $pidExistingOrNotInParent = kill 0, $pid;
                 if ($pidExistingOrNotInParent) {
                     sleep 10;
                     goto LabelToCheck;
                 }
            }
            elsif (defined ($lkpid)) {
                 sleep 12;   
                 my $pidExistingOrNot = kill 0, $pid;
                 if ($pidExistingOrNot){
                      print "$pid still exists\n";
                      my $taskInfoVar1 =`tasklist | grep "$pid"`;
                      chomp ($taskInfoVar1);
                      my $killPID = kill 15, $pid;
                      print "$killPID is the value of PID\n";  ### Here, I am getting output 1 (value of $killPID). Also, I tried with signal 9, and seeing same behavior
                      my $taskInfoVar2 =`tasklist | grep "$pid"`;
                      sleep 10;
                      exit 0;
                 }
            }
             system("TASKKILL /F /T /PID $lkpid") if ($lkpid);   ### Here, child pid is not being killed . Saying "ERROR: The process "-1472" not found"
             sleep 2;
             print "$lkpid is lkpid\n"; ## Here, though I am getting message "-1472 is lkpid"
             #waitpid($lkpid, 0);
             return;
}

为什么即使在“子进程中输入0”和“父进程中等待pid”的情况下,子进程仍未被杀死?如何彻底清理子进程及其子进程?

如果我理解正确的话,您似乎期望exit(0)会杀死进程的子进程?实际上并不是这样的。我不确定是什么让您有了这个想法。 - ikegami
@ikegami,我想要杀死子进程和子程序。怎么做呢?明白了,exit 0并不能杀死子进程。 - PPP
2个回答

4

exit 命令不会影响子进程,它只是退出当前进程。如果要关闭子进程,需要向它们发送信号。

然而,由于这是在 Windows 上模拟的 fork,因此 perlfork 给出了以下建议。

其他Perl特性在forked伪进程中的行为
...
kill() 可以使用"kill('KILL', ...)"通过传递fork()返回的ID来终止伪进程。在伪进程上执行kill的结果是不可预测的,并且除非在紧急情况下,否则不应该使用它,因为当运行线程被终止时,操作系统可能无法保证进程资源的完整性。
...
exit() exit()总是仅退出正在执行的伪进程,然后自动等待任何未完成的子伪进程。请注意,这意味着除非所有正在运行的伪进程都已退出,否则整个进程将不会退出。有关打开文件句柄的一些限制,请参见下文。

因此不要使用kill,而exit的行为几乎与您需要的相反。

但是Windows命令TASKKILL可以终止进程及其树形结构。

system("TASKKILL /F /T /PID $pid");

这应该终止一个进程及其子进程,$pid 可以是进程的ID号,也可以使用进程的名称来代替,TASKKILL /F /T /IM $name,但在忙碌的现代系统上,使用名称可能有些棘手。请参见MS文档中的taskkill

总的来说,更可靠的方法可能是使用专门的Windows进程管理模块。

还有一些其他的评论

  • I also notice that you use pipe-open, while perlfork says for that

    Forking pipe open() not yet implemented
    The open(FOO, "|-") and open(BAR, "-|") constructs are not yet implemented.

    So I am confused, does that pipe-open work in your code? But perlfork continues with

    This limitation can be easily worked around in new code by creating a pipe explicitly. The following example shows how to write to a forked child: [full code follows]

  • That C-style loop, for (my $i=1; $i<=1200; $i++), is better written as

    for my $i (1..1200) { ... }
    

    (or foreach, synonyms) A C-style loop is very rarely needed in Perl.


在Linux中,使用带有负信号(名称或编号)或进程ID的kill通常会终止被信号进程下的整个进程树。

因此,一种方法是在子进程准备就绪时从其父进程向其发出信号,而不是从其退出。 (然后子进程需要以某种方式向父进程发出准备就绪信号。)

或者,子进程可以向其所有直接子进程发送负终止信号,然后退出。


我已经尝试过了。根据我的问题代码,执行"system("TASKKILL /F /T /PID $pid");"会杀死$pid。但是执行"system("TASKKILL /F /T /PID $lkpid");"时,$lkpid(子进程PID)没有被杀死。我收到了消息:"ERROR: The process "-1472" not found. -1472是lkpid。 - PPP
@PPP 好的,所以这是你刚刚添加的代码(在最后一次编辑中)?我不确定整体逻辑和程序流程,但显然有些东西出了问题:错误显示没有进程 - 并显示一个负进程ID,这发生在fork无法启动进程时。所以这不是关于进程未被杀死,而是fork本身就失败了。我需要仔细查看你的程序... - zdim
(我的意思是添加的代码显示了这一点。由于我在跟踪代码时遇到了麻烦,所以我不知道问题出在哪里。我会再试着看看。) - zdim

0

您没有说明使用的是哪个Perl版本。在Windows上使用Strawberry Perl(以及可能的Active State),fork()模拟非常有问题,就像@zdim提到的那样,可能只是“损坏”的。如果您想要更长的解释,请参见Proc::Background::Win32 - Perl Fork Limitations

同时,如果您使用Cygwin的Perl,则fork可以完美地工作。这是因为Cygwin对Unix fork()语义进行了完全模拟,因此针对cygwin构建的任何内容都与在Unix上的操作方式相同。缺点是文件路径显示出现奇怪的情况,例如/cygdrive/c/Program Files。这可能会影响您已经编写的代码。

但是,您可能也会对进程树感到困惑。即使在Unix上,杀死父进程并不会杀死子进程。这通常发生有各种原因,但并没有强制执行。例如,大多数子进程都与父进程打开了一个管道,当父进程退出时,该管道关闭,然后读/写该管道会产生SIGPIPE信号,从而杀死子进程。在其他情况下,父进程捕获SIGTERM,然后在优雅地退出之前向其子进程重新广播该信号。在其他情况下,像Systemd或Docker这样的监视器创建一个由主进程的所有子进程继承的容器,当主进程退出时,监视器会杀死容器中的所有其他进程。

看起来你正在编写自己的任务监视器,我会给出一些建议,这是我为Windows编写的一个监视器(多年后仍在运行)。最终我采用了使用Proc::Background的设计,其中父进程启动一个任务,将输出写入文件作为STDOUT/STDERR。然后它打开同一个日志文件,并每隔几秒钟唤醒一次以尝试读取更多的日志文件,以查看任务正在做什么,并检查Proc::Background对象以查看任务是否已退出。当任务退出时,它将退出代码和时间戳附加到日志文件中。监视器具有超时设置,如果子进程超过该设置,它将不优雅地运行TerminateProcess。(你可以通过将STDIN保持为监视器和工作进程之间的管道并让工作进程不时地检查STDIN来改进它,但在Windows上,这将阻塞,因此必须使用PeekNamedPipe,这会变得混乱)

同时,监视器会解析日志文件的新行以读取状态信息并将更新发送到数据库。系统的其他部分可以观察数据库,以查看后台任务的状态,包括一个 Web 管理界面,它也可以打开和读取日志文件。如果监视器发现某个子进程运行时间过长,它可以使用 TerminateProcess 停止它。该设计缺少的是监视器知道何时被要求退出并清除的方法,这是一个明显的缺陷,可能正是您正在寻找的。不过,实际上并没有办法拦截针对父进程的 TerminateProcess!Windows确实有一些消息队列 API 的东西,可以设置接收关于终止的通知,但我从未追踪过其中的全部详细信息。如果您了解,请回来给我留下评论 :-)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接