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 也喜欢bash
在sh -c cmd
or(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 可能必须运行更多命令。
通过挖掘 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)