从Perl脚本异步执行Bash命令

3

我需要运行一个Bash命令,但是这个命令需要几分钟才能运行完毕。

如果我正常执行这个命令(同步执行),我的应用程序会一直等待命令完成。

如何在Perl脚本中异步运行Bash命令?


你关心bash的输出吗? - mpapec
是的,我想要结果,什么时候可以得到?就像事件触发一样,当命令完成后,它会向我的应用程序发出警报。 - The Anh Nguyen
以下是一个使用线程的示例,您可以从中获取脚本输出,但有时必须手动检查脚本是否已完成。 - mpapec
3个回答

3
如果您不关心结果,只需使用system("my_bash_script &");即可。它会立即返回并执行所需的脚本。
我有两个文件:
$ cat wait.sh
#!/usr/bin/bash
for i in {1..5}; { echo "wait#$i"; sleep 1;}

$cat wait.pl
#!/usr/bin/perl
use strict; use warnings;
my $t = time;
system("./wait.sh");
my $t1 = time;
print $t1 - $t, "\n";
system("./wait.sh &");
print time - $t1, "\n";

输出:

wait#1
wait#2
wait#3
wait#4
wait#5
5
0
wait#1
wait#2
wait#3
wait#4
wait#5

可以看到第二次调用立即返回,但它仍然在向标准输出写入内容。
如果需要与子进程通信,则需要使用fork并重定向STDINSTDOUT(以及STDERR)。或者您可以使用IPC::Open2IPC::Open3包。无论如何,在调用程序退出之前等待子进程退出始终是一个好习惯。
如果要等待执行的进程,可以尝试在Bash中使用以下代码:
#!/usr/bin/bash

cpid=()
for exe in script1 script2 script3; do
  $exe&
  cpid[$!]="$exe";
done

while [ ${#cpid[*]} -gt 0 ]; do
  for i in ${!cpid[*]}; do
    [ ! -d /proc/$i ] && echo UNSET $i && unset cpid[$i]
  done
  echo DO SOMETHING HERE; sleep 2
done

这个脚本首先异步地启动脚本#,并将pids存储在名为cpid的数组中。然后有一个循环,它浏览它们仍在运行(/proc/存在)。如果不存在,则显示文本UNSET <PID>并从数组中删除PID。

它不是万无一失的,因为如果DO SOMETHING HERE部分运行时间很长,那么相同的PID可以被添加到另一个进程。但是它在平均环境下运行良好。

但是这种风险也可以减少:

#!/usr/bin/bash

# Enable job control and handle SIGCHLD
set -m
remove() {
  for i in ${!cpid[*]}; do
    [ ! -d /proc/$i ] && echo UNSET $i && unset cpid[$i] && break
  done
}
trap "remove" SIGCHLD

#Start background processes
cpid=()
for exe in "script1 arg1" "script2 arg2" "script3 arg3" ; do
  $exe&
  cpid[$!]=$exe;
done

#Non-blocking wait for background processes to stop
while [ ${#cpid[*]} -gt 0 ]; do
  echo DO SOMETHING; sleep 2
done

这个版本使得脚本在异步子进程退出时能够接收到SIGCHLD信号。如果接收到了SIGCHLD信号,它会异步地寻找第一个不存在的进程。等待的while循环大大减少。


另一个选项是 open(my $fh, '|-', 'wait.sh', ...)open(my $fh, '-|', 'wait.sh', ...),如果你想要读取其输出或写入其输入,但不需要两者都有。 - reinierpost
不,你没有理解我@TrueY,我的bash命令需要很长时间才能运行,所以当bash命令正在执行时,我希望我的应用程序显示文本“正在执行...”,并在命令完成后,系统触发(通知)我的应用程序显示“执行完成!”。 - The Anh Nguyen
@ThếAnhNguyễn:不好意思,我误解了你的问题,因为你的描述信息太少。我曾经回答 stackoverflow 上的一个问题,它可以在 bash 中运行特定数量的并行程序。我会根据你的需求进行调整。 - TrueY

3
您可以使用线程来异步启动Bash,
use threads;
my $t = async {
  return scalar `.. long running command ..`;
};

然后手动测试线程是否准备好加入,并以非阻塞的方式获取输出。

my $output = $t->is_joinable() && $t->join();

我有一个小问题。如果在async行之后使用$t,那么perl会返回一个错误:Global symbol "$t" requires explicit package name。在第一次使用$t之前加上sleep 1;可以解决这个问题,但是有没有更好的处理方法呢? - TrueY
my $t = async { return `./wait.sh`; } print $t; 如果在这两行代码之间添加`sleep 1`,那么它就可以正常工作。 - TrueY
我遇到了 Perl exited with active threads: 的问题,但你不需要使用 sleep 来避免这种情况,因为 my $foo = $t->join(); 会阻塞直到线程准备好加入。 - mpapec
嗯...$t->join;也不起作用,因为$t不可见(相同的错误消息)。在cygwinCentOS 5.7下也是如此。 - TrueY
@TrueY,您确定$t->join$t的作用域内吗? - aschepler

3

通常的做法是使用 fork。你需要让你的脚本进行分叉,然后子进程会调用 Bash 脚本上的 execsystem 命令(具体取决于子进程是否需要处理 Bash 脚本的返回码或与其交互)。

然后,父进程可能需要结合使用 wait 和/或 SIGCHILD 处理程序。

如何处理它的具体细节很大程度上取决于你的情况和确切需求。


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