之前我们说这个图是程序地址空间,那它是内存吗?
答:根本不是的
它准确来说叫进程虚拟地址空间!
为了方便理解我们用一段代码来看一下
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 0;
}
else if(id == 0){ //child
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
else{ //parent
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
结果:
parent[1234]: 0 : 0x1234567
child[1235]: 0 : 0x1234567
当把代码修改以下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 0;
}
else if(id == 0){ //child
g_val=100;
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
else{ //parent
sleep(3);
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
结果:
parent[2345]: 0 : 0x1234567
child[2356]: 100 : 0x1234567
我们有个惊奇的发现!
父子g_val的不相同,但是地址竟然一样!很明显这已经是两个变量了,按道理应该是两个不同的地址,但是地址相同,说明&打印出来的地址根本不是物理地址,而是虚拟地址!
物理地址,用户一概看不到,由OS统一管理。
OS必须负责将 虚拟地址 转化成 物理地址 。
进程地址空间
我们在语言层面遇到的地址都是虚拟地址!
每个进程都有一个地址空间,都认为自己独占物理内存。
进程地址空间跟进程一样都得被数据化,也是先描述再组织,所以要描述地址空间,就要有一个结构体数据结构类型。(struct mm_struct)。
每个进程都认为地址空间划分是按照4GB空间划分的,都认为自己独有这4GB。
所有虚拟地址是地址空间上进行区域划分时,对应的线性位置。
虚拟地址到物理地址
为什么不让PCB直接去访问物理地址呢?
答:task_struct如果可以直接访问物理地址,有没有想过,物理地址有很多进程,如果你不小心寻址错误,访问到了其他进程,而这个进程是转账类似的,那是不是很危险?而如果我们有一层中间层,不允许你直接访问物理地址,而是在这之间对你的请求进行检查,如果合法给你映射过去,不合法就中止你的请求。
就像const char* s=“hello world”
你想*s=‘H’,这样是不允许的,当页表识别出你是字符常量区的,它映射时就不会给你w的权限,本质上就是OS给你的权限只有r权限。
而这就是说为什么要有地址空间?
⭐
第一个原因:
通过添加一层软件层,完成有效的对进程操作内存进行风险管理(权限管理),本质目的是为了保护物理内存以及各个进程的数据安全。
再者说:
如果我想申请1000字节空间,我们立马就可以得到吗?
不一定,可能我们只是单纯告诉OS我要申请空间,以后我要用,但是现在不一定用。那在OS角度,如果空间给你,你又不立马用,或者用不完,那岂不是这部分空间就闲置着?别人想用也用不了。所以地址空间的第二个作用就来了!
这叫基于缺页中断进行物理内存申请!
⭐
第二个原因:
将内存申请和内存使用的概念在时间上划分清楚,通过虚拟地址空间来屏蔽底层申请内存的过程,达到进程读写内存和OS进行内存管理操作进行软件上面的分离。
你别管我最后咋给你在物理内存上开辟,反正给你开了就行,如果当你要的时候物理内存不够了,我就进行内存管理算法给你腾出地方。
再者说最后一个原因:
cpu说:我想知道我该运行哪个进程了,这个进程的开始地址是啥。
不同进程的main()地址也不同,cpu还得自己找,所以cpu不乐意了,我就在0x00000000位置拿进程地址,我不找了,你想让我运行哪个,你给我送到这。
所以进程地址空间就一直在0x00000000处存进程地址,在页表建立映射关系。
⭐
第三个原因:
站在cpu和应用层角度,进程统一可以看作,它们各自独有4GB虚拟空间,而且这个空间区域相对位置是比较确定的。因为地址空间的存在,程序的代码和数据可以被加载到物理内存的任意位置,通过映射到地址空间,而地址空间的地址是连续的,大大减少了内存管理的负担。
OS最终目的就是:让每个进程都独享4G空间,这样每个进程都统一起来,方便管理。
现在我们再来看这张图:
父子进程本来是共享代码和数据的。子进程的创建是以父进程为模板的。
而进程也是独立的。
当有人要修改数据,那么为了保证进程的独立性,发生写时拷贝,在物理内存新开辟一片空间,其实在物理内存,子进程的g_val地址已经改变,但是映射到虚拟地址上,虚拟地址没有改变,只是页表到物理内存的映射关系变一下。
所以最开始的现象就解释通了。