壳为什么要调用fork()?


32

从外壳启动进程时,为什么外壳在执行进程之前分叉自身?

例如,当用户输入时grep blabla foo,为什么外壳程序不能在exec()没有子外壳程序的情况下仅调用grep?

另外,当外壳在GUI终端仿真器中派生自己时,它是否启动另一个终端仿真器?(例如pts/13开始pts/14

Answers:


34

当您调用exec族方法时,它不会创建新进程,而是exec将当前进程的内存和指令集等替换为您要运行的进程。

例如,您要grep使用exec 运行。bash是一个进程(具有独立的内存和地址空间)。现在,当您调用时exec(grep),exec将用数据替换当前进程的内存,地址空间,指令集等grep's。这意味着bash过程将不再存在。结果,完成grep命令后您将无法返回终端。这就是exec系列方法永不返回的原因。exec之后不能执行任何代码;这是无法到​​达的。


几乎可以---我用bash代替了Terminal。;-)
Rmano 2014年

2
顺便说一句,您可以使用命令告诉bash不先分叉就执行grep exec grep blabla foo。当然,在这种情况下,它不是很有用(因为您的终端窗口将在grep完成后立即关闭),但是它有时还是很方便的(例如,如果您正在通过ssh启动另一个shell) / sudo / screen,并且不打算返回到原始屏幕,或者如果您正在运行的shell进程是子shell,则无论如何它永远都不会执行多个命令。
Ilmari Karonen 2014年

7
指令集具有非常具体的含义。而且这不是你正在使用它的含义。
安德鲁Savinykh

@IlmariKaronen在包装脚本中非常有用,在该脚本中您要为命令准备参数和环境。在您提到的情况下,bash绝不意味着要运行多个命令,而实际上是在bash -c 'grep foo bar'调用exec,bash会自动为您提供优化形式
Sergiy Kolodyazhnyy

3

按照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因为它以您的外壳为父)。


2

TL; DR:因为这是在交互式外壳中创建新流程和保持控制的最佳方法

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和现有文件描述符等。对于交互式外壳,您希望控件返回给用户并让父交互式外壳继续进行。因此,最好的方法是创建一个子流程via fork(),并让该流程通过via接管execve()。因此,交互式外壳程序PID 1156将fork()使用PID 1157 生成一个子代,然后调用execve("/bin/df",["df","-h"],&environment),使它/bin/df -h以PID 1157运行。现在,外壳程序仅需等待进程退出并返回控制权即可。

例如,如果必须在两个或多个命令之间创建管道df | grep,则需要一种方法来创建两个文件描述符(这是从pipe()syscall 读取和写入管道的末尾),然后以某种方式让两个新进程继承它们。这样做完成了新过程,然后通过dup2()调用将管道的写端复制到它的stdoutaka fd 1上(因此,如果写端为fd 4,则可以这样做dup2(4,1))。当何时exec()产生df子进程时,子进程将不会对其进行任何考虑stdout并对其进行写入,而不会意识到(除非主动检查)其输出实际上是管道。发生相同的过程grep,除了我们fork()用fd 3读取管道的末尾,dup(3,0)然后grep生成exec()。一直以来,父进程仍然在那里,等待管道完成后重新获得控制权。

对于内置命令,shell通常不提供fork(),但sourcecommand 除外。子壳需要fork()

简而言之,这是必要且有用的机制。

分叉和优化的缺点

现在,这是对非交互shell不同,如bash -c '<simple command>'。尽管这fork()/exec()是您必须处理许多命令的最佳方法,但是只有一个命令时却浪费了资源。引用斯特凡Chazelas这篇文章

派生是昂贵的,在CPU时间,内存,分配的文件描述符方面……让shell进程躺在退出之前只是等待另一个进程只是浪费资源。同样,这也使得难以正确报告将执行命令的单独进程的退出状态(例如,当进程被杀死时)。

因此,许多外壳程序(而不仅仅是bash)用于exec()允许bash -c ''通过单个简单命令来接管。正是由于上述原因,在shell脚本中最小化管道会更好。通常,您可以看到初学者做这样的事情:

cat /etc/passwd | cut -d ':' -f 6 | grep '/home'

当然,这将需要fork()3个过程。这是一个简单的示例,但考虑到一个千兆字节的大文件。使用一个过程,效率会大大提高:

awk -F':' '$6~"/home"{print $6}' /etc/passwd

浪费资源实际上可能是拒绝服务攻击的一种形式,尤其是分叉炸弹是通过在管道中调用自身的shell函数创建的,分叉自身的多个副本。如今,这可以通过限制systemdcgroups中的最大进程数来缓解,Ubuntu从15.04版本开始也使用该进程数。

当然,这并不意味着分叉是不好的。如前所述,它仍然是一种有用的机制,但是如果可以减少流程,减少连续的资源从而获得更好的性能,那么就应该避免fork()

也可以看看


1

对于在bash提示符下发出的每个命令(例如:grep),您实际上打算启动一个新进程,然后在执行后返回bash提示符。

如果shell进程(bash)调用exec()运行grep,则shell进程将被grep替换。Grep可以正常工作,但执行后,由于bash进程已被替换,因此控件无法返回到Shell。

因此,bash调用fork(),它不会替代当前进程。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.