Answers:
当您调用exec
族方法时,它不会创建新进程,而是exec
将当前进程的内存和指令集等替换为您要运行的进程。
例如,您要grep
使用exec 运行。bash
是一个进程(具有独立的内存和地址空间)。现在,当您调用时exec(grep)
,exec将用数据替换当前进程的内存,地址空间,指令集等grep's
。这意味着bash
过程将不再存在。结果,完成grep
命令后您将无法返回终端。这就是exec系列方法永不返回的原因。exec之后不能执行任何代码;这是无法到达的。
exec grep blabla foo
。当然,在这种情况下,它不是很有用(因为您的终端窗口将在grep完成后立即关闭),但是它有时还是很方便的(例如,如果您正在通过ssh启动另一个shell) / sudo / screen,并且不打算返回到原始屏幕,或者如果您正在运行的shell进程是子shell,则无论如何它永远都不会执行多个命令。
bash -c 'grep foo bar'
调用exec,bash会自动为您提供优化形式
TL; DR:因为这是在交互式外壳中创建新流程和保持控制的最佳方法
为了回答这个问题的特定部分,如果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()
调用将管道的写端复制到它的stdout
aka fd 1上(因此,如果写端为fd 4,则可以这样做dup2(4,1)
)。当何时exec()
产生df
子进程时,子进程将不会对其进行任何考虑stdout
并对其进行写入,而不会意识到(除非主动检查)其输出实际上是管道。发生相同的过程grep
,除了我们fork()
用fd 3读取管道的末尾,dup(3,0)
然后grep
生成exec()
。一直以来,父进程仍然在那里,等待管道完成后重新获得控制权。
对于内置命令,shell通常不提供fork()
,但source
command 除外。子壳需要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函数创建的,分叉自身的多个副本。如今,这可以通过限制systemd上cgroups中的最大进程数来缓解,Ubuntu从15.04版本开始也使用该进程数。
当然,这并不意味着分叉是不好的。如前所述,它仍然是一种有用的机制,但是如果可以减少流程,减少连续的资源从而获得更好的性能,那么就应该避免fork()
。