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之前的变量拷贝一份(拷贝的值和父进程中的值就没有任何关系了),然后子进程再对这些值进行操作,所以二者虽然用的是相同的变量名,但是在物理地址上已经完全不相同了。 -
写时复制技术大大降低了进程对资源的浪费。
再看fork函数
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct结构,区域结构和页表的原本副样。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新的页面,因此,也就为每个进程保持了私有地址的概念。