【问题标题】:xargs exits without waiting for subprocess outputxargs 退出而不等待子进程输出
【发布时间】:2021-11-19 02:18:50
【问题描述】:

只有在所有子流程(任务)完成后,我的脚本才应该退出。我使用xargs 并行运行任务。如果任务以错误结束,它应该等待所有正在运行的任务完成,但不应启动新任务。我在这里运行 3 个任务:sleep 4sleep 2sleep 1。并行任务不超过 2 个。 sleep 1 任务崩溃,但由于某种原因 xargs 没有等待 sleep 4 完成,提前退出。

#!/usr/bin/env bash
set -eu -o pipefail

function foo() {
  local sec="$1"
  echo "start foo $sec"
  sleep "$sec"
  echo "finished foo $sec"
  if ((sec == 1)); then
    return 1
  fi
}

export -f foo

echo "starting..."
printf '%s\0' 4 2 1 | xargs -t -0 -I{} -P 2 bash -c 'foo "{}" || exit 255' || echo "finished early, exit_code=$?"
echo "finished"
❯ ./测试员 开始... bash -c 'foo "4" ||出口 255' bash -c 'foo "2" ||出口 255' 开始 foo 4 开始 foo 2 完成 foo 2 bash -c 'foo "1" ||出口 255' 开始富 1 完成 foo 1 xargs: bash: 以状态 255 退出;中止 提前结束,exit_code=124 完成的 .. 最后一个命令花了 3 秒 ❯ 完成 foo 4

在我看到 bash 提示符后打印最后一行。有趣的是,如果我尝试运行 4 个任务(4、2、1、5),代码会按预期工作:

printf '%s\0' 4 2 1 5 | xargs -t -0 -I{} -P 2 bash -c 'foo "{}" || exit 255' || echo "finished early, exit_code=$?"

这是 xargs 中的错误,还是我做错了什么?

更新:xargs 版本:(GNU findutils) 4.7.0 在 Linux Mint 20.2 上

