目录
1.回顾C/C++程序地址空间
这是我们以前理解的内存布局,那么这是真正的内存吗?不是!
先来验证一下:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int g_unval;
int g_val=10;
int main()
{
const char*s = "hello wrold";
int a = 10;
int*arr = (int*)malloc(4);
printf("code addr: %p\n",main);
printf("string rdonly addr: %p\n",s);
printf("uninit addr: %p\n",&g_unval);
printf("init addr: %p\n",&g_val);
printf("heap addr: %p\n",arr);
printf("stack addr: %p\n",&a);
return 0;
}
可以看到地址确实是按照图中的顺序依次递增的。
再看下面的代码:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int g_val = 100; // 父子进程的数据是私有的,采用写时拷贝
int main()
{
if(fork()==0)
{
int cnt=5;
while(cnt)
{
printf("I am child, times: %d, g_val = %d,&g_val = %p\n", cnt, g_val, &g_val);
sleep(1);
g_val = 200; // 执行依次后将全局变量g_val改为200
cnt--;
}
}
else
{
while(1)
{
printf("I am father, g_val = %d,&g_val = %p\n", g_val, &g_val); // 父进程的g_val该是多少?
sleep(1);
}
}
return 0;
}
可以看到,父进程和子进程中的g_val的地址是一摸一样的,那么按理说将子进程中的g_val改变后,由于他们使用的是一块空间,所以父进程中的g_val的值也应该改变,可这里为什么没有变化??
如果C/C++打印出来的地址是物理内存的地址,这种现象绝不可能存在!而这里使用的地址是虚拟地址。
在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理
所以最上面那张图应该叫做,进程虚拟地址空间。
2. 进程地址空间是什么
每个进程都有一个地址空间,都认为自己在独占物理内存。而这个地址空间在内核中是一个结构体 struct mm_struct.
mm_struct 中的分布类似下面这种:
struct mm_struct {
unsigned int code_start; //地址空间上进行区域划分时,对应的线性位置,称为虚拟地址
unsigned int code_end;
unsigned int init_data_start;
unsigned int init_data_end;
unsigned int uninit_data_start;
unsigned int uninit_data_end ;
unsigned int heap_start;
unsigned int heap_end;
unsigned int stack_start;
unsigned int stack end;
}
虽然这里只有start和end,但每个进程都可以认为mm_struct代表整个内存的所有的地址为0x0000...000~0xFFFF...FFF(即每个进程都认为自己拥有4GB的空间,至于到底有没有,是OS要做的事)
真实的内存关系应该是这样的:
页表:是一种特殊的数据结构,放在系统空间的页表区,存放逻辑页与物理页帧的对应关系。 每一个进程都拥有一个自己的页表,PCB表中有指针指向页表。
3.为什么要有进程地址空间(★)
1.防止访问权限越界
通过添加一层软件曾,完成有效的对进程操作内存进行风险管理(权限管理),本质目的是为了保护物理内存以及各个进程的数据安全。
例如:如果进程直接操作物理内存,那么如果进程A去访问了进程B的空间怎么办?如果进程A中的某些内容不能被修改,而被修改了怎么办?
在物理地址和进程之间就有了虚拟地址和页表,对内存进行了更好的管理。
2.将内存申请和内存使用的概念划分清楚
通过虚拟地址空间,来屏蔽底层申请内存的过程,达到进程读写内存和OS申请内存管理操作,进行软件上面的分离。
例如:
进程A想申请1000字节空间,进程A马上就能使用这1000字节吗?这是不一定的,可能会存在暂时不会全部使用的情况。
在OS角度,如果空间马上给进程A,就意味着整个系统会有一部分空间本来可以给其他进程立即使用,先在却被进程A闲置着。
这样就会存在空间浪费的情况。
所以在这种情况下,OS会在进程A使用空间的时候才将内存申请给进程A。(相当于是类似写时拷贝的思想)
3.站在CPU和应用层的角度,进程同意可以看作统一使用4GB空间,而且每个空间区域的相对位置是比较确定的。
例如:
如果同时存在多个进程,而每个进程代码的其实位置是不确定的,那么CPU在执行时,需要找到代码在哪里,比较混乱。
而使用虚拟地址空间和页表的方式,将内存划分为代码段、常量区、堆、栈等区域,CPU执行进程时,每次从同一个位置开始即可,而不同的进程通过不同的页表映射到自己的物理内存中存放代码和数据的位置,提高了CPU的执行效率。
所以通过虚拟地址和页表,程序的代码和数据可以被加载到物理内存的任意位置!!极大的减少内存管理的负担。
OS最终这样的目的,为了达到一个目标:每个进程都认为自己是独占系统资源的。
4.回顾
还记得上面的父子进程中子进程将g_val值改变后,父进程没变的例子吗,现在就可以很好的解释了:
子进程在创建时会以父进程为模板,即能够拷贝父进程的地方就拷贝,例如虚拟地址,只读区的映射关系(代码共享)。
所以子进程和父进程的虚拟地址是相同的,而页表的映射关系是不同的,所以他们的物理地址也不同。
所以就出现了,子进程改变g_val的值,而父进程不变,但打印出的地址却是一样的情况了。