在分析这三个系统调用之前,先来理解一下进程的4要素:
- 执行代码 每个进程或者线程都要有自己的执行代码,不论是独立的,还是共享的一段代码。
- 私有堆栈空间 进程具有自己独立的堆栈空间,但是对于线程来说它是共享的。
- 进程控制块(task_struct) 不论是进程还是线程,都有自己的task_struct。Linux对于线程的实现采用”轻进程”。
- 有独立的内存空间 线程具有独立的内存空间,而线程共享内存空间。
Linux内核用于创建进程的系统调用有3个,它们的实现分别为:fork、vfork、clone。它们的作用如下表所示:
调用 | 描述 |
---|---|
clone | 创建轻量级进程(也就是线程),pthread库基于此实现 |
vfork | 父子进程共享资源,子进程先于父进程执行 |
fork | 创建父进程的完整副本 |
下面我们来看一下3个函数的区别:
1. clone()
创建轻量级进程,其拥有的参数是:
- fn 指定新进程执行的函数。当从函数返回时,子进程终止。函数返回一个退出码,表明子进程的退出状态。
- arg 指向fn()函数的参数。
- flags 一些标志位,低字节是表示当子进程终止时发送给父进程的信号,通常是SIGCHLD信号。其余的3个字节是一组标志,如下表所示: 名称描述CLONE_VM共享内存描述符和所有的页表CLONE_FS共享文件系统CLONE_FILES共享打开的文件CLONE_SIGHAND共享信号处理函数,阻塞和挂起的信号等CLONE_PTRACEdebug用,父进程被追踪,子进程也追踪CLONE_VFORK父进程挂起,直到子进程释放虚拟内存资源CLONE_PARENT设置子进程的父进程是调用者的父进程, 也就是创建兄弟进程CLONE_THREAD共享线程组,设置相应的线程组数据CLONE_NEWNS设置自己的命令空间,也就是有独立的文件系统CLONE_SYSVSEM共享System V IPC可撤销信号量操作CLONE_SETTLS为轻量级进程创建新的TLS段CLONE_PARENT_SETTID写子进程PID到父进程的用户态变量中CLONE_CHILD_CLEARTID设置时,当子进程exit或者exec时,给父进程发送信号
- child_stack 指定子进程的用户态栈指针,存储在子进程的esp寄存器中。父进程总是给子进程分配一个新的栈。
- tls 指向为轻量级进程定义的TLS段数据结构的地址。只有CLONE_SETTLS标志设置了才有意义。
- ptid 指定父进程的用户态变量地址,用来保存新建轻量级进程的PID。只有CLONE_PARENT_SETTID标志设置了才有意义。
- ctid 指定新进程保存PID的用户态变量的地址。只有CLONE_CHILD_SETTID标志设置了才有意义。
clone()其实是一个C库中的封装函数,它建立新进程的栈并调用sys_clone()系统调用。sys_clone()系统调用没有参数fn和arg。事实上,clone()把fn函数的指针保存到子进程的栈中return地址处,指针arg紧随其后。当clone()函数终止时,CPU从栈上获取return地址并执行fn(arg)函数。
下面我们看一个C代码示例,看看clone()函数的使用:
代码语言:javascript复制#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <linux/sched.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#define FIBER_STACK 8192
int a;
void *stack;
int do_something()
{
printf("This is the son, and my pid is:%d,
and a = %dn", getpid(), a);
free(stack);
exit(1);
}
int main()
{
void * stack;
a = 1;
/* 为子进程申请系统堆栈 */
stack = malloc(FIBER_STACK);
if(!stack)
{
printf("The stack failedn");
exit(0);
}
printf("Creating son thread!!!n");
clone(&do_something, (char *)stack FIBER_STACK, CLONE_VM|CLONE_VFORK, 0);//创建子线程
printf("This is the father, and my pid is: %d, and a = %dn", getpid(), a);
exit(1);
}
上面的代码就相当于实现了一个vfork(),只有子进程执行完并释放虚拟内存资源后,父进程执行。执行结果是:
代码语言:javascript复制Creating son thread!!!
This is the son, and my pid is:3733, and a = 2
This is the father, and my pid is: 3732, and a = 2
它们现在共享堆栈,所以a的值是相等的。
2. fork()
linux将fork实现为这样的clone()系统调用,其flags参数指定为SIGCHLD信号并清除所有clone标志,child_stack参数是当前父进程栈的指针。父进程和子进程暂时共享相同的用户态堆栈。然后采用 写时复制技术,不管是父进程还是子进程,在尝试修改堆栈时,立即获得刚才共享的用户态堆栈的一个副本。也就是成为了一个单独的进程。
代码语言:javascript复制#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
int count = 1;
int child;
child = fork();
if(child < 0){
perror("fork error : ");
}
else if(child == 0){ // child process
printf("This is son, his count is: %d (%p). and his pid is: %dn",
count, &count, getpid());
}
else{ // parent process
printf("This is father, his count is: %d (%p), his pid is: %dn",
count, &count, getpid());
}
return EXIT_SUCCESS;
}
上面代码的执行结果:
代码语言:javascript复制This is father, his count is: 1 (0xbfdbb384), his pid is: 3994
This is son, his count is: 2 (0xbfdbb384). and his pid is: 3995
可以看出,父子进程的PID是不一样的,而且堆栈也是独立的(count计数一个是1,一个是2)。
3. vfork()
将vfork实现为这样的clone()系统调用,flags参数指定为SIGCHLD|CLONE_VM|CLONE_VFORK信号,child_stack参数等于当前父进程栈指针。
vfork其实是一种过时的应用,vfork也是创建一个子进程,但是子进程共享父进程的空间。在vfork创建子进程之后,阻塞父进程,直到子进程执行了exec()或exit()。vfork最初是因为fork没有实现COW机制,而在很多情况下fork之后会紧接着执行exec,而exec的执行相当于之前的fork复制的空间全部变成了无用功,所以设计了vfork。而现在fork使用了COW机制,唯一的代价仅仅是复制父进程页表的代价,所以vfork不应该出现在新的代码之中。
实际上,vfork创建的是一个线程,与父进程共享内存和堆栈空间:
我们看下面的示例代码:
代码语言:javascript复制#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int count = 1;
int child;
printf("Before create son, the father's count is:%dn", count);
if(!(child = vfork())) {
printf("This is son, his pid is: %d and the count is: %dn",
getpid(), count);
exit(1);
}
else {
printf("This is father, pid is: %d and count is: %dn",
getpid(), count);
}
}
执行结果为:
代码语言:javascript复制Before create son, the father's count is:1
This is son, his pid is: 3564 and the count is: 2
This is father, pid is: 3563 and count is: 2
从运行结果看,vfork创建的子进程(线程)共享了父进程的count变量,所以,子进程修改count,父进程的count值也改变了。
另外由vfork创建的子进程要先于父进程执行,子进程执行时,父进程处于挂起状态,子进程执行完,唤醒父进程。除非子进程exit或者execve才会唤起父进程,看下面程序:
代码语言:javascript复制#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int count = 1;
int child;
printf("Before create son, the father's count is:%dn", count);
if(!(child = vfork())) {
int i;
for(i = 0; i < 100; i )
{
printf("Child process & i = %dn", i);
count ;
if(i == 20)
{
printf("Child process & pid = %d;count = %dn",
getpid(), count);
exit(1);
}
}
}
else {
printf("Father process & pid = %d ;count = %dn",
getpid(), count);
}
}
执行结果为:
代码语言:javascript复制Before create son, the father's count is:1
Child process & i = 0
Child process & i = 1
Child process & i = 2
Child process & i = 3
Child process & i = 4
Child process & i = 5
Child process & i = 6
Child process & i = 7
Child process & i = 8
Child process & i = 9
Child process & i = 10
Child process & i = 11
Child process & i = 12
Child process & i = 13
Child process & i = 14
Child process & i = 15
Child process & i = 16
Child process & i = 17
Child process & i = 18
Child process & i = 19
Child process & i = 20
Child process & pid = 3755;count = 23
Father process & pid = 3754 ;count = 23
从上面的结果可以看出,父进程总是等子进程执行完毕后才开始继续执行。
总结
- clone、vfork和fork是根据不同的需求而开发的。
- clone 参数比较多,可以实现的控制就比较多,clone的设计初衷是给pthread线程库的开发提供支持的。其实用它也完全可以实现另外两种系统调用。
- vfork是一个过时的系统调用,当时是因为写时复制(COW)技术还没有。所以才设计了这个子进程先于父进程的执行的创建进程的系统调用。
- fork就是一个创建完整进程的调用。
- clone、vfork和fork在内核层都是调用的_do_fork()这个函数。