内存管理
内存管理在 OS 中是一项重要功能,这不仅是能给每个用户程序,设备驱动合理分配内存,也关乎 OS 是否能保障内核内存的安全。
在 32 位模式下,OS 有两种内存管理方式
- 分页式内存管理
- 分段式内存管理
在 64 位长模式下,分段式内存管理被废除,分页式内存管理是强制性的
分段式内存管理
此种内存管理非常落后,且无法整理碎片化的内存。故本教程不作详细代码指导,仅简单介绍一下其功能。
分段式内存管理将程序的地址空间划分为逻辑上的段,每个段可以有自己的访问权限和保护机制。 与分页不同,分段允许变量大小的内存分配,这更符合程序的自然结构。 分段的一个主要优点是它能够提供更强的隔离性和安全性,因为每个段可以被赋予独立的访问但是,分段可能会导致内存碎片和复杂的地址转换过程。
全局描述符表 (Global Description Table)
在保护模式下通过段寄存器和段偏移量寻址,但是此时段寄存器保存的数据意义与实模式不同。 此时的 CS 和 SS 寄存器后 13 位相当于 GDT 表中某个描述符的索引,即段选择子。第 2 位存储了 TI 值 (0 代表 GDT,1 代表 LDT),第 0、1 位存储了当前的特权级 (CPL)。
GDT 段描述符结构
一个 GDT 段描述符占用 8 个字节,包含三个部分:
- 段基址 (32 位),占据描述符的第 16~39 位和第 55 位~63 位,前者存储低 16 位,后者存储高 16 位
- 段界限 (20 位),占据描述符的第 0~15 位和第 48~51 位,前者存储低 16 位,后者存储高 4 位。
- 段属性 (12 位),占据描述符的第 39~47 位和第 49~55 位,段属性可以细分为 8 种:TYPE 属性、S 属性、DPL 属性、P 属性、AVL 属性、L 属性、D/B 属性和 G 属性。
段寄存器
使用 lgdt
指令加载 gdt 后,一般情况下您还需要初始化段寄存器
ds
fs
gs
es
ss
段寄存器需要初始化为0x10
cs
段寄存器无法直接初始化,您需要段跳转等方式
分页式内存管理
在分页系统下,一个程序发出的虚拟地址由两部分组成:页面号和页内偏移值。 对于 32 位寻址的系统,如果页面大小为 4KB,则页面号占 20 位,页内偏移值占 12 位。 而这个翻译过程由内存管理单元 (MMU) 完成,MMU 根据 CPU 中 CR3 寄存器存储的页目录基址来寻找页表,进而将线性地址翻译成物理地址再去寻址。 内存管理单元按照该物理地址进行相应访问后读出或写入相关数据。
以下虚拟地址 (也叫线性地址) 的构成如图所示
页表
页表的根本功能是提供从虚拟页面到物理页框的映射。 因此,页表的记录条数与虚拟页面数相同。 MMU 依赖于页表来进行一切与页面有关的管理活动,这些活动包括判断某一页面号是否在内存里,页面是否受到保护,页面是否非法空间等等。 由于页表的特殊地位,决定了它是由硬件直接提供支持,即页表是一个硬件数据结构。
所以,页表整体必须在物理地址上具有结构有效性 (也就是页表所在的区域其线性地址与物理地址必须是一一对应的) 否则,MMU 会发生严重的错误导致计算机系统直接重启,或者出现缺页情况 (某一线性地址无法映射到指定的物理地址)
恒等映射:指物理地址与虚拟地址以一一对应或固定偏移的方式进行映射,常用于设备 MMIO 以及操作系统用内存
物理内存管理
为了方便管理,我们将物理内存分成以 4kb 为大小的一个一个页框,这些页框可以被页映射。
物理内存管理没有过多的复杂逻辑结构,你只需要能合理分配即可 (常用方法是在页框头打上标记以区分该页框是否被占用)
页框不止可以以 4kb 为大小,你也可以设置为 4MB、1GB 等大小。但是那样的页太大了,一般用于 64 位更大的内存上管理 现在首要任务是确保我们所写的分页式内存管理可用
堆内存管理
堆内存是基于每个进程的专属区域内存管理方式,是一种软件实现的内存管理方式。
堆内存管理会向用户程序提供两个函数,用户程序将使用这两种函数合理安排自己所使用的内存。
void* malloc(size_t size);
向 OS 申请分配一段指定大小的内存void free(void* ptr);
释放一块已分配的内存