为什么在简单的 bash 命令中没有明显的 clone 或 fork 以及它是如何完成的?

Ser*_*nyy 8 shell strace syscalls

考虑以下(含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 +++
Run Code Online (Sandbox Code Playgroud)

没有什么不寻常的,从主 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 +++
Run Code Online (Sandbox Code Playgroud)

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

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

Sté*_*las 10

sh -c 'command line'通常使用之类的东西system("command line")ssh host 'command line'vi!cron以及更普遍的任何被用来解释一个命令行,所以它,使之尽可能高效是很重要的。

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

许多 shell 通常会尽量减少分叉的数量作为优化。即使是非优化的 shell 也喜欢bashsh -c cmdor(cmd in subshell)情况下这样做。与 ksh 或 zsh 相反,它不在bash -c 'cmd > redir'or 中执行 bash -c 'cmd1; cmd2'(在子shell中相同)。ksh93 是在避免分叉方面走得最远的进程。

在某些情况下无法进行优化,例如执行以下操作时:

sh < file
Run Code Online (Sandbox Code Playgroud)

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

或者:

sh -c 'trap "echo Ouch" INT; cmd'
Run Code Online (Sandbox Code Playgroud)

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


Ser*_*nyy 8

通过挖掘 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;
    }
Run Code Online (Sandbox Code Playgroud)

后来这些标志转到execute_disk_command()函数,它设置nofork整数变量,然后在尝试 fork 之前检查该变量。实际命令本身将由分叉进程或父进程的execve()包装函数 shell_execve()运行,在这种情况下,它是实际的父进程。

Stephane 的回答中很好地解释了这种机制的原因。


这个问题范围之外的旁注:应该注意的是,shell 是交互式的还是通过-c. 在执行命令之前会有一个fork。从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
Run Code Online (Sandbox Code Playgroud)

另请参阅为什么 bash 不会为简单命令生成子shell?