Linux 系统调用 fork()、vfork() 和 clone()

fork()

生成一个进程,实际上就是把当前进程复制一份。 除了一些特殊的方面(比如,进程
号),新的进程(子进程)和老的进程(父进程)几乎一模一样。fork()虽然只调
用了一次,但会在父进程和子进程中分别返回(等于是一共返回了两次),父进程中
返回所创建子进程的pid,子进程中返回 0。在 fork() 结束后,父进程和子进程
的执行顺序不确定,由高度程序决定谁先执行。不过可以在父进程中调用wait()
待子进程结束。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
int count = 0;

if (fork()) { /* parent */
wait(0);
printf("In parent\n");
} else { /* child */
printf("In child\n");
}
return 0;
}

wait(0)中的参数0表示等待任一(进程组 ID 相同的)子进程结束,也可以将
fork()的返回值保存下来,作为wait()的参数。

vfork()

vfork()的产生原因来理解它会比较容易。因为fork()操作会将当前进程的任何
资源几乎完全复制一份,其中包括了地址空间。一般fork()调用后都会跟着调用
execve(),用新的内存镜像取代原来的内存镜像,当地址空间很大时,复制的操作会
很费时,而且又是做无用功,所以就产生了vfork()

vfork()产生的子进程与父进程共享地址空间,就没有了复制产生的开销。而且
vfork()保证父进程在子进程调用execve()exit()之前不会执行。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
int count = 0;

printf("count before fork = %d\n", count);
if (vfork()) {
printf("In parent: count = %d\n", count);
} else {
printf("In child: count = %d\n", ++count);
}
exit(0);
}

注意程序不能使用return语句退出(至少子进程不能使用),原因我猜测是
return语句会使得main函数的栈被清空,因为是使用的同一个内存空间,子进程
把栈清空后,父进程的栈就被破坏了,于是就出错了。使用exit()可以避免这个问题。

运行结果如下:

1
2
3
4
5
$ clang vfork.c 
$ ./a.out
count before fork = 0
In child: count = 1
In parent: count = 1

可以看到父进程中变量的值确实被修改过了。

clone()

clone()可以更细粒度地控制与子进程共享的资源,因而参数也更复杂。原型如下:

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

根据 man 2 clone 上的描述,这个原型并不是最底层的系统调用,而是封装过的,
真正的系统调用这里不给出,有兴趣可以查看手册。

clone()创建子进程成功后,应付执行函数(fn(arg))。因为要执行不同的函数,
当然就不能和父进程共用一个栈了,第二个参数child_stack就为子进程提供了栈空
间。参数flags提供了克隆过程中的各个控制选项,具体还是查看手册。

手册中也说了,clone()的用途之一就是创建线程,多个线程并行地运行在同一个地
址空间里。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#define _GNU_SOURCE   /* Must define before other headers */
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sched.h>

#define CHILD_STACK 8192

int count = 0;

int child_run(void *arg)
{
printf("count in child: %d\n", ++count);
return 0;
}

int main(int argc, char *argv[])
{
int pid, status;
void *child_stack = malloc(CHILD_STACK);
if (!child_stack) {
fprintf(stderr, "failed to allocate child stack\n");
exit(1);
}

printf("count before clone: %d\n", count);
/* Simulate vfork */
pid = clone(child_run,
(void*)((char*)child_stack+CHILD_STACK),
CLONE_VM | CLONE_VFORK,
0);

if (pid == -1) {
fprintf(stderr, "failed to clone\n");
perror("clone failed: ");
exit(2);
} else {
waitpid(pid, &status, 0);
printf("count after clone: %d\n", count);
}
return 0;
}

这个程序模仿了上面vfork()的功能。程序最开始的#define _GNU_SOURCE不能省
略,不然会报错,虽然没有完全测试,但好像必须放在所有头文件的上面。

References

Linux 手册:man 2 clone
linux下 fork(),vfork(),clone()的用法及区别
CLONE_VM undeclared

0%