当从 shell 启动一个进程时,为什么 shell 会在执行该进程之前自行 fork?
例如,当用户输入时grep blabla foo
,为什么 shell 不能直接调用exec()
grep 而不使用子 shell?
另外,当 shell 在 GUI 终端仿真器中分叉时,它会启动另一个终端仿真器吗?(例如pts/13
启动pts/14
)
答案1
当你调用一个exec
家族方法时,它不会创建新的进程。相反,exec
取代当前进程内存和指令集等,以及您要运行的进程。
例如:你想grep
使用exec
bash
是一个进程(具有单独的内存、地址空间)。现在当您调用时exec(grep)
,exec
将用的数据替换当前进程的内存、地址空间、指令集等grep
。
这意味着该bash
流程将不复存在。
因此,完成grep
命令后您无法返回终端。
这就是exec
家族方法永不返回的原因。您无法在 之后执行任何代码exec
;它是不可访问的。
答案2
按照说明pts
自行检查:在 shell 中运行
echo $$
要知道你的进程 ID (PID),例如
echo $$
29296
然后运行例如sleep 60
然后在另一个终端
(0)samsung-romano:~% ps -edao pid,ppid,tty,command | grep 29296 | grep -v grep
29296 2343 pts/11 zsh
29499 29296 pts/11 sleep 60
因此,通常情况下,您与该进程关联的是同一个 tty。(请注意,这是您的,sleep
因为它以您的 shell 作为父级)。
答案3
对于您在 bash 提示符下发出的每个命令(例如:grep),您实际上打算启动一个新进程,然后在执行后返回 bash 提示符。
如果 shell 进程(bash)调用 exec() 来运行 grep,shell 进程将被 grep 替换。Grep 可以正常工作,但执行后,控制权无法返回到 shell,因为 bash 进程已被替换。
因此,bash 调用 fork(),它不会替换当前进程。
答案4
总结:因为这是在交互式 shell 中创建新进程并保持控制的最佳方法
fork() 对于进程和管道是必需的
要回答这个问题的具体部分,如果直接在父级中grep blabla foo
通过调用exec()
,父级将不再存在,并且其 PID 和所有资源将被接管grep blabla foo
。
但是,我们还是来谈谈exec()
和fork()
。这种行为的主要原因是因为fork()/exec()
这是在 Unix/Linux 上创建新进程的标准方法,而这并不是 bash 特有的东西;这种方法从一开始就存在,并受到当时已经存在的操作系统中相同方法的影响。换言之goldilocks 的答案关于相关问题,fork()
创建新进程更容易,因为内核在分配资源方面要做的工作较少,并且许多属性(例如文件描述符,环境等)都可以从父进程(在本例中为)继承bash
。
其次,就交互式 shell 而言,如果不分叉,您就无法运行外部命令。要启动位于磁盘上的可执行文件(例如/bin/df -h
),您必须调用系列函数之一exec()
,例如execve()
,它将用新进程替换父进程,接管其 PID 和现有文件描述符等。对于交互式 shell,您希望将控制权返回给用户并让父交互式 shell 继续运行。因此,最好的方法是通过创建一个子进程fork()
,然后让该进程通过被接管execve()
。因此,交互式 shell PID 1156 将通过生成一个fork()
PID 为 1157 的子进程,然后调用execve("/bin/df",["df","-h"],&environment)
,这使/bin/df -h
PID 为 1157 的子进程运行。现在 shell 只需要等待进程退出并将控制权返回给它。
如果您必须在两个或多个命令之间创建一个管道,比如说df | grep
,您需要一种方法来创建两个文件描述符(即来自pipe()
syscall 的读取和写入管道端),然后以某种方式让两个新进程继承它们。这是通过分叉新进程然后通过调用将管道的写入端复制dup2()
到其stdout
aka fd 1 上来完成的(因此,如果写入端是 fd 4,我们就会这样做dup2(4,1)
)。当exec()
发生生成时df
,子进程不会考虑它stdout
并在不意识到(除非它主动检查)它的输出实际上进入管道的情况下对其进行写入。同样的过程也发生在 上grep
,除了我们,在使用生成 之前fork()
,使用 fd 3 获取管道的读取端。所有这段时间,父进程仍然存在,等待管道完成后重新获得控制权。dup(3,0)
grep
exec()
对于内置命令,一般 shell 不需要fork()
,但命令除外source
。子 shell 需要fork()
。
总之,这是一个必要且有用的机制。
分叉和优化的缺点
现在,这是对于非交互式 shell 则不同,例如bash -c '<simple command>'
。尽管fork()/exec()
这是处理许多命令的最佳方法,但当您只有一个命令时,它就浪费了资源。引用斯蒂芬·查泽拉斯从这个帖子:
分叉的代价是昂贵的,包括 CPU 时间、内存、分配的文件描述符等……让 shell 进程在退出前等待另一个进程,这纯粹是浪费资源。此外,它还使得正确报告将执行命令的单独进程的退出状态变得困难(例如,当进程被终止时)。
因此,许多 shell(不仅仅是bash
)都exec()
允许bash -c ''
通过单个简单命令来接管它。正是由于上述原因,在 shell 脚本中尽量减少管道是更好的选择。通常你会看到初学者做这样的事情:
cat /etc/passwd | cut -d ':' -f 6 | grep '/home'
当然,这将需要fork()
3 个进程。这是一个简单的例子,但请考虑一个大文件,大小在 GB 范围内。使用一个进程会更有效率:
awk -F':' '$6~"/home"{print $6}' /etc/passwd
资源浪费实际上可以是一种拒绝服务攻击,特别是叉子炸弹是通过在管道中调用自身的 shell 函数创建的,这会分叉自身的多个副本。如今,可以通过限制systemd 上的 cgroups,Ubuntu 从 15.04 版本开始也使用该功能。
当然,这并不意味着分叉就是坏事。正如之前讨论的那样,它仍然是一种有用的机制,但如果你可以使用更少的进程,从而连续使用更少的资源,从而获得更好的性能,那么你应该尽可能避免fork()
。