目录
引入
进程地址空间
虚拟地址与物理地址
如何理解虚拟地址的不同区域
写时拷贝
动态开辟的细节
为什么存在进程地址空间
避免地址被随意访问
进程管理和内存管理解耦合
使进程用统一的视角看待代码和数据
引入
🎃我们写一个这样的程序,运行并观察其输出结果。
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <cerrno>
#include <cstring>
using namespace std;
int main()
{
int value = 10; //定义一个变量
pid_t id = fork();
if (id == 0) //子进程
{
while(1)
{
value++; //改变value的值
printf("我是子进程,我的pid是: %d,value是: %d,&value是: %p\n",getpid(),value,&value); //输出变量值
sleep(1);
}
}
else if (id > 0) //父进程
{
while(1)
{
printf("我是父进程,我的pid是: %d,value是: %d,&value是: %p\n",getpid(),value,&value); //输出变量值
sleep(1);
}
}
else //fork出错
{
cout << errno << ": " << strerror(errno) << endl;
}
return 0;
}
🎃可以观察到,父子进程都有一个 value 值,且共用一个地址。而子进程改变 value 时父进程的 value 却不受影响。
🎃假设,我们使用的这个地址就是物理地址的话,可能会出现读取同一个地址而出现两个不同的值的情况吗?
🎃因此我们得出一个结论: 在语言层面用的地址并非物理地址,而是虚拟地址。
🎃我们在用C/C++语言所看到的地址,全部都是虚拟地址!而用户一概看不到物理地址,而是由OS统一管理。
🎃我们使用的这个虚拟地址总的叫进程地址空间又叫 mm_struct ,是一种线性的结构,其通过页表的某种映射关系,从而找到物理内存。
进程地址空间
讲个故事吧:有个大富翁,他有很多的私生子。每个私生子不知道彼此的存在,因此都觉得大富翁的所有财产本质上也是自己的。因此,需要用钱的之后,只要找大富翁要就可以了,但也不能一下子要太多,否则大富翁会拒绝这种无礼的请求。而大富翁为了避免出现忘记给儿子们画的饼的尴尬情况,因此需要将他曾经画过的饼都管理起来。
🎃对号入座之后我们便能够发现,大富翁就是OS,私生子就是一个个进程。而其画的饼其实就是进程地址空间,本质上就是一个内核数据结构,struct mm_struct 。
虚拟地址与物理地址
如何理解虚拟地址的不同区域
🎃由于mm_struct本质上是一个线性结构,因此只要对线性区域进行指定 start 和 end 即可完成区域的划分,因此 mm_struct 内都是一段一段区域的划分,区域之间就叫做虚拟地址或线性地址。将来只需要修改区域边界的位置便可以修改区域的大小。
🎃之后 mm_struct 便可以借助页表与MMU(内存管理单元)与物理内存确立映射关系、建立联系。
🎃不仅如此,页表之中还存储了对于该空间的权限,正如一个常量区之中的字符串,我们可以对其读取却无法更改其内容。便是因为在页表之中,我们对该空间只有读权限而没有写权限。因此无法进行写入。
写时拷贝
🎃这时我们就可以回过头来讲讲,如何做到用一个地址却能得到两个值?
🎃在父进程创建子进程之前,申请了一个变量 value,因此在进程地址空间中存在一个地址,能够通过映射找到物理内存中的 value。
🎃之后父进程创建了子进程,子进程会继承父进程的进程地址空间,因此二者存储 value 的虚拟地址是相同的,并映射到同一块物理内存。若子进程未进行写入,则两个进程便会保持原有的映射关系。
🎃若子进程对值进行修改,便会触发写时拷贝,在物理内存的新地址中拷贝一份新的值进行修改,之后修改页表的映射,指向这块区域。
🎃值得注意的是,哪个进程先进行修改,哪个进程就触发写时拷贝。
动态开辟的细节
🎃不知道是否想过一个问题,动态开辟的空间是一申请就给我们呢?还是使用的时候再给?
🎃在动态开辟在申请时到使用前有个空窗期,这段时间内空间就被闲置了,而 OS 一般不允许任何的浪费和不高效的操作。
🎃因此动态开辟时值分配虚拟内存,当要使用该空间时再分配物理内存,完善页表之中的映射关系。
为什么存在进程地址空间
避免地址被随意访问
🎃若直接使用物理内存,则在访问空间时无法检测野指针问题,放任指针随意地修改,可能会使数据受损或导致其他进程崩溃出错。
进程管理和内存管理解耦合
🎃如此使用进程地址空间后,管理进程时并不关心内存是如何管理的。
🎃同理,管理内存是也不关心进程是如何管理的,实现了进程管理和内存管理的解耦合。再使用页表将二者关联起来。
使进程用统一的视角看待代码和数据
🎃原代码被编译的时候就是按照虚拟地址空间的方式,对代码和数据完成了对应的编制。
🎃这是由于虚拟地址这样的策略并不只影响 OS,编译器也要遵守。
🎃因此在内存中打开文件时,自然就有了对应的物理地址,之后在运行的时候若对函数进行调用,通过映射就会找到函数的虚拟地址(当前物理地址中存的是虚拟地址),再通过映射便可以找到函数体(再通过映射找到存函数体的物理地址)。
🎃因此 cpu 中读取的都是虚拟地址。 ---这样便实现使进程用统一的视角看待代码和数据。
🎃且进程的代码和数据并非一直都在内存中,而是用多少加载多少,不再使用就将其从内存之中移除
🎃好了,今天进程地址空间的讲解到这里就结束了,如果这篇文章对你有用的话还请留下你的三连加关注