【问题标题】:Fork and returning twice分叉和返回两次
【发布时间】:2012-03-04 00:07:35
【问题描述】:

我正在做一个需要在 unix 中实现 fork() 的项目。我阅读了 freeBSD 和 openBSD 源代码,但它真的很难理解。有人可以解释一下返回两次的概念吗?我知道一个返回是一个孩子的pid,它被返回给父母,另一个是零,它被返回给一个子进程。但是我无法理解如何实现两次返回的概念……我怎样才能返回两次?提前谢谢大家。

【问题讨论】:

  • 它并没有真正“返回两次”;它在两个进程中的每一个中返回一次。
  • 为什么需要实现fork()?您在一个还没有它的系统上吗?

标签: c unix fork freebsd openbsd


【解决方案1】:

当您调用fork 时,它会返回“两次”,因为分叉会生成两个进程,每个进程都会返回。

因此,如果您正在实施fork,则必须创建第二个进程而不结束第一个进程。然后返回两次行为将自然发生:两个不同进程中的每一个都将继续执行,只是它们返回的 value 不同(孩子给零,而父母给孩子的 PID)。

【讨论】:

    【解决方案2】:

    当您想到一个函数返回时,您会想到通常的代码流,它从入口点(通常是main)开始,然后以严格确定性和线性的方式逐行执行。

    然而,在现实世界的系统中,可能有多个执行上下文,每个执行上下文都有自己的控制流(新的 C++ 标准实际上包含了这个概念)。每个单独的进程都是一个从main 开始的执行上下文,但您也可以从现有的执行上下文中创建一个新的执行上下文(事实上,所有操作系统都必须能够做到这一点!)。 fork 是创建新执行上下文的一种方式,新上下文的入口点fork 返回的点。但是,原始上下文也继续运行,并且在fork 调用之后照常继续运行。新的上下文是一个单独的进程,因此fork 在两个上下文中都返回(一次)。

    还有其他方法可以创建新的执行上下文;一种是通过实例化std::thread 对象或使用特定于平台的函数来创建一个新的线程(在同一进程中);另一个是 Linux 的 clone() 函数,它是 Posix 线程实现和 Linux 中 fork 的基础(通过为内核的调度程序创建新的执行路径,并复制所有虚拟内存(新进程)或不复制(新线程)。

    【讨论】:

      【解决方案3】:

      下面我将尝试解释如何从一个函数中返回两次。 我从一开始就警告你,这一切都是黑客行为。 但是有很多地方使用了这些技巧。

      首先假设我们有以下 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 行的here,它与上面的 C 代码几乎相同。然后看看汇编代码here,第 542 行是第一次在挂起时返回的 savecpu 函数,第 375 行是我们在恢复时第二次返回的地方。

      【讨论】:

        猜你喜欢
        • 2019-05-01
        • 1970-01-01
        • 1970-01-01
        • 2022-10-09
        • 2017-12-18
        • 1970-01-01
        • 2020-12-04
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多