收集并行后台进程的退出代码(子shell)


18

假设我们有一个bash脚本,如下所示:

echo "x" &
echo "y" &
echo "z" &
.....
echo "Z" &
wait

有没有办法收集子外壳程序/子进程的退出代码?寻找这样做的方法,找不到任何东西。我需要并行运行这些子shell,否则,这会更容易。

我正在寻找一个通用的解决方案(我有一个未知/动态数量的子进程可以并行运行)。


1
我将建议您弄清楚您想要的是什么,然后问一个新问题,尝试明确您要查找的行为(也许使用伪代码或更大的示例)。
Michael Homer

3
我实际上认为现在的问题很好-我有数量动态的子流程。我需要收集所有退出代码。就这样。
亚历山大·米尔斯

Answers:


6

亚历山大·米尔斯(Alexander Mills)使用handleJobs的答案给了我一个很好的起点,但也给了我这个错误

警告:run_pending_traps:trap_list [17]中的值错误:0x461010

这可能是一场激烈的比赛条件问题

相反,我只是存储每个孩子的pid,然后等待,并专门获取每个孩子的退出代码。我从子流程的角度来看,找到了这种更清洁的方法,这些子流程在函数中产生了子流程,并且避免了在我打算等待子进程的情况下等待父进程的风险。由于不使用陷阱,因此会更清楚发生什么情况。

#!/usr/bin/env bash

# it seems it does not work well if using echo for function return value, and calling inside $() (is a subprocess spawned?) 
function wait_and_get_exit_codes() {
    children=("$@")
    EXIT_CODE=0
    for job in "${children[@]}"; do
       echo "PID => ${job}"
       CODE=0;
       wait ${job} || CODE=$?
       if [[ "${CODE}" != "0" ]]; then
           echo "At least one test failed with exit code => ${CODE}" ;
           EXIT_CODE=1;
       fi
   done
}

DIRN=$(dirname "$0");

commands=(
    "{ echo 'a'; exit 1; }"
    "{ echo 'b'; exit 0; }"
    "{ echo 'c'; exit 2; }"
    )

clen=`expr "${#commands[@]}" - 1` # get length of commands - 1

children_pids=()
for i in `seq 0 "$clen"`; do
    (echo "${commands[$i]}" | bash) &   # run the command via bash in subshell
    children_pids+=("$!")
    echo "$i ith command has been issued as a background job"
done
# wait; # wait for all subshells to finish - its still valid to wait for all jobs to finish, before processing any exit-codes if we wanted to
#EXIT_CODE=0;  # exit code of overall script
wait_and_get_exit_codes "${children_pids[@]}"

echo "EXIT_CODE => $EXIT_CODE"
exit "$EXIT_CODE"
# end

为了清晰起见,我认为for job in "${childen[@]}"; do应该很酷for job in "${1}"; do
Alexander Mills

我对此脚本唯一关心的是,如果children_pids+=("$!")实际上正在捕获子外壳所需的pid。
亚历山大·米尔斯

1
我使用“ $ {1}”进行了测试,但是它不起作用。我将数组传递给函数,显然在bash中需要特别注意。$!是上一次产生的作业的pid,请参阅tldp.org/LDP/abs/html/internalvariables.html在我的测试中似乎可以正常工作,现在我在unRAID cache_dirs脚本中使用它,并且似乎可以它的工作。我正在使用bash 4.4.12。
arberg '18 -4-12

是的,不错,看来您是对的
Alexander Mills

20

使用wait具有PID,这将:

等待直到由每个进程ID pid或作业规范jobspec指定的子进程退出,并返回等待的最后一个命令的退出状态。

进行时,您需要保存每个进程的PID:

echo "x" & X=$!
echo "y" & Y=$!
echo "z" & Z=$!

您还可以在脚本中使用set -m和使用%njobspec 来启用作业控制,但是您几乎肯定不希望作业控制还有很多其他副作用

wait将返回与完成过程相同的代码。您可以wait $X在以后的任何(合理)时间使用来访问最终代码,$?也可以将其用作true / false:

echo "x" & X=$!
echo "y" & Y=$!
...
wait $X
echo "job X returned $?"

wait 将暂停直到命令完成(如果尚未执行)。

如果要避免这样的停顿,可以设置traponSIGCHLD,计算终止的数量,并wait在所有结束时立即处理所有。您wait几乎可以一直独自使用,从而摆脱困境。


1
嗯,对不起,我需要并行运行这些子shell,我将在问题中具体说明……
Alexander Mills

没关系,也许这适用于我的设置...代码中的wait命令在哪里起作用?我不关注
Alexander Mills

1
@AlexanderMills它们正在并行运行。如果它们的数量可变,请使用数组。(例如,此处可能是重复的)。
Michael Homer

是的,谢谢,我会检查一下,如果wait命令与您的答案有关,那么请添加它
Alexander Mills

您可以wait $X在任何(合理的)稍后时间运行。
Michael Homer

5

如果您有识别命令的好方法,则可以将其退出代码打印到tmp文件中,然后访问您感兴趣的特定文件:

#!/bin/bash

for i in `seq 1 5`; do
    ( sleep $i ; echo $? > /tmp/cmd__${i} ) &
done

wait

for i in `seq 1 5`; do # or even /tmp/cmd__*
    echo "process $i:"
    cat /tmp/cmd__${i}
done

不要忘记删除tmp文件。


