【问题标题】:At what point does a fork() child process actually begin?fork() 子进程实际上在什么时候开始?
【发布时间】:2014-02-12 20:41:56
【问题描述】:

在声明 fork() 时该过程是否开始?这里有什么被杀的吗?

pid_t child;
child = fork();
kill (child, SIGKILL);

或者您是否需要为 fork 进程声明操作才能真正“开始”?

pid_t child;
child = fork();

if (child == 0) {
  // do something 
}
kill (child, SIGKILL);

我问是因为我想做的是创建两个孩子,等待第一个完成,然后在退出前杀死第二个:

pid_t child1;
pid_t child2;
child1 = fork();
child2 = fork();
int status;

if (child1 == 0) { //is this line necessary?
}

waitpid(child1, &status, 0);

kill(child2, SIGKILL);

【问题讨论】:

  • $status?那是什么?
  • 代码child1 = fork(); child2 = fork()创建了3个“子”(第二次fork返回后有4个进程在运行,除非出现错误,1个父,2个子,1个孙子。)

标签: c process fork


【解决方案1】:

C 函数fork 在标准C 库(Linux 上的glibc)中定义。当您调用它时,它会通过特殊的 CPU 指令(在 x86 上 sysenter)执行等效的系统调用(在 linux 上它的名称是 clone)。这会导致 CPU 切换到特权模式并开始执行内核的指令。然后内核创建一个新进程(列表中的记录和伴随的结构),该进程继承原始进程(文本、堆、堆栈等)、文件描述符等的内存映射副本。

内存区域被标记为不可写,因此当新进程或原始进程试图覆盖它们时,内核可以处理 CPU 异常并执行 copy-on-write (因此延迟复制内存页面的需要,直到绝对必要)。这是因为映射最初指向两个进程中的相同页面(物理内存)。

然后内核将执行交给调度程序,调度程序决定接下来运行哪个进程。它可以是原始进程、子进程或系统中运行的任何其他进程。

注意:Linux内核在运行队列中实际上是把子进程放在父进程的前面,所以比父进程运行得更早。当孩子在分叉后立即调用exec 时,这被认为提供了更好的性能。

当执行给原始进程时,CPU 切换回非特权模式并开始执行下一条指令。在这种情况下,它继续使用标准库的 fork 函数,该函数返回子进程的 PID(由 clone 系统调用返回)。

同样,子进程在fork函数中继续执行,但这里它返回0给调用函数。

之后,程序会在这两种情况下正常继续。子进程将原始进程作为父进程(这在内核的结构中注明)。当它存在时,父进程应该通过调用wait进行清理(接收子进程的退出状态)。

注意: clone 系统调用相当复杂,因为它将 fork 与线程的创建以及 linux 命名空间统一起来。其他操作系统有不同的 fork 实现,例如FreeBSD 本身有fork 系统调用。

免责声明:我不是内核开发人员。如果你知道的更好,请更正答案。

另见

