考虑以下(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
sh -c 'command line'
通常用于诸如system("command line")
,ssh host 'command line'
,vi
's!
,之类的东西cron
,以及更普遍地用于解释命令行的任何东西,因此使其尽可能高效非常重要。分叉是昂贵的,在 CPU 时间、内存、分配的文件描述符方面......让一个 shell 进程在退出之前等待另一个进程只是浪费资源。此外,很难正确报告将执行命令的单独进程的退出状态(例如,当进程被终止时)。
许多 shell 通常会尽量减少分叉的数量作为优化。即使是未优化的外壳也喜欢
bash
在sh -c cmd
or(cmd in subshell)
情况下这样做。与 ksh 或 zsh 不同,它不在bash -c 'cmd > redir'
or 中执行此操作bash -c 'cmd1; cmd2'
(在子shell 中相同)。ksh93 是在避免分叉方面走得最远的过程。在某些情况下无法进行优化,例如:
哪里
sh
不能跳过最后一个命令的分叉,因为在该命令运行时可以将更多文本附加到脚本中。对于不可查找的文件,它无法检测到文件结尾,因为这可能意味着从文件中读取得太早太早。或者:
在执行“最后一个”命令后,shell 可能必须运行更多命令。
通过挖掘 bash 源代码,我发现如果没有管道或重定向,bash 实际上会忽略分叉。从execute_cmd.c 的第 1601 行开始:
稍后,这些标志开始
execute_disk_command()
起作用,它设置nofork整数变量,然后在尝试分叉之前对其进行检查。实际的命令本身将由分叉或父进程的execve()
包装函数 shell_execve()运行,在这种情况下,它是实际的父进程。Stephane 的回答很好地解释了这种机制的原因。
此问题范围之外的旁注:应注意,显然外壳是交互式的还是通过
-c
. 在执行命令之前会有一个分叉。从strace
在交互式 shell (strace -e trace=process -f -o test.trace bash
) 上运行并检查输出文件可以看出这一点:另请参阅为什么 bash 不会为简单命令生成子 shell?