并行运行bash命令,跟踪结果和计数

时间:2011-06-17 09:45:45

标签: bash

我想知道如果可能的话,我可以在BASH中创建一个简单的作业管理来并行处理多个命令。也就是说,我有一大堆要运行的命令,我希望在任何给定的时间运行其中两个。

我对bash有很多了解,所以这里有一些要求让它变得棘手:

  • 命令具有可变的运行时间,所以我不能只生成2,等待,然后继续下两个。一旦完成一个命令,就必须运行下一个命令。
  • 控制过程需要知道每个命令的退出代码,以便它可以保持总共多少个失败

我在想我可以使用trap,但我没有看到一个简单的方法来获取处理程序中子项的退出值。

那么,关于如何做到这一点的任何想法?


嗯,这里有一些概念代码可能有用,但它打破了bash:无效的命令行生成,挂起,有时是核心转储。

# need monitor mode for trap CHLD to work
set -m
# store the PIDs of the children being watched
declare -a child_pids

function child_done
{
    echo "Child $1 result = $2"
}

function check_pid
{
    # check if running
    kill -s 0 $1
    if [ $? == 0 ]; then
        child_pids=("${child_pids[@]}" "$1")
    else
        wait $1
        ret=$?
        child_done $1 $ret
    fi
}

# check by copying pids, clearing list and then checking each, check_pid
# will add back to the list if it is still running
function check_done
{
    to_check=("${child_pids[@]}")
    child_pids=()

    for ((i=0;$i<${#to_check};i++)); do
        check_pid ${to_check[$i]}
    done
}

function run_command
{
    "$@" &
    pid=$!
    # check this pid now (this will add to the child_pids list if still running)
    check_pid $pid
}

# run check on all pids anytime some child exits
trap 'check_done' CHLD

# test
for ((tl=0;tl<10;tl++)); do
    run_command bash -c "echo FAIL; sleep 1; exit 1;"
    run_command bash -c "echo OKAY;"
done

# wait for all children to be done
wait

请注意,这不是我最终想要的,但却是获得我想要的基础。


跟进:我已经在Python中实现了一个系统。所以任何使用Python编写脚本的人都可以拥有上述功能。请参阅shelljob

7 个答案:

答案 0 :(得分:23)

GNU Parallel非常棒:

$ parallel -j2 < commands.txt
$ echo $?

它会将退出状态设置为失败的命令数。如果您有超过253个命令,请查看--joblog。如果您事先不知道所有命令,请查看--bg

答案 1 :(得分:7)

我可以说服你使用make吗?这样做的好处是,您可以告诉它并行运行多少命令(修改-j号)

echo -e ".PHONY: c1 c2 c3 c4\nall: c1 c2 c3 c4\nc1:\n\tsleep 2; echo c1\nc2:\n\tsleep 2; echo c2\nc3:\n\tsleep 2; echo c3\nc4:\n\tsleep 2; echo c4" | make -f - -j2

将其粘贴在Makefile中,它将更具可读性

.PHONY: c1 c2 c3 c4
all: c1 c2 c3 c4
c1:
        sleep 2; echo c1
c2:
        sleep 2; echo c2
c3:
        sleep 2; echo c3
c4:
        sleep 2; echo c4

请注意,这些不是行首的空格,它们是TAB,因此剪切和粘贴在这里不起作用。

放一个&#34; @&#34;如果你没有回应命令,则在每个命令的前面。 e.g:

        @sleep 2; echo c1

这将在第一个失败的命令上停止。如果您需要计算失败,您需要以某种方式在makefile中对其进行设计。也许像是

command || echo F >> failed

然后检查失败的长度。

答案 2 :(得分:4)

您遇到的问题是您无法等待多个后台进程中的一个完成。如果您观察作业状态(使用作业),则会从作业列表中删除已完成的后台作业。您需要另一种机制来确定后台作业是否已完成。

以下示例使用开始后台进程(休眠)。然后使用ps循环查看它们是否仍在运行。如果没有,则使用wait来收集退出代码并开始新的后台进程。

#!/bin/bash

sleep 3 &
pid1=$!
sleep 6 &
pid2=$!

while ( true ) do
    running1=`ps -p $pid1 --no-headers | wc -l`
    if [ $running1 == 0 ]
    then
        wait $pid1
        echo process 1 finished with exit code $?
        sleep 3 &
        pid1=$!
    else
        echo process 1 running
    fi

    running2=`ps -p $pid2 --no-headers | wc -l`
    if [ $running2 == 0 ]
    then
        wait $pid2
        echo process 2 finished with exit code $?
        sleep 6 &
        pid2=$!
    else
        echo process 2 running
    fi
    sleep 1
done

编辑:使用SIGCHLD(不进行轮询):

#!/bin/bash

set -bm
trap 'ChildFinished' SIGCHLD

function ChildFinished() {
    running1=`ps -p $pid1 --no-headers | wc -l`
    if [ $running1 == 0 ]
    then
        wait $pid1
        echo process 1 finished with exit code $?
        sleep 3 &
        pid1=$!
    else
        echo process 1 running
    fi

    running2=`ps -p $pid2 --no-headers | wc -l`
    if [ $running2 == 0 ]
    then
        wait $pid2
        echo process 2 finished with exit code $?
        sleep 6 &
        pid2=$!
    else
        echo process 2 running
    fi
    sleep 1
}

sleep 3 &
pid1=$!
sleep 6 &
pid2=$!

sleep 1000d

答案 3 :(得分:2)

我认为以下示例回答了您的一些问题,我正在研究其余的问题

(cat list1 list2 list3 | sort | uniq > list123) &
(cat list4 list5 list6 | sort | uniq > list456) &

从:

Running parallel processes in subshells

答案 4 :(得分:0)

还有另一个名为 xjobs 的debian系统软件包。

您可能需要查看它:

http://packages.debian.org/wheezy/xjobs

答案 5 :(得分:0)

如果由于某种原因无法安装parallel,则可以使用普通shell或bash

# String to detect failure in subprocess
FAIL_STR=failed_cmd

result=$(
    (false || echo ${FAIL_STR}1) &
    (true  || echo ${FAIL_STR}2) &
    (false || echo ${FAIL_STR}3)
)
wait

if [[ ${result} == *"$FAIL_STR"* ]]; then
    failure=`echo ${result} | grep -E -o "$FAIL_STR[^[:space:]]+"`
    echo The following commands failed:
    echo "${failure}"
    echo See above output of these commands for details.
    exit 1
fi

true&amp; false是命令的占位符。你还可以回音$?与FAIL_STR一起获取命令状态。

答案 6 :(得分:0)

另一个 bash 示例,仅供您参考。当然,更喜欢使用 GNU 并行,这将提供更多开箱即用的功能。

此解决方案涉及创建用于收集作业状态的 tmp 文件输出。

我们使用 /tmp/${$}_ 作为临时文件前缀 $$ 是实际的父进程号,对于所有脚本执行都是一样的。

首先是批量启动并行作业的循环。批量大小使用 max_parrallel_connection 设置。 try_connect_DB() 是同一个文件中的慢速 bash 函数。这里我们收集 stdout + stderr 2>&1 用于故障诊断。

nb_project=$(echo "$projects" | wc -w)
i=0
parrallel_connection=0
max_parrallel_connection=10
for p in $projects
do
  i=$((i+1))
  parrallel_connection=$((parrallel_connection+1))
  try_connect_DB $p "$USERNAME" "$pass" > /tmp/${$}_${p}.out 2>&1 &

  if [[ $parrallel_connection -ge $max_parrallel_connection ]]
  then
    echo -n " ... ($i/$nb_project)"
    wait
    parrallel_connection=0
  fi
done
if [[ $nb_project -gt $max_parrallel_connection ]]
then
  # final new line
  echo
fi

# wait for all remaining jobs
wait                                                                                                                                                                                                                                         

运行完所有作业后查看所有结果:

SQL_connection_failed 是我们的错误约定,由 try_connect_DB() 输出,您可以按照最适合您需要的方式过滤作业成功或失败。

这里我们决定只输出失败的结果,以减少大型作业的输出量。特别是如果他们中的大多数或全部成功通过。

# displaying result that failed
file_with_failure=$(grep -l SQL_connection_failed /tmp/${$}_*.out)
if [[ -n $file_with_failure ]]
then
  nb_failed=$(wc -l <<< "$file_with_failure")
  # we will collect DB name from our output file naming convention, for post treatment
  db_names=""
  echo "=========== failed connections : $nb_failed/$nb_project"
  for failure in $file_with_failure
  do
    echo "============ $failure"
    cat $failure
    db_names+=" $(basename $failure | sed -e 's/^[0-9]\+_\([^.]\+\)\.out/\1/')"
  done
  echo "$db_names"
  ret=1
else
  echo "all tests passed"
  ret=0
fi

# temporary files cleanup, could be kept is case of error, adapt to suit your needs.
rm /tmp/${$}_*.out
exit $ret