fork()、vfork()、clone()的区别(三)

  在linux源码中这三个调用的执行过程是执行fork(),vfork(),clone()时,通过一个系统调用表映射到sys_fork(),sys_vfork(),sys_clone(),再在这三个函数中去调用do_fork()去做具体的创建进程工作。

一 fork

  fork创造的子进程复制了父亲进程的资源(写时复制技术),包括内存的内容task_struct内容(2个进程的pid不同)。这里是资源的复制不是指针的复制。说到fork(),就不得不说一个技术:(Copy-On-Write)写时复制技术。盗用一张图,感觉描述的确实挺到位:
在这里插入图片描述
  我们都知道fork创建进程的时候,并没有真正的copy内存(听着好像矛盾了,资源的赋值为什么有没有真正的赋值呢?),因为我们知道,对于fork来讲,有一个很讨厌的东西叫exec系列的系统调用,它会勾引子进程另起炉灶。如果创建子进程就要内存拷贝的的话,一执行exec,辛辛苦苦拷贝的内存又被完全放弃了。由于fork()后会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,处于效率考虑,linux中引入了“写时复制技术-Copy-On-Write”。
   换言之,在fork()之后exec之前两个进程用的是相同的物理空间(内存区),先把页表映射关系建立起来,并不真正将内存拷贝。子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。当父进程中有更改相应段的行为发生时,如进程写访问,再为子进程相应的段分配物理空间,如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。fork时子进程获得父进程数据空间、堆和栈的复制所以变量的地址(当然是虚拟地址)是一样的。
  具体过程是这样的:
   fork子进程完全复制父进程的栈空间,也复制了页表,但没有复制物理页面,所以这时虚拟地址相同,物理地址也相同,但是会把父子共享的页面标记为“只读”,如果父子进程一直对这个页面是同一个页面,直到其中任何一个进程要对共享的页面“写操作”,这时内核会复制一个物理页面给这个进程使用,同时修改页表。而把原来的只读页面标记为“可写”,留给另外一个进程使用。这就是所谓的“写时复制”。
  但实际上,linux为了提高fork的效率,采用了copy-on-write技术,fork后,这两个虚拟地址实际上指向相同的物理地址。(内存页),只有任何一个进程试图修改这个虚拟地址里的内容前,两个虚拟地址才会指向不同的物理地址。新的物理地址的内容从源物理地址中复制得到。
问题:fork采用了这种写时复制的机制,那么fork出来子进程后,理论上子进程和父进程哪个先调度呢(理论效率分析,个人觉得有一定的道理)?
  fork之后内核一般会通过将子进程放在队列的前面,以让子进程先执行,因为很多情况下子进程要马上执行exec,会清空栈、堆,这些和父进程共享的空间,加载新的代码段。这就避免了父进程“写时复制”拷贝共享页面的机会。如果父进程先调度很可能写共享页面,而子进程什么也没做,会产生“写时复制”的无用功。所以,一般子进程先调度。避免因无意义的复制而造成效率的下降。
  如下示例:

#include <stdio.h>

int main()
{
	int count = 1;
	int child;
	int i;
	if(!(child = fork()))
	{

		for(i = 0; i <20; i++)
		{
			printf("This is son, his count is: %d. and his pid is: %d\n", i, getpid());
		}
	} 
	else 
	{
		for(i=0;i<20;i++)
			printf("This is father, his count is: %d, his pid is: %d\n", count, getpid());
	}
}

  而当子进程改变了父进程的变量时候,会通过copy_on_write的手段为所涉及的页面建立一个新的副本。子进程才新建了一个页面复制原来页面的内容,基本资源的复制是必须的,而且是高效的。整体看上去就像是父进程的独立存储空间也复制了一遍。其次,我们看到子进程和父进程直接没有互相干扰,明显2者资源都独立了。从运行的结果可以看出父子2个进程是同步运行的,其实不分先后。

二 vfork

  vfork是一个过时的应用,vfork也是创建一个子进程,但是子进程共享父进程的空间。在vfork创建子进程之后,父进程阻塞,直到子进程执行了exec()或者exit()。vfork最初是因为fork没有实现COW机制,而很多情况下fork之后会紧接着exec,而exec的执行相当于之前fork复制的空间全部变成了无用功,所以设计了vfork。而现在fork使用了COW机制,唯一的代价仅仅是复制父进程页表的代价,所以vfork不应该出现在新的代码之中。
  vfork创建出来的不是真正意义上的进程,而是一个线程,因为它缺少进程要素(4),独立的内存资源,看下面的程序:
  另外由vfork创建的子进程要先于父进程执行,子进程执行时,父进程处于挂起状态,子进程执行完,唤醒父进程。除非子进程exit或者execve才会唤起父进程。

三 clone

#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 */ );

  clone是Linux为创建线程设计的(虽然也可以用clone创建进程)。所以可以说clone是fork的升级版本,不仅可以创建进程或者线程,还可以指定创建新的命名空间(namespace)、有选择的继承父进程的内存、甚至可以将创建出来的进程变成父进程的兄弟进程等等。
  clone函数功能强大,带了众多参数,它提供了一个非常灵活自由的常见进程的方法。因此由他创建的进程要比前面2种方法要复杂。clone可以让你有选择性的继承父进程的资源,你可以选择想vfork一样和父进程共享一个虚存空间,从而使创造的是线程,你也可以不和父进程共享,你甚至可以选择创造出来的进程和父进程不再是父子关系,而是兄弟关系。先有必要说下这个函数的结构:
  
  

发布了67 篇原创文章 · 获赞 26 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/essity/article/details/82291378
今日推荐