4

使用compound command-将语句放在括号中:

( echo "x" ; echo X: $? ) &
( true ; echo TRUE: $? ) &
( false ; echo FALSE: $? ) &

将给出输出

x
X: 0
TRUE: 0
FALSE: 1

并行运行多个命令的一种真正不同的方法是使用GNU Parallel。列出要运行的命令列表,并将其放入文件中list

cat > list
sleep 2 ; exit 7
sleep 3 ; exit 55
^D

并行运行所有命令,并在文件中收集退出代码job.log

cat list | parallel -j0 --joblog job.log
cat job.log

输出为:

Seq     Host    Starttime       JobRuntime      Send    Receive Exitval Signal  Command
1       :       1486892487.325       1.976      0       0       7       0       sleep 2 ; exit 7
2       :       1486892487.326       3.003      0       0       55      0       sleep 3 ; exit 55

好的,谢谢,有没有一种方法可以使之泛化?我不仅有3个子流程,而且还有Z个子流程。
亚历山大·米尔斯

我更新了原始问题以反映我在寻找通用解决方案,谢谢
Alexander Mills

一种泛化的方法可能是使用循环构造?
亚历山大·米尔斯

循环播放?您有固定的命令列表还是由用户控制?我不确定我了解您要做什么,但是也许PIPESTATUS您应该检查一下。这seq 10 | gzip -c > seq.gz ; echo ${PIPESTATUS[@]}将返回0 0(第一个和最后一个命令的退出代码)。
hschou

是的,基本上是由用户控制的
Alexander Mills

2

这是您要查找的通用脚本。唯一的缺点是您的命令用引号引起来,这意味着通过IDE突出显示语法不会真正起作用。否则,我尝试了其他几个答案,这是最好的答案。这个答案包含了wait <pid>@Michael给出的使用想法,但是通过使用trap似乎效果最好的命令又走了一步。

#!/usr/bin/env bash

set -m # allow for job control
EXIT_CODE=0;  # exit code of overall script

function handleJobs() {
     for job in `jobs -p`; do
         echo "PID => ${job}"
         CODE=0;
         wait ${job} || CODE=$?
         if [[ "${CODE}" != "0" ]]; then
         echo "At least one test failed with exit code => ${CODE}" ;
         EXIT_CODE=1;
         fi
     done
}

trap 'handleJobs' CHLD  # trap command is the key part
DIRN=$(dirname "$0");

commands=(
    "{ echo 'a'; exit 1; }"
    "{ echo 'b'; exit 0; }"
    "{ echo 'c'; exit 2; }"
)

clen=`expr "${#commands[@]}" - 1` # get length of commands - 1

for i in `seq 0 "$clen"`; do
    (echo "${commands[$i]}" | bash) &   # run the command via bash in subshell
    echo "$i ith command has been issued as a background job"
done

wait; # wait for all subshells to finish

echo "EXIT_CODE => $EXIT_CODE"
exit "$EXIT_CODE"
# end

感谢@michael homer使我步入正轨,但是使用trap命令是AFAICT的最佳方法。


1
您也可以使用SIGCHLD陷阱在子项退出时对其进行处理,例如打印出当时的状态。或更新进度计数器:声明一个函数,然后使用“ trap function_name CHLD”,尽管这可能还需要在非交互式外壳中打开一个选项,例如可能是“ set -m”
Chunko

1
同样,“ wait -n”将等待任何孩子,然后在$中返回该孩子的退出状态?变量。因此,您可以在每个退出时打印进度。但是请注意,除非您使用CHLD陷阱,否则您可能会错过某些子出口。
春子

@Chunko谢谢!那是很好的信息,您能否用您认为最好的东西来更新答案?
亚历山大·米尔斯

谢谢@Chunko,陷阱效果更好,您是对的。通过等待<pid>,我陷入了失败。
亚历山大·米尔斯

您能否解释一下为什么以及为什么您相信带有陷阱的版本比没有陷阱的版本更好?(我相信这不会更好,因此会变得更糟,因为它更复杂,没有任何好处。)
Scott

1

@rolf答案的另一种变化:

保存退出状态的另一种方法是

mkdir /tmp/status_dir

然后有每个脚本

script_name="${0##*/}"  ## strip path from script name
tmpfile="/tmp/status_dir/${script_name}.$$"
do something
rc=$?
echo "$rc" > "$tmpfile"

这为每个状态文件提供了唯一的名称,包括创建该文件的脚本的名称及其进程ID(以防正在运行同一脚本的多个实例),您可以将其保存以供日后参考,并将其全部放入放在同一位置,这样您就可以在完成后删除整个子目录。

您甚至可以通过以下操作从每个脚本中保存多个状态:

tmpfile="$(/bin/mktemp -q "/tmp/status_dir/${script_name}.$$.XXXXXX")"

它会像以前一样创建文件,但是会向其中添加一个唯一的随机字符串。

或者,您可以仅将更多状态信息附加到同一文件。


1

script3仅在script1script2成功时才会执行,script1并且script2将并行执行:

./script1 &
process1=$!

./script2 &
process2=$!

wait $process1
rc1=$?

wait $process2
rc2=$?

if [[ $rc1 -eq 0 ]] && [[ $rc2 -eq 0  ]];then
./script3
fi

AFAICT,这无非是对迈克尔·荷马的答案的重提。
斯科特(Scott)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.