【讨论】:

    【解决方案2】:

    “声明”在这种情况下是错误的; C 用这个词来谈论仅仅断言某物存在的结构,例如

    extern int fork(void);
    

    是函数fork声明。在您的代码中写入(或因#include <unistd.h> 而为您编写)不会导致调用fork

    现在,您的示例代码中的语句 child = fork(); 当写在函数体中时,会(生成代码以)调用函数fork。该函数,假设它实际上是您操作系统上的系统原语fork(2),并假设它成功,具有返回两次的特殊行为,一次在原始进程中,一次在新进程中,每个都有不同的返回值,所以你可以知道哪个是哪个。

    因此,您的问题的答案是,在您展示的两个代码片段中,假设我在上一段中提到的内容,child = fork(); 行之后的所有代码至少可能执行两次,一次由孩子和父母一次。 if (child == 0) { ... } 构造(同样,这不是“声明”)是让父母和孩子做不同事情的标准习语。

    编辑:在您的第三个代码示例中,是的,child1 == 0 块是必需的,但不能确保创建子项。相反,它可以确保您希望 child1 执行的任何操作do 仅在 child1 中完成。此外,正如所写(并且再次假设所有调用都成功),您正在创建 三个 子进程,因为 second 分叉调用将由父子进程执行!你可能想要这样的东西:

    pid_t child1, child2;
    int status;
    child1 = fork();
    if (child1 == -1) {
      perror("fork"); 
      exit(1);
    }
    else if (child1 == 0) {
      execlp("program_to_run_in_child_1", (char *)0);
      /* if we get here, exec failed */
      _exit(127);
    }
    
    child2 = fork();
    if (child2 == -1) {
      perror("fork");
      kill(child1, SIGTERM);
      exit(1);
    }
    
    else if (child2 == 0) {
      execlp("program_to_run_in_child_2", (char *)0);
      /* if we get here, exec failed */
      _exit(127);
    }
    
    /* control reaches this point only in the parent and only when
       both fork calls succeeded */
    if (waitpid(child1, &status, 0) != child1) {
      perror("waitpid");
      kill(child1, SIGTERM);
    }
    
    /* only use SIGKILL as a last resort */
    kill(child2, SIGTERM);
    

    仅供参考,这只是一个骨架。如果我正在编写代码来真正做到这一点(我有:例如参见https://github.com/zackw/tbbscraper/blob/master/scripts/isolate.c),那么将有更多的代码来全面检测和报告错误,以及处理文件描述符管理所需的额外逻辑孩子们和其他一些皱纹。

    【讨论】:

    • 所以,因为你调用 child2 = fork();在child1的动作块之后,它不会创建三个子进程? child1 什么时候在这里终止?
    • 正确。 child1 进入它的“动作块”并且永远不会出来,因为 execlp 只有在失败时才会返回,而 _exit 永远不会返回,句号。
    • 所以如果 child1 的“动作块”为空,会调用 child2 = fork();之后还是会导致孩子太多?
    • 是的。您有责任确保孩子无论如何都无法摆脱“行动障碍”。好吧,除非您希望父母和孩子做同样的事情,但这几乎不会发生。
    【解决方案3】:

    fork 进程生成一个与旧进程相同的新进程并在两个函数中返回。

    这会自动发生,因此您无需执行任何操作。

    但是,检查调用是否确实成功更简洁:

    低于 0 的值表示失败。在这种情况下,不好打电话给kill()

    值 == 0 表示我们是子进程。在这种情况下,调用kill()不是很干净。

    值 > 0 表示我们是父进程。在这种情况下,返回值是我们的孩子。在这里可以安全地拨打kill()

    在您的情况下,您甚至会得到 4 个进程:

    • 您的父级调用 fork(),剩下 2 个进程。
    • 他们都再次调用fork(),从而为他们每个人生成一个新的子进程。

    您应该将第二个fork() 进程移动到父代码运行的分支中。

    【讨论】:

      【解决方案4】:

      子进程在调用fork() 后的某个时间开始(在子进程的上下文中发生了一些设置)。

      fork() 返回时,您可以确定孩子正在运行。

      所以代码

      pid_t child = fork();
      kill (child, SIGKILL);
      

      会杀了孩子。孩子可能会执行kill(0, SIGKILL),它什么都不做并返回错误。

      没有办法判断孩子是否能活到足以执行它的杀戮。很可能不会,因为 Linux 内核将为子进程设置进程结构并让父进程继续。孩子只会在进程的就绪列表中等待。然后 kill 将再次删除它。

      编辑如果fork()返回值<= 0,那么你不应该等待或杀死。

      【讨论】:

        猜你喜欢
        • 2017-11-25
        • 1970-01-01
        • 2021-10-05
        • 2016-12-01
        • 2016-01-06
        • 2012-07-26
        • 2018-12-13
        • 1970-01-01
        • 2011-05-16
        相关资源
        最近更新 更多