关于 `fork`、子进程和“子 shell”

关于 `fork`、子进程和“子 shell”

这篇文章基本上是一篇文章的后续先前的问题我的。

从这个问题的答案中,我意识到我不仅不太理解“子shell”的整个概念,而且更一般地说,我不理解fork-ing 和子进程之间的关系。

我曾经认为当进程X执行 a时fork,a新的Y创建的进程的父进程是X,但根据该问题的答案,

[a] subshel​​l 并不是一个全新的进程,而是现有进程的一个分支。

这里的含义是“分叉”不是(或不会导致)“一个全新的过程”。

我现在很困惑,太困惑了,事实上,无法提出一个连贯的问题来直接消除我的困惑。

然而,我可以提出一个可能间接带来启发的问题。

因为,根据zshall(1)$ZDOTDIR/.zshenv每当一个新的实例启动时都会获取源,因此任何导致创建“一个全新的 [zsh] 进程”的zsh命令都将导致无限倒退。$ZDOTDIR/.zshenv另一方面,在文件中包含以下任一行$ZDOTDIR/.zshenv都会不是导致无限回归:

echo $(date; printenv; echo $$) > /dev/null    #1
(date; printenv; echo $$)                      #2

我发现通过上述机制引发无限回归的唯一方法是在文件中包含类似以下1 的$ZDOTDIR/.zshenv行:

$SHELL -c 'date; printenv; echo $$'            #3

我的问题是:

  1. #1上面标记的命令#2与标记帐户#3的这一行为差异有何区别?

  2. 如果在 中创建的 shell#1#2称为“子 shell”,那么那些类似于调用生成的 shell 是什么#3

  3. 是否有可能根据 Unix 进程的“理论”(因为缺乏更好的词)来合理化(并可能概括)上述经验/轶事发现?

最后一个问题的动机是能够确定提前时间(即不诉诸实验)如果将哪些命令包含在中,它们会导致无限回归$ZDOTDIR/.zshenv


1我在上面的各个示例中使用的特定命令顺序date; printenv; echo $$并不是太重要。它们恰好是其输出可能有助于解释我的“实验”结果的命令。 (然而,我确实希望这些序列包含多个命令,原因如下这里.)

答案1

因为,根据 zshall(1),每当 zsh 的新实例启动时,都会获取 $ZDOTDIR/.zshenv

如果你把注意力集中在“开始”这个词上,你会过得更好。的作用fork()是创建另一个进程从当前进程所在的位置开始。它克隆一个现有的进程,唯一的区别是 的返回值fork。该文档使用“开始”表示从头开始进入程序。

您的示例 #3 运行$SHELL -c 'date; printenv; echo $$',从头开始一个全新的进程。它将经历普通的启动行为。例如,您可以通过交换另一个 shell 来说明这一点:运行bash -c ' ... '而不是zsh -c ' ... '.在这里使用并没有什么特别的$SHELL

示例#1 和#2 运行子shell。 shellfork本身并在该子进程内执行您的命令,然后在子进程完成时继续执行自己的命令。


您的问题#1 的答案如下:示例 3 从一开始就运行一个全新的 shell,而其他两个则运行子 shell。启动行为包括加载.zshenv

他们专门指出这种行为的原因可能是导致您感到困惑的原因,因为该文件(与其他一些文件不同)会在交互式和非交互式 shell 中加载。


对于你的问题#2:

如果在 #1 和 #2 中创建的 shell 被称为“子 shell”,那么那些类似于 #3 生成的 shell 被称为“子 shell”?

如果你想要一个名字,你可以称它为“子壳”,但实际上它没什么。它与从 shell 启动的任何其他进程没有什么不同,无论是相同的 shell、不同的 shell 还是cat.


对于你的问题#3:

是否有可能根据 Unix 进程的“理论”(因为缺乏更好的词)来合理化(并可能概括)上述经验/轶事发现?

fork创建一个带有新 PID 的新进程,该进程从该进程停止的地方开始并行运行。exec用从某处加载的新程序替换当前正在执行的代码,并从头开始运行。当您生成一个新程序时,首先您fork自己,然后是exec子程序中的该程序。这是适用于任何地方的过程的基本理论,无论是壳内部还是壳外部。

子 shell 是forks,您运行的每个非内置命令都会导致 aforkexec


请注意,$$扩展为父 shell 的 PID在任何 POSIX 兼容的 shell 中,因此无论如何您可能都无法获得预期的输出。另请注意,zsh 无论如何都会积极优化子 shell 执行,并且通常exec是最后一个命令,或者如果所有命令在没有它的情况下都是安全的,则根本不会生成子 shell。

测试你的直觉的一个有用命令是:

strace -e trace=process -f $SHELL -c ' ... '

...这会将您在新 shell 中运行的命令的所有与进程相关的事件(而不是其他事件)打印到标准错误。您可以查看新进程中运行和不运行的内容以及exec发生的位置。

另一个可能有用的命令是pstree -h,它将打印并突出显示当前进程的父进程树。您可以在输出中看到您的层数。

答案2

当手册中说命令.zshenv是“来源”时,这意味着它们是在运行它们的 shell 中执行的。它们不会引起对 的调用fork(),因此它们不会生成子 shell。您的第三个示例显式运行一个子 shell,调用 调用fork(),从而无限递归。我相信,这应该(至少部分地)回答你的第一个问题。

  1. 命令 1 和 2 中没有“创建”任何内容,因此没有任何东西可以被称为任何东西 - 这些命令是在采购 shell 的上下文中运行的。

  2. 概括而言,“调用”shell 例程或程序与“采购”shell 例程或程序之间的区别 - 后者通常仅适用于 shell 命令/脚本,而不适用于外部程序。 “获取”shell 脚本通常是通过. <scriptname>而不是或来./<scriptname>完成的/full/path/to/script- 请注意获取指令开头的“点-空间”序列。也可以使用 调用 Sourcing source <scriptname>,该source命令是 shell 内部命令。

答案3

fork,假设一切顺利,返回两次。一个返回在父进程中(具有原始进程 ID),另一个返回在新子进程中(不同的进程 ID,但在其他方面与父进程共享许多共同点)。此时,子进程可以exec(3)执行某些操作,这将导致一些“新”二进制文件加载到该进程中,尽管子进程不需要这样做,并且可以运行已通过父进程加载的其他代码(例如 zsh 函数) 。因此,fork如果“全新”被认为是指通过exec(3)系统调用加载的东西,则 a 可能会也可能不会导致“全新”进程。

提前猜测哪些命令会导致无限倒退是很棘手的。除了 fork-calling-fork 情况(又名“forkbomb”)之外,另一个简单的情况是通过一些命令的简单函数包装器

function ssh() {
   ssh -o UseRoaming=no "$@"
}

相反,可能应该写成

function ssh() {
  =ssh -o UseRoaming=no "$@"
}

或者command ssh ...避免ssh函数的无限函数调用,调用ssh该函数的函数调用...这绝不涉及fork,因为函数调用是 ZSH 进程内部的,但会愉快地发生到无穷大,直到该单个函数遇到某个限制ZSH进程。

strace与往常一样,可以方便地准确揭示任何命令涉及哪些系统调用(特别是这里fork,也许还有一些exec调用); shell 可以使用或类似的方式进行调试-x,显示 shell 内部正在执行的操作(例如函数调用)。如需更多阅读,Stevens 在《Unix 环境中的高级编程》中有几章与新进程的创建和处理相关。

相关内容