fork()函数内部调用了clone()函数,这是真的吗?

55

我在罗伯特·洛夫的《Linux内核开发第二版》(ISBN:0-672-32720-1)的第三章中读到,clone系统调用用于在Linux中创建线程。现在clone语法要求传递一个起始例程/函数地址。

但是在同一页上,它也写道fork内部调用了clone。那么我的问题是,由fork创建的子进程如何开始运行fork调用后的代码部分,即如何不需要一个函数作为起始点?

如果我提供的链接信息有误,请指导我一些更好的链接/资源。


1
将函数作为参数传递只是内存中的一个地址。在汇编级别上,您会看到它可以从堆栈中简单地弹出返回地址,并将其用作新线程入口点的目标。 - Havenard
从“this”文本链接到的页面是clone文档,与从“syntax”文本链接到的页面相同。也许您想链接到fork文档。该文档说明fork使用设置为SIGCHLD的标志调用clone。大概是告诉clone改变其正常行为,并继续执行作为调用的返回,而不是调用新例程。我会质疑SIGCHLD是否正确;我希望看到更像CLONE_CHILD的东西。 - Eric Postpischil
@Havenard:你的意思是说它会将下一条指令的地址(PC将要存储的地址)保存/推入堆栈,并在创建子进程后使用它?这意味着clone()在创建线程时使用函数地址(通过例如pthread_create()传递),而在创建进程时,它直接使用堆栈中的返回地址。 - 0xF1
@EricPostpischil:抱歉链接有误,我已经进行了更正。 - 0xF1
@Havenard:这不是fork的正确实现方式。从fork返回后,堆栈应该看起来像从fork返回(与调用之前完全相同),而不是像从调用fork的位置调用子程序。 - Eric Postpischil
显示剩余8条评论
2个回答

95

对于这样的问题,总是要阅读源代码。

从glibc的nptl/sysdeps/unix/sysv/linux/fork.cGitHub)中我们可以找到fork()的实现。它绝对不是系统调用,我们可以看到魔法发生在ARCH_FORK宏内部,该宏被定义为对nptl/sysdeps/unix/sysv/linux/x86_64/fork.c中的clone()进行内联调用(GitHub)。但是等等,这个版本的clone()没有传递任何函数或堆栈指针!那么这里发生了什么?

让我们看一下glibc中clone()的实现。它在sysdeps/unix/sysv/linux/x86_64/clone.S中(GitHub)。你会看到它在子进程的堆栈上保存了函数指针,调用了clone系统调用,然后新进程将从堆栈中读取弹出该函数并调用它。

因此它的工作方式如下:

clone(void (*fn)(void *), void *stack_pointer)
{
    push fn onto stack_pointer
    syscall_clone()
    if (child) {
        pop fn off of stack
        fn();
        exit();
    }
}

fork() 是...

fork()
{
    ...
    syscall_clone();
    ...
}

摘要

clone()系统调用并不接受函数参数,它只是从返回点继续执行,就像fork()一样。因此,clone()fork()这两个库函数都是clone()系统调用的包装器。

文档

我的手册副本更明确地指出clone()既是一个库函数又是一个系统调用。但我觉得有些误导人的是,clone()在第2节中找到,而不是同时在第2节和第3节中找到。来自手册页面的说明:

#include <sched.h>

int clone(int (*fn)(void *), void *child_stack,
          int flags, void *arg, ...
          /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

/* Prototype for the raw system call */

long clone(unsigned long flags, void *child_stack,
          void *ptid, void *ctid,
          struct pt_regs *regs);

并且,

本页面介绍了glibc的clone()包装器函数和其基础系统调用。主要内容描述了包装器函数;原始系统调用的差异在本页面末尾描述。

最后,

原始的clone()系统调用更接近于fork(2),因为子进程在调用点继续执行。因此,clone()包装器函数的fn和arg参数被省略。此外,参数顺序也发生了变化。


5
fork()不会调用clone(),它们都是使用系统调用clone的函数。 - Javier

17

@Dietrich通过查看实现方式进行了很好的解释。太神奇了!不管怎样,还有另一种发现方法:通过查看调用strace“sniffs”。

我们可以编写一个非常简单的程序,使用fork(2),然后检查我们的假设(即,是否真的没有fork系统调用发生)。

#define WRITE(__fd, __msg) write(__fd, __msg, strlen(__msg))

int main(int argc, char *argv[])
{
  pid_t pid;

  switch (pid = fork()) {
    case -1:
      perror("fork:");
      exit(EXIT_FAILURE);
      break;
    case 0:
      WRITE(STDOUT_FILENO, "Hi, i'm the child");
      exit(EXIT_SUCCESS);
    default:
      WRITE(STDERR_FILENO, "Heey, parent here!");
      exit(EXIT_SUCCESS);
  }

  return EXIT_SUCCESS;
}

现在,编译该代码(clang -Wall -g fork.c -o fork.out),然后使用strace执行它:

strace -Cfo ./fork.strace.log ./fork.out

这将拦截我们的进程调用的系统调用(使用-f选项,我们还会拦截子进程的调用),然后将这些调用记录到./fork.trace.log中;-c选项会在最后给出汇总结果。在我的机器上(Ubuntu 14.04,x86_64 Linux 3.16),结果为(汇总):

6915  arch_prctl(ARCH_SET_FS, 0x7fa001a93740) = 0
6915  mprotect(0x7fa00188c000, 16384, PROT_READ) = 0
6915  mprotect(0x600000, 4096, PROT_READ) = 0
6915  mprotect(0x7fa001ab9000, 4096, PROT_READ) = 0
6915  munmap(0x7fa001a96000, 133089)    = 0
6915  clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fa001a93a10) = 6916
6915  write(2, "Heey, parent here!", 18) = 18
6916  write(1, "Hi, i'm the child", 17 <unfinished ...>
6915  exit_group(0)                     = ?
6916  <... write resumed> )             = 17
6916  exit_group(0)                     = ?
6915  +++ exited with 0 +++
6916  +++ exited with 0 +++
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 24.58    0.000029           4         7           mmap
 17.80    0.000021           5         4           mprotect
 14.41    0.000017           9         2           write
 11.02    0.000013          13         1           munmap
 11.02    0.000013           4         3         3 access
 10.17    0.000012           6         2           open
  2.54    0.000003           2         2           fstat
  2.54    0.000003           3         1           brk
  1.69    0.000002           2         1           read
  1.69    0.000002           1         2           close
  0.85    0.000001           1         1           clone
  0.85    0.000001           1         1           execve
  0.85    0.000001           1         1           arch_prctl
------ ----------- ----------- --------- --------- ----------------
100.00    0.000118                    28         3 total

如预期一样,没有fork调用。只有原始的clone系统调用,其标志、子进程堆栈等都已正确设置。


非常好的回答,干得好,Ciro。您介意修复那些现在失效的链接吗? - Goncalo
2
在某些架构上,fork()系统调用会发生,但fork()在内核模式下仍会调用clone()。 - Joshua

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接