fork到底复制了父进程的哪些资源?

我们来看一个例子

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>

int main()
{
	pid_t pid;
	int num = 5;
	int status;
	pid = fork();
	switch(pid)
	{
		case -1:
			perror("create porcess is failed");
			exit(-1);
		case 0:
			printf("child process num is:%d\n", num);
			exit(0);
		default:
			wait(&status);
			printf("parent process num is: %d\n", num);
			break;
	}
	return 0;
}

运行结果如下:
这里写图片描述
我们发现这个时候两者的num值是相同的,那我们想想如果在子进程中改变一下num这个值,父进程中的num值会被改变吗?

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>

int main()
{
	pid_t pid;
	int num = 5;
	int status;
	pid = fork();
	switch(pid)
	{
		case -1:
			perror("create porcess is failed");
			exit(-1);
		case 0:
		    num = 10;
			printf("child process num is:%d\n", num);
			exit(0);
		default:
			wait(&status);
			printf("parent process num is: %d\n", num);
			break;
	}
	return 0;
}

这里写图片描述
这时我们发现子进程中的num值改变了而父进程中的值没有改变,这是为什么呢?
这个时候要牵扯到两个概念,一个是逻辑地址(虚拟地址),一个是物理地址。

  • 逻辑地址 cpu所生成的地址。cpu产生的逻辑地址分为:p(页号)它包含在每个页在物理内存中的基址,用来做页表的索引;d(页偏移),同基址相结合,用来确定送入内存设备的物理内存地址。

  • 物理地址:内存单元所看到的地址。
    用户程序看不到物理地址。用户只生成逻辑地址。逻辑地址与物理地址呈现一一映射的关系。

fork()会产生一个和父进程完全相同的子进程,一般情况下,子进程会调用exec函数族去执行新的程序,这个时候子进程就会有新的栈,堆,数据段,和代码段。linux系统经过不断发展,从效率的角度出发,创造了一个写时复制技术。linux中引入了“写时复制“技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

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

  • 就以上面两个例子分析一下: 第一个例子,子进程访问了num的值而却没有改变num的值,只是相当于读取了一下,这个时候内核不会给子进程分配新的资源,而是接着共享父进程的资源,这个时候num的值还是5,这样就节省了系统调用时候的资源。
    第二个例子,子进程在执行过程中改变了num的值,这个时候系统发现了这个操作,就会给子进程分配新(新指物理地址上的新)的栈,堆和数据段,并且将父进程中fork之前的变量拷贝一份(拷贝的值和父进程中的值就没有任何关系了),然后子进程再对这些值进行操作,所以二者虽然用的是相同的变量名,但是在物理地址上已经完全不相同了。
    写时复制技术大大降低了进程对资源的浪费。

  • 写时复制在linux中具体是怎么实现的呢?
    fork之后,linux为子进程复制一份区链表(vm_area_struct),但是让父子进程指向相同的页表(相当于指向同一块物理内存)。区域被标记为可读/可写,但是页面自己却标记为只读。如果任何一个进程试图写页面,就会产生一个故障,此时内核发现该内存区逻辑上是可写的,但是页面却是不可写入的,因此它把该页面的一个副本给当前进程同时标记为可读可写。

再看fork函数

当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct结构,区域结构和页表的原本副样。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有写时复制
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新的页面,因此,也就为每个进程保持了私有地址的概念。

Logo

更多推荐