为什么 shell 会调用 fork()?

为什么 shell 会调用 fork()?

当从 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 -hPID 为 1157 的子进程运行。现在 shell 只需要等待进程退出并将控制权返回给它。

如果您必须在两个或多个命令之间创建一个管道,比如说df | grep,您需要一种方法来创建两个文件描述符(即来自pipe()syscall 的读取和写入管道端),然后以某种方式让两个新进程继承它们。这是通过分叉新进程然后通过调用将管道的写入端复制dup2()到其stdoutaka fd 1 上来完成的(因此,如果写入端是 fd 4,我们就会这样做dup2(4,1))。当exec()发生生成时df,子进程不会考虑它stdout并在不意识到(除非它主动检查)它的输出实际上进入管道的情况下对其进行写入。同样的过程也发生在 上grep,除了我们,在使用生成 之前fork(),使用 fd 3 获取管道的读取端。所有这段时间,父进程仍然存在,等待管道完成后重新获得控制权。dup(3,0)grepexec()

对于内置命令,一般 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()

也可以看看

相关内容