地址空间的整体
接下来我们用代码验证存储位置。
#include<iostream>
#include<stdlib.h>
using namespace std;
int g_nval;
int g_val=10;
int main()
{
cout<<"code add:"<<&main<<endl;
cout<<"nval add:"<<&g_nval<<endl;
cout<<"val add:"<<&g_val<<endl;
char *heap=(char*)malloc(10);
cout<<"heap add:"<<(void*)head<<endl;
cout<<"stack: "<<&head<<endl;
return 0;
}
命令行参数,以及环境变量。以及他们指针都存在命令行环境变量。
我们发现一个神奇的现象
不同的值可以对应相同的地址。这是怎么做到的呢?
其实,上面所说的地址并不是真正的地址,而是虚拟地址。
进程地址空间
进程地址空间是一个结构体,通过进程控制块控制内存。每当你想使用内存时,会先映射一个进程地址空间。这样虚拟地址和实际地址分开。每个进程被创建时,其对应的进程控制块(task_struct)和进程地址空间(mm_struct)也会随之被创建。而操作系统可以通过进程的task_struct找到其mm_struct,因为task_struct当中有一个结构体指针存储的是mm_struct的地址。
子进程被创建时,子进程和父进程拥有一样的代码块。但是他们可以通过页表映射不同的物理地址。
进程空间的写时拷贝
写时拷贝是节约内存的一种方法,所谓写时拷贝,就是你需要我才给你开空间,并不是你malloc多少,我就给你开到内存开多少。而是真正有需要,我才给你开辟那么多的空间。
进程控制块
再谈到进程控制块时, 现在我们知道了,进程地址空间,进程控制块都是结构体,通过指针关联了进程地址空间
当子进程刚刚被创建时,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间。只有当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,然后再进行修改。
例如,子进程需要将全局变量g_val改为200,那么此时就在内存的某处存储g_val的新值,并且改变子进程当中g_val的虚拟地址通过页表映射后得到的物理地址即可。
子进程和父进程在页表中虽然有相同的虚拟地址,但是映射关系不一样,则子进程和父进程就可以映射不同的地址。