背景 : 在此文章里会从分页分段机制去解析 Linux 内存管理系统如何工作的, 由于 Linux 内存管理过于复杂而本人能力有限. 会尽量将自己总结归纳的部分写清晰.
从实模式到保护模式的寻址方式的不同 :
16 位 CPU 的寻址方式 : 在 8086 CPU 中, 提供了两类寄存器来进行寻址, 分别为段寄存器 (例如 CS,DS,SS) 和段偏移寄存器(例如 SI,DI,SP). 而这几种寄存器的长度都为 16bit, 寻址方式也很简单 : cs:ip = (cs << 4 + ip). 也就是说 cs 寄存器的值左移 4 位加上 ip 的值得到的就是物理地址(物理地址就是内存中真实的值).
32 位 CPU 的寻址方式 : 在 80X86 CPU 中, 提供了分段与分页机制. 对于 CPU 的寻址而言, 不再像 8086 CPU 那般将 段寄存器段偏移寄存器 直接运算得到结果.
1)那么在 32 位 CPU 中是如何寻址的呢?
i)如何开启分页分段模式?
首先要介绍的就是 CR0 寄存器(如下图):
对于 CR0 来说, 存在两个 bit :
PE 位 : 如若置位 (1) 则表示开启保护 (分段) 模式.
PG 位 : 在 PE 位置位的前提下置位 PG 位表示开启分页模式.
ii)分段机制如何进行寻址(得到线性地址)?
简述 : 段寄存器(例如 CS) 里面存在一个索引(index), 它会根据 GDTR 寄存器找到一个表(GDT), 然后这个表里面有元素, 元素内部含有段基址. 而这个段基址加上段变址寄存器的值就直接得到了线性地址的值.
详述 :
在开启分段模式之后, 段寄存器里面的值的含义就不再只是一个简单段基址了 (也就是 (cs << 4) 得到段基址), 当下段寄存器加载的值称为段选择子, 结构如下:
可以看到这里有一个由几个 bit 组成的 描述符索引(也就是简述里所说的 index), 以及 TI 和 RPL 位(但目前不用管它).
GDTR :
可以看到 GDTR 和 IDTR(这个其实是另一个类似于 GDTR 的寄存器)都是由线性基地址和表长度组成, 线性基地址也就是说这个表的头部所在的线性基地址(类似于数组名), 表长度也就是这个表的长度啦.
那么自然我们就能得到一个类似于数组 (由连续的地址组成) 的表.
对于这个 "数组" 来说, 它的元素则被称为 段描述符:
可以看到段描述符很长(一共 64bit)... 但是没关系, 我们当下只需要把其分为三个部分 : 段基地址, 段限长, 段属性. 即可.(这里之所以基地址和段限长啥的分了几个部分主要是因为历史遗留问题, 但是没关系, 他们只不过需要把几个分开的连在一起就能得到了真正的段基地址了).
那么得到了段基址, 我们自然将其与段变址寄存器内的值相加就得到了线性地址了!
iii)分页机制如何进行寻址(得到物理地址)?
如若我们开始分页了, 那么就表示我们已经得到了一个线性地址(分页是在分段的基础上进行的).
简述 : 首先我们把线性地址分为几个部分, 目录(本质是页目录表的索引), 页面(本质是页表的索引), 页内偏移(本质是偏移量).
由 CR3 寄存器 作为 页目录表 的指针, 通过 CR3 寄存器就可以得到一个表称为页目录表, 页目录表内元素 称为 页目录项, 页目录项本质也是一个指针, 指向一个 页表, 而页表内元素称为页表项, 页表项内存在着 页基地址, 物理地址 = 页基地址(物理基地址) + 页内偏移(物理偏移地址).
简单来说我们可以把 页目录表和页表想象成一个二维的数组. 页目录表元素是页表(一维数组), 页表元素则是页基地址.
我们只需要有两个元素 (页目录表索引和页表索引) 就可以得到一个物理 (页) 基地址, 然后我们再将 页内偏移加上物理基地址, 就得到了真正的物理地址了! 而一个页在 80x86 中是 4K 大小(页基址 至 页基址 + 4K 为一页). 所以内存管理的页也是 4K 大小.
附图(寄存器数据) :
由图我门可以知道, 页基地址(页帧), 是 4K 对齐的(2^12 = 4K), 也就是说页表项内只有 12 - 31 位是页基地址, 其他的位是页属性, 每次通过页表项计算物理地址只需要将 0 - 11 位复位(0), 即可.
对于页属性 : 表述这个页的权限之类的, 因为有的页面是属于内核才能去使用的. 更重要的一点是 : 这个页是否存在.
页目录和页表的表项格式:
如图所示 : 我们可以知道当 P 位被置位则表示页面存在, 当 P 位复位(为 0) 则表示页面不存在, 如若页面不存在, 那么就会产生缺页中断, 执行缺页中断处理程序.
来源: https://www.cnblogs.com/vizdl/p/12233033.html