Bash 的 no-fork 优化
我们知道,Bash 在执行一个外部命令时,会先 fork() 一个子进程,然后在子进程里面执行 execve() 去加载那个外部程序。fork 子进程是会耗性能的,所以 Bash 会在下面几种情况下不 fork 子进程,直接在当前进程执行 execve()。
bash -c 'command'
如果用了 bash -c 的形式启动 Bash,同时 -c 选项的参数里只包含一个命令,比如 bash -c 'sleep 666',这时 Bash 不会 fork 子进程去运行 sleep 命令,它会让 sleep 直接占用自己现有的进程:
$ bash -c 'sleep 666' & $ pstree -ap ... | `-bash,3117 | |-pstree,3119 -ap | `-sleep,3118 666 ... |
3117 是我当前敲入命令的交互 Shell,3118 就是 bash -c 启动的那个进程,然后直接被替换成了 sleep,pid 还是 3118。
我们可以看一下 Bash 无法优化的情况下,进程树是什么样的:
$ bash -c 'sleep 666;ls' & $ pstree -ap ... | `-bash,3117 | |-bash,3120 -c sleep\040666;ls | | `-sleep,3121 666 | `-pstree,3122 -ap ... |
这次我们给 -c 的参数包含了两个命令,sleep 和 ls,所以 Bash 不能让 sleep 占用它的进程,因为执行完 sleep 它还得去执行 ls。
bash -c 'command1 && command2 || command3 ... && commandN'
在这种由若干个 && 和 || 把若干个简单命令组成的的复合命令中,最右侧的那个(commandN)命令执行时(如果执行到的话)会进行 no-fork 优化:
$ bash -c 'sleep 1 || sleep 2 && sleep 666' & $ pstree -ap # 等 3 秒钟后再执行这条 ... | `-bash,3117 | |-pstree,3126 -ap | `-sleep,3123 666 ... |
在 bash -c 'sleep 1 || sleep 2 && sleep 666' 这条命令中,一共产生过 3 个进程,bash -c 首先产生了一个进程 3123,然后 3123 又分别 fork 出两个子进程 3124 和 3125 来分别执行 sleep 1 和 sleep 2,sleep 666 没有产生新的进程,它和 bash -c 用了同一个进程,也就是 3123。这个优化在 Bash 4.4 之前没有。
( command )
用显示的子 shell 语法运行一个单独的命令,比如 ( sleep 100 ),如果不进行优化的话,这里应该先 fork 一个子 shell,然后这个子 shell 会再 fork 一个子子 shell 去运行 sleep,一共 fork 两次,再极端点:( ( ( ( ( sleep 100 ) ) ) ) ),会产生一个 5 级的子 shell(( ( ( ( ( echo $BASH_SUBSHELL ) ) ) ) ) 的确会输出 5),一共 fork 6次,然而 Bash 并不会这样做,无论你嵌套了多少级,Bash 只会 fork 一次,只产生一个子进程,然后在这个进程里执行 sleep 命令。