对于这样的问题,总是要阅读源代码。
从glibc的nptl/sysdeps/unix/sysv/linux/fork.c
(GitHub)中我们可以找到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参数被省略。此外,参数顺序也发生了变化。
fork()
不会调用clone()
,它们都是使用系统调用clone
的函数。 - Javier@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
系统调用,其标志、子进程堆栈等都已正确设置。
clone
文档,与从“syntax”文本链接到的页面相同。也许您想链接到fork
文档。该文档说明fork
使用设置为SIGCHLD
的标志调用clone
。大概是告诉clone
改变其正常行为,并继续执行作为调用的返回,而不是调用新例程。我会质疑SIGCHLD
是否正确;我希望看到更像CLONE_CHILD
的东西。 - Eric Postpischilclone()
在创建线程时使用函数地址(通过例如pthread_create()
传递),而在创建进程时,它直接使用堆栈中的返回地址。 - 0xF1fork
的正确实现方式。从fork
返回后,堆栈应该看起来像从fork
返回(与调用之前完全相同),而不是像从调用fork
的位置调用子程序。 - Eric Postpischil