我正在开发一个需要在 unix 中实现 fork() 的项目。我阅读了 freeBSD 和 openBSD 源代码,但它真的很难理解。有人可以解释一下返回两次的概念吗?我知道一个返回是一个孩子的pid,它被返回给父母,另一个是零,它被返回给一个子进程。但是我无法理解如何实现两次返回的概念……我怎样才能返回两次?提前感谢大家。
3 回答
当您调用 时fork
,它会返回“两次”,因为分叉会产生两个进程,每个进程都会返回。
因此,如果您正在实施fork
,则必须创建第二个流程而不结束第一个流程。然后返回两次行为将自然发生:两个不同进程中的每一个都将继续执行,只是它们返回的值不同(孩子给零,而父母给孩子的 PID)。
当您想到一个函数返回时,您会想到通常的代码流,它从入口点(通常main
)开始,然后以严格确定性和线性的方式逐行执行。
然而,在现实世界的系统中,可能有多个执行上下文,每个上下文都有自己的控制流(新的 C++ 标准实际上包含了这个概念)。每个单独的进程都是一个从 开始的执行上下文main
,但您也可以从现有的执行上下文中创建一个新的执行上下文(事实上,所有操作系统都必须能够做到这一点!)。fork
是创建新执行上下文的一种方式,新上下文的入口点是返回点fork
。fork
但是,原始上下文也继续运行,并且在调用后它照常继续运行。新上下文是一个单独的进程,因此fork
在两个上下文中都返回(一次)。
还有其他方法可以创建新的执行上下文;一种是通过实例化对象或使用特定于平台的函数来创建一个新线程(在同一进程中) ;std::thread
另一个是 Linux 的clone()
功能,它是 Posix 线程实现和fork
Linux 的基础(通过为内核的调度程序创建新的执行路径,并复制所有虚拟内存(新进程)或不复制(新线程)。
下面我将尝试解释如何从一个函数返回两次。我从一开始就警告你,这一切都是黑客行为。但是有很多地方使用这些黑客。
首先假设我们有以下 C 程序。
#include <stdio.h>
uint64_t saved_ret;
int main(int argc, char *argv[])
{
if (saveesp()) {
printf("here! esp = %llX\n", saved_ret);
jmpback();
} else {
printf("there! esp = %llX\n", saved_ret);
}
return 0;
}
现在我们希望 saveesp() 返回两次,这样我们就可以访问两个 printf。下面是 saveesp() 的实现方式:
#define _ENTRY(x) \
.text; .globl x; .type x,@function; x:
#define NENTRY(y) _ENTRY(y)
NENTRY(saveesp)
movq (%rsp), %rax
movq %rax, saved_ret
movl $1, %eax
ret
NENTRY(jmpback)
xorq %rax, %rax
pushq saved_ret
ret
这绝不是可移植的代码。但是您可以为您想要支持的所有平台编写类似的程序集存根。
saveesp() 所做的是,它获取存储在堆栈中的返回地址并将其保存到局部变量中。之后它返回 1。这是一个非零返回,它将我们带到第一个 printf。
在 printf() 之后我们调用 jmpback()。这是真正的黑客。这个函数使得 saveesp() 看起来第二次返回。
它通过将保存的返回地址压入堆栈并执行 ret 来做到这一点。ret 将从堆栈中弹出地址并跳转到它。这次返回代码设置为零。因此,当我们“到达”我们的 C 例程时,看起来我们刚刚从 saveesp() 返回值为零。这样就达到了第二个 printf 。
如果您对这类 hack 感兴趣,您应该阅读更多关于用于实现异常处理的 C 标准中的 setjmp 和 longjmp 的信息。
此外,我们实际上在 OpenBSD 内核中的挂起/恢复代码路径上使用它。看一下第 231 和 250 行,它与上面的 C 代码几乎相同。然后看看这里的汇编代码,第 542 行是 savecpu 函数,它在挂起时第一次返回,第 375 行是我们在恢复时第二次返回的地方。