考虑以下内容(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