执行单个命令时 Bash 会“吞噬”子 shell 进程

11
我意外遇到了一个 bash/sh 行为,想知道其中的道理和提供下面问题的解决方案。
在一个交互式的 bash shell 会话中,我执行: $ bash -c 'sleep 10 && echo' 在 Linux 上使用 ps 命令,看起来像这样: \_ -bash \_ bash -c sleep 10 && echo \_ sleep 10 进程树是我所期望的:
- 我的交互式 bash shell 进程 ($) - 一个子 shell 进程 (bash -c ...) - 一个 sleep 子进程
然而,如果我 bash -c 的命令部分是一个single 命令,例如: $ bash -c 'sleep 10' 那么中间的子 shell 就被忽略了,我的交互式终端会话直接执行 sleep 作为子进程。进程树看起来像这样: \_ -bash \_ sleep 10 因此,从进程树的角度来看,这两个命令产生相同的结果:
- $ bash -c 'sleep 10' - $ sleep 10
这里发生了什么?
现在来回答我的问题:有没有办法强制中间的 shell 进程存在,无论传递给 bash -c ... 的表达式的复杂性如何?
(我可以在实际命令后面添加类似于 ; echo; 这样的内容,这样做“有效”,但我宁愿不这样做。有没有更好的方法来强制中间进程的存在?)
(编辑:在 ps 输出中有一个打字错误;如评论中建议,删除了 sh 标记;还有一个打字错误)

当有可能进行优化时,为什么你不想要它呢? - Charles Duffy
1
主要确保在用户可以传递任意命令的环境中处理子进程时保持一致的行为。我不确定绕过此优化是否是我的解决方案(我遇到的实际问题与sudo的更改有关:https://dev59.com/bZLea4cB1Zd3GeqPyyyZ#34376188)。但这种行为很有趣,我想了解更多相关信息。 - Marco
很棒的问题。您在第一行ps树的第2行中是不是想说\_ bash -c sleep 10 && echo而不是\_ bash -c sleep 10 && sleep 10 - codeforester
@codeforester 确实,谢谢。 - Marco
1个回答

9

实际上,在bash源代码中有一个注释,描述了该功能的很多基本原理:

/* If this is a simple command, tell execute_disk_command that it
   might be able to get away without forking and simply exec.
   This means things like ( sleep 10 ) will only cause one fork.
   If we're timing the command or inverting its return value, however,
   we cannot do this optimization. */
if ((user_subshell || user_coproc) && (tcom->type == cm_simple || tcom->type == cm_subshell) &&
    ((tcom->flags & CMD_TIME_PIPELINE) == 0) &&
    ((tcom->flags & CMD_INVERT_RETURN) == 0))
  {
    tcom->flags |= CMD_NO_FORK;
    if (tcom->type == cm_simple)
      tcom->value.Simple->flags |= CMD_NO_FORK;
  }

bash -c '...' 的情况下,当由 builtins/evalstring.c 中的should_suppress_fork 函数判断时,会设置 CMD_NO_FORK 标志。让 shell 自己完成这个过程,总是有益的。但只在以下情况下执行:

  • 输入来自硬编码字符串,并且 shell 在该字符串的最后一个命令处。
  • 没有其他命令、陷阱、钩子等需要在命令完成后运行。
  • 退出状态不需要被反转或以其他方式修改。
  • 无需撤消重定向。

这样可以节省内存,使进程的启动时间略微更快(因为它不需要进行 fork),并确保传递到进程 ID 的信号直接转到您正在运行的进程,从而使 sh -c 'sleep 10' 的父进程能够确定哪个信号杀死了 sleep,如果它实际上被信号杀死的话。

但是,如果由于某种原因您想要禁止它,只需要设置任何陷阱即可:

# run the noop command (:) at exit
bash -c 'trap : EXIT; sleep 10'

我无法从源代码中确定,但在我的看法中,如果整个“-c”命令行上只有一个命令,Bash才会执行该操作。因此,像bash -c“:; sleep 10”这样的东西即使在启动睡眠后也会保持shell运行状态。 - ilkkachu
@ilkkachu,我的直觉期望是这可能会因版本而异--3.2将优先于2014年后期实现的should_suppress_fork。即使今天是这种情况,我也不希望在未来的版本中仍然如此(希望能解决缺少优化机会的问题)--而陷阱将始终需要保持shell运行。 - Charles Duffy
在3.2和4.4中似乎表现相似。 - ilkkachu
nod。了解当前行为很有用,但我仍然怀疑它在未来是否可靠。如果通过对其最后一个命令进行隐式的exec可以使脚本运行得更快(尽管在典型情况下只有几毫秒,但仍然如此),那么如果有人编写这个补丁,为什么Chet会拒绝呢? - Charles Duffy
1
谢谢。如果你想知道为什么我想在所有情况下强制存在中间 shell,那么答案与此处提到的 sudo 更改有关:https://dev59.com/bZLea4cB1Zd3GeqPyyyZ#34376188。对 sudo 的 kill 以前会被转发给它的子进程,但现在不再是这样了,所以我正在尝试在不同的进程组中生成我的进程。 - Marco

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