first commit

This commit is contained in:
张乾
2024-10-16 10:18:29 +08:00
parent 9f7e624377
commit 4d66554867
131 changed files with 27252 additions and 0 deletions

View File

@@ -0,0 +1,236 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
24 虚实结合:虚拟内存和物理内存
你好我是LMOS。
上一课中学习了内存地址空间,我们搞清楚了内存地址与地址空间的本质。
今天我们开始学习虚拟内存与物理内存。其实虚拟内存也好物理内存也罢我们从储存并索引数据的角度来看内存的重要组成部分就两个一个是地址另一个就是储存字节单元即能存放8个二进制位的容器。把两者合起来我们可以将内存理解为能索引到具体储存字节单元的地址集合。
这节课我会带你解决以下三个问题:
虚拟内存的本质是什么?-
物理内存是什么,它的结构长什么样?-
虚拟内存如何与物理内存结合在一起,真正实现储存数据的功能?
课程配套代码你可以从这里下载。让我们带着上面的问题,正式开始今天的探索之旅吧!
虚拟内存
上节课我们了解了内存地址的产生方式,以及应用程序的链接过程,也知道了内存就是能索引到具体储存单元的地址集合。但是程序中的地址能否索引到具体储存单元呢?具体的储存单元,又是如何分配的呢?下面我们用两个问题来说明其中的原理。
第一个问题
我的第一个问题来了,应用程序中使用的地址是什么内存地址?是不是感觉情况有很多种,一时很难回答清楚?遇到这种状况不要慌,我们只要动手写一个简单的程序就可以验证。
好,我们立刻动手写一写,代码如下:
#include "stdio.h"
#include "stdlib.h"
void func_a()
{
//定义地址0x40000000
int* p = (int*)0x40000000;
printf("内存地址:%p\n", p);
//向该地址写入数据
*p = 0xABABBABA;
printf("内存地址:%p处的值:%x\n", p, *p);
return;
}
int main()
{
func_a();
return 0;
}
上述应用程序非常简单我们在main函数中调用函数func_a而在函数func_a中我们定义一个整型指针C语言中指针就是内存地址其地址值为0x40000000。
代码我给你存到了课程相关的工程目录中你可以打开工程目录make一下就会自动编译好。然后你需要在终端下运行这个main.elf程序首先会出现“内存地址0x40000000”接着会出现“段错误程序异常退出”的提示。
出现了段错误提示,在你的预料之中么?我来解释一下,为什么会出现这种情况,这是因为我们使用了一个没有分配的地址。很显然,如果一个地址真的能索引到内存,该地址就能访问内存,除非这地址是个假地址,在内部需要某种机制进行转换才能访问内存。这个转换机制可能需要一些表或者数据结构进行控制,并且这个控制权掌握在操作系统的手里。
由于操作系统管理内存的规则,是先分配后使用,所以,我们就猜想操作系统分配内存的时候,就会处理控制地址转换的相关表和数据结构。接下来我们写段代码,来验证一下猜想,如下所示:
#include "stdio.h"
#include "stdlib.h"
void func_b()
{
//分配内存,返回其地址
int* p = (int*)malloc(sizeof(int));
if(p)
{
printf("内存地址:%p\n", p);
//向该地址写入数据
*p = 0xABABBABA;
printf("内存地址:%p处的值:%x\n", p, *p);
}
return;
}
int main()
{
func_b();
return 0;
}
这次我们编译运行,就会正确地输出结果了。
其实malloc函数在内部最终会调用Linux内核的API函数在该进程的虚拟地址空间中分配一小块虚拟内存返回其首地址。这个过程我用一幅图来为你展示如下所示
由于代码优化的原因malloc函数并不是每次调用都会导致Linux内核建立一个vm_area_struct数据结构。我们假定malloc函数导致Linux内核建立了一个vm_area_struct数据结构该结构中有描述虚拟内存的开始地址、大小、属性等相关字段表示已经分配的虚拟内存空间。
许多个这样的结构可以一起表示进程的虚拟地址空间分配情况。但是这个从vm_area_struct数据结构中返回的地址仍然是虚拟的、是假的是不能索引到内存单元的直到访问该地址时会发生另一个故事如下图所示
上图中CPU拿着一个虚拟地址访问内存首先会经过MMU对于调用malloc函数的情况是该虚拟地址没有映射到物理内存所以会通知CPU该地址禁止访问。
上图中1到4个步骤为硬件自动完成的然后CPU中断到Linux内核地址错误处理程序软件开始工作也就是说Linux内核会对照着当前进程的虚拟地址空间去查找对应的vm_area_struct数据结构找不到就证明虚拟地址未分配直接结束进程会发出段错误若是找到了则证明虚拟地址已经分配接着会分配物理内存建立虚拟地址到物理地址的映射关系接着程序就可以继续运行了。
当然了实际情况比图中的复杂这里我们只是要理清楚malloc函数的逻辑并且明确malloc是返回的虚拟内存地址就可以了。
第二个问题
我们要想清楚的第二个问题就是直接使用物理内存地址会出现什么后果我们来看一个程序下面这段代码是一个简单版的memset函数。
void mymemset(void* start, char val, int size)
{
char* buf = (char*)start;
for(int i = 0; i < size; i++)
{
buf[i] = val;
}
return;
}
我们提出一个假设这个函数被不同的应用程序调用且使用的地址就是物理地址能直接访问物理内存单元
你可以想一想如果假设成立恶果就是一个程序可以改变另一个程序的内存甚至是全部的内存想想吧这是何等可怕通过这个例子我们发现物理地址不能有效地隔离内存达到保护内存的结果
想要隔离内存就需要依赖虚拟内存这个东西我画了一幅图带你总结一下虚拟内存的本质如下所示
由上图可知我们各种应用都可以拥有从0到最大虚拟地址的完整的虚拟内存空间并且可以任意使用这个虚拟内存空间每个应用都认为自己拥有整个内存这一点可以从所有的应用程序使用相同的链接脚本进行链接得到佐证各个应用程序调用malloc函数可能得到相同地址是另一个佐证
我们现在终于知道了虚拟地址真的只是一个整数一系列的这种整数集合就构成了虚拟内存空间这个整数能索引一个字节的虚拟内存单元但这个虚拟内存单元不会对应到真正的物理设备因此它虽然可以独立存在但却需要下层的物理内存作为支撑才能实现访问和储存数据
物理内存
上一课中我们了解到物理地址空间是CPU地址线位宽所能表示最大整数集合只是一个地址它能索引物理设备或者什么都不索引这里的物理设备中就包括了物理内存
下面我们来看看真实的内存长什么样如下所示
从上图可以看到 PCB 板上有内存颗粒芯片主要是用来存放数据的SPD 芯片用于存放内存自身的容量频率厂商等信息还有最显眼的金手指用于连接数据总线和地址总线电源等
其实内存应该叫 DRAM即动态随机存储器内存储存颗粒芯片中的存储单元是由电容和相关元件做成的电容存储电荷的多少代表数字信号 0 1而随着时间的流逝电容存在漏电现象就会引起电荷不足的情况导致存储单元的数据出错所以DRAM 需要周期性刷新以保持电荷状态
DRAM 结构比较简单且集成度很高通常用于制造内存条中的储存颗粒芯片我们无需过多关注内存硬件层面的技术规格标准这里重点需要关注的是逻辑上内存和硬件系统的连接方式和结构
我还是画幅图来说明吧这样方便你建立直观印象如下图所示
我们假定从物理地址0开始索引的是物理内存CPU发出的地址是虚拟地址经由MMU转换变成物理地址物理地址经由地址译码单元就会对应到具体的内存字节储存单元一个字节单元能储存8个二进制位即一个地址能对应到8个二进制位
你可以通过dmsg命令查看你物理机上的情况在我的x86机器里情况如下图所示
从图里我们可以看到usable类型的物理地址区间对应的是DRAM即内存其它的则是保留的或者硬件设备的地址空间这些空间程序是不能当作内存来使用的
讲到这里我们就明白了逻辑上物理内存相当于几个地址上不连续的字节数组始终有一个物理地址能索引到其中一个字节
虚实结合
提出虚拟内存这个概念一是为了让应用认为自己享有完整的地址空间拥有整个内存的使用权二是要对物理内存进行保护即使各个应用程序都存放在物理内存之中也不能随意访问自己的物理内存更不能侵犯别的应用程序所占用的物理内存不然就会出现互相改写对方内存的情况一旦出现这样的情况后果就严重了任何应用程序都不能正常运行了
那接下来要考虑的问题就是虚拟内存跟物理内存要如何对应起来
虚拟内存必须要落实到物理内存才能真正完成工作最简单的方案是让虚拟地址能够索引到物理内存单元但是虚拟地址和物理地址显然不能一一对应如果那样的话虚拟地址等于物理地址且不受控制这样虚拟地址就没有任何意义了
因此我们需要在虚拟地址空间与物理地址空间之间加一个机构这个机构相当于一个函数p=f(v) 对这函数传入一个虚拟地址它就能返回一个物理地址该函数有自己的计算方法对于没法计算的地址或者没有权限的地址还能返回一个禁止访问
这个函数用硬件实现出来就是CPU中的MMU即内存管理单元CPU发出的虚拟地址首先经过MMUMMU内部计算得出物理地址最后用物理地址去访问内存MMU的结构如下图所示
上图中展示了CPU发出的虚拟地址经过MMU转换出物理地址进而访问内存的过程但我们并没有弄清楚MMU是使用什么方法进行转换的所以下面我们继续探讨MMU的地址转换过程
你不妨想一想把一个数据转换成另一个数据最简单的方案是什么当然是建立一个对应表格对照表格进行查询就行了MMU也是使用一个地址转换表但是它做很多优化和折中处理不做任何折中处理的话这种方案是无法实施的
你可以想象一下32位的地址空间有4G个虚拟地址和4G个物理地址在这种情况下每8个字节存放两个地址数据想要装下所有的地址这个表有多大应该放在哪里查询代价有多大所以这个方案直接pass掉
我们现在来看看通常情况下MMU是如何解决这个问题的一共有三个关键环节
首先MMU对虚拟地址空间和物理地址空间进行分页处理一个页大小可以是4KB16KB2MB4MB1GB不等这是为了增加地址的粒度避免采用每个字节一个地址现在一页一个地址地址数量就会大大减少从而减少转换表的大小
其次MMU采用的转换表也称为页表其中只会对应物理页地址不会储存虚拟地址而是将虚拟地址作为页表索引这进一步缩小了页表的大小
最后MMU对页表本身进行了拆分变成了多级页表假如不分级4GB内存空间 按照4KB大小分页有1M个页表项每个页表项只占用4个字节也需要4MB空间如果页表分级在建立页表时就可以按需建立页表而不是一次建立4MB大小的页表
我们一起来画一幅图来描述一下这个过程如下所示
对照图片我们可以看到虚拟内存页和物理内存页是同等大小的都为4KB各级页表占用的空间也是一个页即为4KBMMU把虚拟地址分为5个位段各位段的位数根据实际情况有所不同按照这些位段的数据来索引各级页表中的项一级一级往下查找直到页表项最后用页表项中的地址加页内偏移就得到了物理地址
我再画一幅图为你描述这一过程
看到这幅图我们就清楚了MMU用虚拟地址转换物理地址的过程如果转换成功就可以直接访问内存了但如果转换失败MMU就会通知CPU地址转换失败让CPU产生一个异常中断进而通知操作系统内核让操作系统内核来处理这个异常就像malloc分配内存的过程那样
我们已经知道了虚拟地址如何转换成物理地址但是如果只是按部就班地转换可不行别忘了还需要对物理内存进行保护这个保护物理内存的问题的关键就是想清楚一个虚拟地址在什么情况下能被转换成物理地址
这就要说到MMU是如何控制转换动作的要进行控制就需要相关的控制信息聪明如你大概已经猜到了控制信息就放在页表项中MMU在转换过程中首先就会查看那些信息以此作出判断
下面我们看一下控制信息的格式如下所示
从上图中可以看到页表项中的低12位为属性位段这里保存一个物理内存页面的读写执行存在的相关权限还有页面是否存在可不可以缓存是否已经访问或者写入大小等信息这些信息统统编码在12个二进制位中
为什么表示各种页面地址的页表项能让出12位用于编码这些信息呢这是因为一个页面最小也是4KB且与4KB对齐那么页面开始地址的低12位永远为0所以可以挪为它用
到这里我们就已经搞清楚虚拟地址如何转换成物理地址并且知道了MMU如何控制转换过程恭喜你解锁了虚实结合的思路和过程
现在你可能隐约感觉到只要操作系统牢牢控制页表数据就能实现对内存的完全控制和保护使得各个应用程序在自己的虚拟地址空间中安全地运行不被打扰也不能打扰别人每个应用程序都有相同的虚拟内存但却占用着不同的物理内存
重点回顾
今天的课程就要结束了下面我们来回顾一下今天的内容
首先我们从两个实际问题出发研究了虚拟内存的本质虚拟内存的应用一是为了保护内存二是为了限制访问内存让应用程序拥有独立的地址空间误以为自己能享用全部的内存
接着我们分析了物理内存了解了DRAM的特性和结构因为DRAM就是我们常说的内存设备这里你重点要关注的是内存的逻辑结构和系统连接方式
最后我们讨论了虚实结合究竟是怎么实现的硬件工程设计了MMU让它把虚拟内存地址通过页表中的信息转换成物理地址并控制转换过程如果转换失败就会通知CPU然后CPU产生地址异常中断最后由操作系统处理这个异常操作系统将会通过修改页表的数据来修复这个问题进而完全控制内存的访问
我画了一张导图梳理这节课内容供你参考
应用程序的虚拟地址空间里还有更多奥秘我会在下节课继续为你展开敬请期待
思考题
请问页表数据究竟放在什么地方呢
欢迎你在留言区跟我交流互动说说你对虚实结合的认识如果觉得这节课还不错也推荐你把它分享给身边的朋友