为什么简单的 bash 命令中没有明显的克隆或分叉以及它是如何完成的?

为什么简单的 bash 命令中没有明显的克隆或分叉以及它是如何完成的?

考虑以下内容(sh存在/bin/dash):

$ strace -e trace=process sh -c 'grep "^Pid:" /proc/self/status /proc/$$/status'
execve("/bin/sh", ["sh", "-c", "grep \"^Pid:\" /proc/self/status /"...], [/* 47 vars */]) = 0
arch_prctl(ARCH_SET_FS, 0x7fcc8b661540) = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fcc8b661810) = 24865
wait4(-1, /proc/self/status:Pid:    24865
/proc/24864/status:Pid: 24864
[{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 24865
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=24865, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
exit_group(0)                           = ?
+++ exited with 0 +++

没有什么不寻常的,从主 shell 进程grep替换了一个分叉进程(这里通过 完成)。clone()到目前为止,一切都很好。

现在使用 bash 4.4:

$ strace -e trace=process bash -c 'grep "^Pid:" /proc/self/status /proc/$$/status'
execve("/bin/bash", ["bash", "-c", "grep \"^Pid:\" /proc/self/status /"...], [/* 47 vars */]) = 0
arch_prctl(ARCH_SET_FS, 0x7f8416b88740) = 0
execve("/bin/grep", ["grep", "^Pid:", "/proc/self/status", "/proc/25798/status"], [/* 47 vars */]) = 0
arch_prctl(ARCH_SET_FS, 0x7f8113358b80) = 0
/proc/self/status:Pid:  25798
/proc/25798/status:Pid: 25798
exit_group(0)                           = ?
+++ exited with 0 +++

这里明显的是grep假设 shell 进程的 pid 并且没有明显的fork()clone()调用。那么问题是,如何bash在没有任何调用的情况下实现这样的杂技呢?

但请注意,clone()如果命令包含 shell 重定向,则会出现系统调用,例如df > /dev/null

答案1

它们sh -c 'command line'通常用于system("command line")ssh host 'command line'vi's !cron,以及更常见的任何用于解释命令行的东西,因此使其尽可能高效非常重要。

分叉是昂贵的,在CPU时间、内存、分配的文件描述符上......让一个shell进程在退出之前等待另一个进程只是浪费资源。此外,它使得正确报告将执行命令的单独进程的退出状态变得困难(例如,当进程被终止时)。

许多 shell 通常会尝试最小化分叉数量作为优化。即使是未优化的 shell 也喜欢在或情况bash下执行此操作。与 ksh 或 zsh 相反,它不在or 中执行此操作(在子 shell 中相同)。 ksh93 是最能避免分叉的 shell。sh -c cmd(cmd in subshell)bash -c 'cmd > redir'bash -c 'cmd1; cmd2'

在某些情况下无法进行优化,例如:

sh < file

无法sh跳过最后一个命令的 fork,因为在该命令运行时可以将更多文本附加到脚本中。对于不可查找的文件,它无法检测到文件结尾,因为这可能意味着过早地从文件中读取太多内容。

或者:

sh -c 'trap "echo Ouch" INT; cmd'

在执行“最后一个”命令后,shell 可能必须运行更多命令。

答案2

通过研究 bash 源代码,我发现如果没有管道或重定向,bash 实际上会忽略分叉。execute_cmd.c 中的第 1601 行:

  /* 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;
    }

后来这些标志开始execute_disk_command()起作用,这设置了诺福克整数变量,稍后是在尝试分叉之前检查。实际的命令本身将由execve()包装器运行函数 shell_execve()来自分叉进程或父进程,在本例中它是实际的父进程。

这种机制的原因在斯蒂芬的回答


旁注超出了这个问题的范围:应该注意的是,显然 shell 是交互式的还是通过 运行的很重要-c。在执行命令之前会有一个分叉。从strace在交互式 shell ( ) 上运行strace -e trace=process -f -o test.trace bash并检查输出文件可以明显看出这一点:

19607 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_t
idptr=0x7f2d35e93a10) = 19628
19607 wait4(-1,  <unfinished ...>
19628 execve("/bin/true", ["/bin/true"], [/* 47 vars */]) = 0

也可以看看为什么 bash 不为简单命令生成子 shell?

相关内容