fork()函数功能——创建新进程
1、父子进程有独立的数据段、堆、栈,共享代码段
Linux中每个进程都有4G的虚拟地址空间(独立的3G用户空间和共享的1G内核空间),fork()创建的子进程也不例外。子进程资源的由来:
1、1G内核空间既然是所有进程共享,因此fork()创建的子进程自然也将拥有;
2、3G的用户空间是从父进程进程而来。
fork()创建子进程时继承了父进程的数据段、代码段、栈段、堆,注意从父进程继承来的是虚拟地址空间,同时也复制了页表(没有复制物理块)。因此,此时父子进程拥有相同的虚拟地址,映射的物理内存也是一致的(独立的虚拟地址空间,共享父进程的物理内存)。
由于父进程和子进程共享物理页面,内核将其标记为“只读”(类似mmap)的private的方式),父子双方均无法对其修改。无论父进程和子进程何时试图对一个共享的页面执行写操作,就产生一个错误,这时内核就把这个页复制到一个新的页面给这个进程,并标记为可写,同时修改页表,把原来的只读页面标记为“可写”,留给另外一个进程使用——写时复制技术。
注意:内核在为子进程分配物理内存时,并没有将代码段对应的数据另外复制一份给子进程,最终父子进程代码段映射的是同一块物理内存(代码段在单个进程内部本来就是只读的)。
每个进程的虚拟地址空间都可以是0到4G,只不过其中只有一部分有权访问,每个进程可以有不同的映射。两次运行同一个程序就是使用的相同的虚拟地址,但是映射到的物理地却是不一样的。每个进程都有自己的虚拟地址空间,不同进程的相同的虚拟地址显然可以对应不同的物理地址。因此地址相同(虚拟地址)而值不同没什么奇怪。
2、一次调用两次执行
3、 竞争条件
Linux是一个多用户操作系统,在同一时间会有许多的用户在争夺系统的资源.有时进程为了早一点完成任务就创建子进程来争夺资源. 一旦子进程被创建,父子进程一起从fork处继续执行,相互竞争系统的资源.有时候我们希望子进程继续执行,而父进程阻塞,直到子进程完成任务.这个时候我们可以调用wait或者waitpid系统调用.
对子进程来说,fork返回给它0,但它的pid绝对不会是0;之所以fork返回0给它,是因为它随时可以调用getpid()来获取自己的pid;fork之后父子进程除非采用了同步手段,否则不能确定谁先运行,也不能确定谁先结束。认为子进程结束后父进程才从fork返回的,这是不对的,fork不是这样的,vfork才这样。
4、fork之后跟随exec
fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,linux中引入了“写时复制“技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。由于写时复制,在fork之后exec之前两个进程有独立的虚拟地址空间,共享物理内存。只有其中一方需要写操作时,再为子进程的数据段、栈、堆分配物理空间。但在子进程上调用exec时,会清空栈、堆,以及和父进程共享的空间,重新加载新的代码段,这样避免了“写时复制”拷贝共享页面的机会,父进程也同时独自拥有了原来共享的物理内存(可对其读写操作)。
fork出来子进程之后,父子进程哪个先调度直接决定了是否需要拷贝的问题?内核一般会先调度子进程,因为很多情况下子进程是要马上执行exec,而避免无用的复制。如果父进程先调度很可能写共享页面,会产生“写时复制”的无用功。所以,一般是子进程先调度滴。
在网上看到还有个细节问题就是,fork之后内核会通过将子进程放在队列的前面,以让子进程先执行,以免父进程执行导致写时复制,而后子进程执行exec系统调用,因无意义的复制而造成效率的下降。
如果不是因为exec,子进程执行时很可能修改内存,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。
5、fork()函数的实现过程