fork,vfork,clone函数的区别及其联系

fork,vfork,clone函数的区别及其联系

fork

 fork函数用于创建子进程,典型的调用一次,返回两次的函数,其中返回子进程的PID和0,其中调用进程返回了子进程的PID,而子进程则返回了0。

当发出fork()系统调用时,内核原样复制父进程的整个地址空间并把复制的那一份分配给子进程(把所有的资源复制给新创建的进程,,进程的pid号不一样)。但这种复制行为非常耗时,因为它需要:为子进程的页表分配页面,为子进程的页分配页面,初始化子进程的页表,把父进程的页复制到子进程相应的页中。由于创建一个物理地址空间的这种方法涉及许多内存访问,消耗许多CPU周期,并且完全破坏了高速缓存中的内容。在大多数情况下,这样做常常是毫无意义的,因为许多子进程通过装入一个新的程序开始它们的执行,这样就完全丢弃了所继承的地址空间。故引入了写时复制技术(COW)

  • COW技术

内核只为新生成的子进程创建虚拟空间,父进程和子进程共享页面而不是复制页面。然而,只要页面被共享,它们就不能被修改。无论父进程和子进程何时试图写一个共享的页面,就产生一个错误,这时内核就把这个页复制到一个新的页面中并标记为可写。原来的页面仍然是写保护的:当其它进程试图写入时,内核检查写进程是否是这个页面的唯一属主;如果是,它把这个页面标记为对这个进程是可写的。从而fork复制的开销就是:复制父进程的页表以及给子进程创建一个进程描述符。在一般情况下,进程创建后都会马上运行一个可执行的文件,这种优化,可以避免拷贝大量根本就不会被使用的数据(地址空间里常常包含数十兆的数据)。

故COW的核心是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

  • 子进程的物理空间没有代码,怎么去取指令执行exec系统调用呢?

在fork之后exec之前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。    

一般情况下,fork之后内核会通过将子进程放在队列的前面,以让子进程先执行,以免父进程执行导致写时复制,而后子进程执行exec系统调用,因无意义的复制而造成效率的下降。 

vfork

内核连子进程的虚拟地址空间也不创建,直接共享了父进程的虚拟空间

vfork()跟fork()类似,都是创建一个子进程,这两个函数的的返回值也具有相同的含义。但是vfork()创建的子进程基本上只能做一件事,那就是立即调用_exit()函数或者exec函数族成员,调用任何其它函数(包括exit())、修改任何数据(除了保存vfork()返回值的那个变量)、执行任何其它语句(包括return)都是不应该的。此外,调用vfork()之后,父进程会一直阻塞,直到子进程调用_exit()终止,或者调用exec函数族成员,父进程才可能被调度运行(由于子进程在父进程中执行(栈中),故父进程会一直阻塞)。子进程对任何数据变量进行修改,都会影响到父进程——故在子进程中不能随便调用别函数。

  • 为什么vfork()子进程中可以调用_exit(),却不可以调用exit(),也不可以直接return呢?

exit()是对_exit()的封装,它自己在调用_exit()前会做很多清理工作,其中包括刷新并关闭当前进程使用的流缓冲(比如stdio.h里面的printf等),由于vfork()的子进程完全共享了父进程地址空间,子进程里面的流也是共享的父进程的流,所以子进程里面是不能做这些事的。
直接return就更不行了,子进程return以后,会从当前函数的外部调用点后面继续执行,这后面子进程可能将会执行很多语句,结果就没法预料了。

#include <stdio.h>
#include <unistd.h>

void stack1() {
	vfork();
	printf("%d\n",getpid());
}

void stack2() {
	printf("%d",getpid());
	_exit(0);
	printf("%d\n",getpid());//当退出后,此句不执行,即_exit(0)后续的不执行。
	
}

int main() {

	stack1();
	printf("%d goes 1\n", getpid());
	stack2();
	printf("%d goes 2\n", getpid());
	return 0;
}

运行结果为:(若父进程号为11,子进程号为12)12 \n 12 goes 1 1211  \n 11 goes 2。说明了父进程仍然也保存了创建进程的函数栈,并从此处开始执行,当返回到主函数时,若子进程执行过的语句,则不执行(由于IP指针发生了变化,而父子进程共享IP指针)。

故vfork有限制,子进程生成后,父进程在vfork中被内核挂起,直到子进程有了自己的内存空间(exec**{装载其它执行程序})或退出(_exit)。在此之前,子进程不能从调用vfork的函数中返回(同时,不能修改栈上变量、不能继续调用除_exit或exec系列之外的函数,否则父进程的数据可能被改写)。

  • vfork使用说明

  • 由vfork创造出来的子进程还会导致父进程挂起,除非子进程exit或者execve才会唤起父进程
  • 由vfok创建出来的子进程共享了父进程的所有内存,包括栈地址,直至子进程使用execve启动新的应用程序为止
  • 由vfork创建出来得子进程不应该使用return返回调用者,或者使用exit()退出,但是它可以使用_exit()函数来退出
  • fork与vfork的区别

  • fork会复制父进程的页表,而vfork不会复制,让子进程共享父进程的页表
  • fork使用了写时复制技术,而vfork没有,即它任何时候都不会复制父进程地址空间
  • fork父子进程执行次序不确定,一般先是子进程执行;vfork保证子进程现在执行。

vfork()保证子进程先运行,在她调用exec或_exit之后父进程才可能被调度运行。如果在 调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。

clone

clone函数功能强大,带了众多参数,因此由他创建的进程要比前面2种方法要复杂,而fork与vfork都是无参数的,即共享那些资源早已规定。

clone可以让你有选择性的继承父进程的资源,你可以选择想vfork一样和父进程共享一个虚存空间,从而使创造的是线程,你也可以不和父进程共享,你甚至可以选择创造出来的进程和父进程不再是父子关系,而是兄弟关系。

三者实现方式

系统调用服务例程sys_clone, sys_fork, sys_vfork三者最终都是调用do_fork函数完成。do_fork的参数与clone系统调用的参数类似, 不过多了一个regs(内核栈保存的用户模式寄存器), 实际上其他的参数也都是用regs取的。

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章