当前位置: 首页>>技术问答>>正文


为什么炮弹叫 fork()?

, ,

问题描述

当一个进程从shell启动时,为什么shell在执行进程之前会自行分叉?

例如,当用户输入grep blabla foo时,为什么shell不能在没有子shell的情况下在grep上调用exec()

此外,当一个shell在GUI终端仿真器中分叉时,它是否启动另一个终端仿真器? (例如pts/13启动pts/14)

最佳解决思路

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

例如,您想使用exec运行grepbash是一个进程(具有独立的内存,地址空间)。现在,当您调用exec(grep)时,exec将使用grep's数据替换当前进程的内存,地址空间,指令集等。这意味着bash进程将不复存在。因此,在完成grep命令后,您无法返回终端。这就是为什么exec家庭方法永远不会回来的原因。 exec后你不能执行任何代码;它无法到达。

次佳解决思路

根据pts,自己检查一下:在shell中运行

echo $$ 

我知道你的process-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作为父级)。

第三种解决思路

长话短说:因为这是创建新进程并在交互式shell中保持控制的最佳方法

fork()是进程和管道所必需的

要回答这个问题的具体部分,如果grep blabla foo直接在父母中通过exec()调用,则父母将抓住存在,并且其所有资源的PID将由grep blabla foo接管。

但是,我们来谈谈exec()fork()。这种行为的关键原因是因为fork()/exec()是在Unix /Linux上创建新进程的标准方法,这不是特定于bash的事情。这种方法从一开始就已经存在,并且受到当时现有操作系统的相同方法的影响。在一个相关的问题上有点解释goldilocks’s answerfork()用于创建新进程更容易,因为内核在分配资源方面做的工作较少,而且很多属性(例如文件描述符,环境等) – 都可以从父进程继承(在本例中来自bash)。

其次,就交互式shell而言,你不能在没有分叉的情况下运行外部命令。要启动存在于磁盘上的可执行文件(例如,/bin/df -h),您必须调用exec()系列函数之一,例如execve(),它将使用新进程替换父级,接管其PID和现有文件描述符等。对于交互式shell,您希望控件返回给用户并让父交互式shell继续运行。因此,最好的方法是通过fork()创建子进程,并通过execve()接管该进程。因此,交互式 shell PID 1156将通过带有PID 1157的fork()生成一个子节点,然后调用execve("/bin/df",["df","-h"],&environment),这使得/bin/df -h与PID 1157一起运行。现在shell只需要等待进程退出并将控制权返回给它。

如果你必须在两个或多个命令之间创建一个管道,比如df | grep,你需要一种方法来创建两个文件描述符(来自pipe()系统调用的读写管道末尾),然后以某种方式让两个新进程继承它们。这是通过dup2()调用将dup2()呼叫的写入端复制到其stdout(即fd 1)(如果写入结束为fd 4,我们进行dup2(4,1))。当exec()产生df时,子进程将不会想到它的stdout并写入它而不知道(除非它主动检查)它的输出实际上是一个管道。除了fork()之外,grep发生相同的过程,在用exec()产生grep之前,用fd 3读取管末端和dup(3,0)。所有这一次父进程仍然存在,等待管道完成后重新获得控制权。

在内置命令的情况下,一般shell不是fork(),但source命令除外。子壳需要fork()

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

分叉和优化的缺点

现在,这是different for non-interactive shells,例如bash -c '<simple command>'。尽管fork()/exec()是您必须处理许多命令的最佳方法,但是当您只有一个命令时,这会浪费资源。从this post引用Stéphane Chazelas

Forking is expensive, in CPU time, memory, allocated file descriptors… Having a shell process lying about just waiting for another process before exiting is just a waste of resources. Also, it makes it difficult to correctly report the exit status of the separate process that would execute the command (for instance, when the process is killed).

因此,许多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

浪费资源实际上可能是拒绝服务攻击的一种形式,特别是fork bombs是通过shell函数创建的,这些函数在管道中调用自己,这些函数会自行分配多个副本。如今,通过限制cgroups on systemd中的最大进程数来减轻这种情况,Ubuntu自15.04版以来也使用了该进程。

当然,这并不意味着分叉就是坏事。如前所述,它仍然是一种有用的机制,但是如果你可以减少进程,连续减少资源,从而提高性能,那么你应该尽可能避免使用fork()

也可以看看

参考资料

本文由Ubuntu问答整理, 博文地址: https://ubuntuqa.com/article/7114.html,未经允许,请勿转载。