fork,vfork,clone都是linux系统调用,这三个函数分别调用sys_fork,sys_vfork,sys_clone,最终都会调用到do_fork函数。差别就在于参数的传递和一些准备工作的不同,上一章节已经详细学习了fork的流程,本章主要专注学习这三个接口函数的使用方法和差异点。
1 进程的四要素
linux进程所必须的四个要素:
- 程序代码,有一段程序供其执行: 代码不一定是进程专有,可以与其它进程共享
- 有自己专用系统堆栈空间:
- 有进程控制块(task_struct):
- 有独立的存储空间:
以上4条,缺一不可。如果缺少第四条,那么就称其为"线程"。如果完全没有用户空间,称其位”内核线程“;如果共享用户空间,则称其为”用户线程"。
2 fork
系统调用fork,允许父进程创建一个新的进程(子进程)。新的子进程是父进程的翻版:完全继承父进程的栈、数据段、堆和执行文本的拷贝。其接口如下:
NAME
fork - create a child process
SYNOPSIS
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
完成对其调用后将存在两个进程,且每个进程都会从fork的返回处继续执行。这两个进程将执行相同的程序文本段,但却各自拥有不同的栈段、数据段以及堆段拷贝。子进程的栈、数据以及栈段开始时是对父进程内存相应各部分的完全复制。执行fork之后,每个进程均可修改各自的栈数据以及堆中的变量而不影响另一进程。
为调用进程创建一个一模一样的新进程 ,但父子进程需要改变时候,执行一个copy,但是任何修改都造成分裂,如:chroot, open, 写memory,mmap,sigaction….
fork的示例
考虑以下代码的输出,假设test.txt中的内容”abcdefghijklmnopqrst…”
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include<fcntl.h>
int main(void)
{
char str[10];
int count = 1;
int fd = open("test.txt", O_RDWR);
if(fork() == 0)
{
int cnt = read(fd, str, 10);
printf("Child process : %s\n", (char *)str);
printf("This is son, his count is: %d (%p). and his pid is: %d\n", ++count, &count, getpid());
}
else
{
int cnt = read(fd, str, 10);
printf("Child process : %s\n", (char *)str);
printf("This is father, his count is: %d (%p), his pid is: %d\n", count, &count, getpid());
}
return 0;
}
输出为:
-
从结果来看,子进程和父进程的PID不同,内存资源count是值的复制,子进程改变了count的值,而父进程中的count的值没有改变,这个过程请参考之前章节的写时复制技术。
-
两个进程共享了同一个指向文件的结构体,所以当子进程输出“abcdefghij”后,父进程就接着输出"klmnopqrst"
3 vfork
vfork也是创建子进程,但是子进程共享父进程的空间。在vfork创建子进程之后,父进程阻塞,直到子进程执行exec()或者exit()。vfork设计的最初是因为fork没有实现COW机制,很多情况下fork之后会紧跟着exec,而exec的执行相当于前面fork复制的空间全部变得无用,所以设计了vfork。而现在fork使用了COW,唯一的代价仅仅是复制父进程页表的代价,所以vfork的功能就变得越来越不重要。
NAME
vfork - create a child process and block parent
SYNOPSIS
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
vfork因为如下两个特性而更具效率,也是区别与fork所在:
-
无需为子进程复制虚拟内存页或页表,相反,子进程共享父进程的内存,直至其成功执行exec或调用exit退出
-
在子进程调用exit或exec之前,将暂停执行父进程,所以在使用vfork时,一般立即在vfork之后调用exec,如果exec调用失败,子进程应调用exit退出。
vfork示例
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
int count =1;
int child;
printf("Before create son, the father's count is %d\n",count);
if(!(child = vfork()))
{
int i = 0;
for( i = 0; i< 3; i++)
{
count++;
printf("This is son This i is: %d count: %d\n", i, count);
if(i == 2)
{
printf("This is son This pid is: %d count: %d\n", getpid(), count);
exit(1);
}
}
}
else
{
printf("This is father This pid is: %d count: %d\n", getpid(), count);
}
return 0;
}
输出:
- 用
vfork
创建的子进程与父进程共享地址空间
,也就是说子进程完全运行在父进程的地址空间上,如果这时子进程修改了某个变量,这将影响到父进程 子进程
在vfork()
返回后直接运行在父进程的栈空间,并使用父进程的内存和数据。这意味着子进程可能破坏父进程的数据结构或栈
,造成失败。为了避免这些问题,需要确保一旦调用vfork(),子进程就不从当前的栈框架中返回
,并且如果子进程改变了父进程的数据结构就不能调用exit函数。子进程还必须避免改变全局数据结构或全局变量中的任何信息,因为这些改变都有可能使父进程
不能继续。- 值得注意的是用vfork创建的子进程必须显示调用exit()来结束,否则子进程将不能结束,父进程就讲一直阻塞,出现异常
大家可以实际将上述例子中exit(1)这个注释掉后,会出现什么情况。对于Vfork和fork是类似的,除了下面两点:
1、阻塞父进程
2、不复制父进程的页表
之所以vfork要阻塞父进程是因为vfork后父子进程使用的是完全相同的mm_struct,也就是由完全相同的虚拟地址空间,包括栈也相同,所以两个进程就不能同时运行,否则栈就会乱掉。所以vfork后,父进程是阻塞的,直到调用了exec系列或者exit后,这个时候,子进程的mm需要释放,不再与父进程公用,这个时候就可以解除父进程的阻塞状态。
4 clone
clone是Linux为创建线程设计的,所以可以说clone是fork的升级版本,不仅可以创建进程或线程,还可以指定创建新的命名空间,有选择的继承父进程的内存、甚至可以将创建出来的进程编程父进程的兄弟进程等。
clone函数功能强大,待有很多参数,提供了一个非诚灵活自由的常见进程的方法,因此它创建进程要比前面两种方法更为复杂。clone可以有选择继承父进程的资源,你可以选择像vfork一样和父进程共享一个虚拟存储空间,也可以不和父进程共享,甚至可以选择创造出来的进程和父进程不再是父子关系,而是兄弟关系。
NAME
clone, __clone2 - create a child process
SYNOPSIS
/* Prototype for the glibc wrapper function */
#define _GNU_SOURCE
#include <sched.h>
int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
/* pid_t *ptid, void *newtls, pid_t *ctid */ );
参数 | 含义 |
---|---|
fn为函数指针 | 此指针指向一个函数体,即想要创建进程的静态程序 |
child_stack | 为给子进程分配系统堆栈的指针 |
arg | 传给子进程的参数一般为(0) |
flags | 要复制资源的标志,描述你需要从父进程继承那些资源(是资源复制还是共享,在这里设置参数) |
下面是flaga可以取得值
标志 | 含义 |
---|---|
CLONE_PARENT | 建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子” |
CLONE_FS | 子进程与父进程共享相同的文件系统,包括root、当前目录、umask |
CLONE_FILES | 子进程与父进程共享相同的文件描述符(filedescriptor)表 |
CLONE_NEWNS | 在新的namespace启动子进程,namespace描述了进程的文件hierarchy |
CLONE_SIGHAND | 子进程与父进程共享相同的信号处理(signalhandler)表 |
CLONE_PTRACE | 若父进程被trace,子进程也被trace |
CLONE_VFORK | 父进程被挂起,直至子进程释放虚拟内存资源 |
CLONE_VM | 子进程与父进程运行于相同的内存空间 |
CLONE_PID | 子进程在创建时PID与父进程一致 |
CLONE_THREAD | Linux2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群 |
#define _GNU_SOURCE
#include <sys/wait.h>
#include <sys/utsname.h>
#include <sched.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>
#define FIBER_STACK 8192
int a;
void *stack;
int do_something()
{
a = 10;
printf("This is son, the pid is: %d, the a is: %d\n",getpid(), a);
free(stack);
exit(1);
}
int main(void)
{
void *stack;
a = 1;
stack = malloc(FIBER_STACK);
if(!stack)
{
printf("The stack failed\n");
exit(0);
}
printf("Create son thread \n");
clone(&do_something, (char *)stack + FIBER_STACK, CLONE_VM | CLONE_VFORK, 0);
printf("This is father, the pid is: %d, the a is: %d\n",getpid(), a);
return 0;
}
输出结果:
inux创建线程的API,本质上去调 clone。要求把P2的所有资源的指针,都指向P1。线程,也被称为 Light weight process。而Linux在clone线程时也十分灵活,可以选择共享/不共享部分资源。
POSIX标准要求,进程里面如果有多个线程,在用户空间 getpid() 看到的都是同一个id,这个id其实是TGID。一个进程里面创建了多个线程,在/proc 下 的是 tgid,/proc/tgid/task/{pidx,y,z}pthread_self() 看到的是用户空间pthread线程库里获得的id 。
5. 总结
下面是三个接口的优缺点对比
类型 | 优点 | 缺点 |
---|---|---|
fork | 1.接口非常简洁2.将进程“创建”和执行(exec)解耦,提高了灵活性3.刻画了进程间的内在关系(进程树、进程组) | 1.完全拷贝,过于粗暴(不如clone)2.性能差,可扩展性差(不如vfork)3.不可组合(如fork()+pthread()) |
vfork | 1.类似fork,但让父子进程共享同一地址空间2.连映射关系都不需要拷贝,性能更好 | 1.只能用在fork+exec的场景中2.共享地址空间存在安全问题 |
clone | 1.fork的进阶版本,可以选择地不拷贝内存2.高度可控,可按照需求调整 | 接口比fork复杂,选择性拷贝容易出错 |