【问题讨论】:

  • 请注意,xargs -I{} bash -c '...{}...' 会带来严重的安全风险,无论您在 {} 周围使用何种引用。考虑数据项$(rm -rf ~)'$(rm -rf ~)'——它在所有可能的上下文中执行(不带引号的、单引号的、双引号的)。
  • (作为另一个没有严格解决您的问题的旁白:我也强烈建议不要使用 set -e - 它的行为在 shell 之间以及同一 shell 的各个版本之间差异很大,使得代码难以检查正确性;请参阅exercise section of BashFAQ #105)
  • 另外,我强烈建议printf '%s\0' 4 2 1 5。格式字符串会根据需要重复多次以使用所有参数。此外,您希望有一个最终的 NUL - NUL 是终止符,而不是分隔符。就像 read 如果末尾没有换行符(如果末尾没有 NUL 则 read -d '')返回非零退出状态一样,当您没有最终分隔符时 xargs 的行为也没有明确定义。
  • 另外,回复:function foo() {,参见wiki.bash-hackers.org/scripting/obsolete
  • (回到我之前开始的切线:作为xargs -I{} bash -c '...{}...' 的更安全替代方案,请考虑xargs bash -c 'for arg; do foo "$arg" || exit 255; done' _;它也更有效,因为您可以将更多项目传递给每个bash 副本 - - 使用 xargs -n 参数调整数量 - 并减少支付 shell 启动成本)。

标签: bash xargs


【解决方案1】:

fwiw,我能够重现 OP 的输出...

感兴趣的:

  • OP 提到:sleep 1 任务崩溃,但由于某种原因 xargs 没有等待 sleep 4 完成
  • 输出(OP 和我的)将stderr 上的最后一条消息显示为 xargs: bash: exited with status 255;中止
  • 根据man xargs

如果命令的任何调用以状态 255 退出,xargs 将立即停止而不读取任何进一步的输入。一个错误 发生这种情况时,会在 stderr 上发出消息。

  • 我们还在输出中看到:exit_code=124,它也与手册页中的 xargs 退出代码匹配:

如果命令以状态 255 退出,则为 124

在我看来,真正发生的事情是:

  • 命令“崩溃”,状态为 255
  • xargs 看到命令崩溃并报告问题(即,stderr 上的最后一条消息)
  • xargs 立即停止(根据文档;与 OP 的评论相反 xargs 不等待)但是 ...
  • 任何已调用的命令(在本例中为 foo())继续运行(即,xargs 生成的进程不会收到终止/停止信号)
  • 由于xargs 停止,我们立即发现自己回到了命令提示符
  • 仍在运行的命令 (foo()) 最终完成,该命令的输出现在显示在控制台/终端中

根据man xargs,看起来xargs 正在按设计运行。

fwiw,我的xargs 版本:

$ xargs --version
xargs (GNU findutils) 4.8.0

解决 OP 的问题/评论:

为什么printf '%s\0' 4 2 1 5 ... 没有表现出同样的行为?

事实上它确实表现出相同的行为:

$ ./tester
starting...
bash -c 'foo "4" || exit 255'
bash -c 'foo "2" || exit 255'
start foo 4
start foo 2
finished foo 2
bash -c 'foo "1" || exit 255'
start foo 1
finished foo 1                                     # last foo call finishes well before the script finishes
xargs: bash: exited with status 255; aborting      # xargs sees a 255 and aborts
finished foo 4                                     # even this longer foo call finishes before the script finishes
finished early, exit_code=124                      # xargs exit code = 124
finished
$                                                  # nothing shows up 'after' our script finishes

注意:

  • foo 5 未被调用
  • 所有其他foo() 调用完成之前脚本完成因此没有输出“之后”脚本完成

最终结果,我们有一个竞争条件,即所有foo() 调用恰好在脚本完成之前完成

如果我们切换 args 以使最后一个 foo() 调用休眠超过几秒钟,我们可以看到这种竞争条件发生了切换,例如,我们看到 printf '%s\0' 4 2 5 1 ...

$ ./tester
starting...
bash -c 'foo "4" || exit 255'
bash -c 'foo "2" || exit 255'
start foo 4
start foo 2
finished foo 2
bash -c 'foo "5" || exit 255'
start foo 5
finished foo 4
bash -c 'foo "1" || exit 255'
start foo 1
finished foo 1
xargs: bash: exited with status 255; aborting     # xargs sees a 255 and aborts
finished early, exit_code=124                     # xargs exit code = 124
finished
$                                                 # initially nothing but command prompt but ...

...几秒钟后(现在是孤立的)foo 5 调用完成,我们的控制台/终端看起来像:

... snip ...
finished early, exit_code=124
finished
$ finished foo 5  

至于如何让脚本等待直到所有foo()调用完成,不管xargs是否中止`?

特别感兴趣...

  • xargs 调用被中止时,仍在运行的foo() 调用被有效地孤立并与父脚本解除关联
  • 在脚本末尾添加 wait 无效,因为现在孤立的 foo() 调用不是主脚本的从属

在这一点上,我认为这变成了一个新问题:

  • 如何(重新)编写此脚本,以便在xargs 中止后我们等待任何xargs-spawned 进程完成?

或者:

  • shell 脚本如何等待另一个不相关的操作系统进程完成?

【讨论】:

  • 谢谢,但是您如何解释我帖子底部提到的 4 任务测试 - 为什么正确完成所有输出?
  • @YuriAstrakhan 同样的行为也适用,但是因为最后一次foo() 调用非常短(sleep 1),我们看到最终的foo() 输出在脚本“结束”之前出现在终端中;我已经用更多细节更新了答案...注意foo() 输出行相对于中止xargs 的输出显示的位置...对于整个脚本,我们正在处理结束... 2个“并行”进程中的哪一个首先完成...中止的xargs 或“孤立”的foo() 调用...这取决于foo() 调用的休眠时间
  • 谢谢,但这没有意义 -- 第一个线程 -- 4 秒,第二个线程 -- 2s + 1s 秒。这意味着第二个线程应该比第一个早一秒完成,并且应该立即退出,但事实并非如此。要进行测试,请尝试将第一个值设置为 10 而不是 4,并查看它仍然等到第一个完成。
  • 当然更重要的问题是我将如何做我需要做的事情,而不是为什么它不起作用:)
  • 在我的系统上...切换到 10 2 1 5 和 3 倍以上的 foo 调用 (2/5/10) 在我返回命令提示符后全部打印;再一次,这一切都归结为一个竞争条件...... foo() 调用需要多长时间才能完成与 xargs 中止需要多长时间与谁知道其他 -操作系统调度队列问题可以在系统上运行脚本正在运行的地方
猜你喜欢
  • 2011-07-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-09-24
相关资源
最近更新 更多