1 分钟阅读 次阅读

概要:本文详细介绍了 i386 架构下的页式内存管理机制,包括从线性地址到物理地址的两级或三级映射过程、Linux 针对不同位宽 CPU 的分页结构适配方法,并进一步说明了用户空间与内核空间的划分原则及虚实地址之间的转换实现。

1. i386 CPU 页式存管思路

i386 CPU 页式存管通过页面目录和页面表分两个层次实现从线性地址到物理地址的映射。

  • 可以理解为一种“快递分拣系统”:地址簿分两层管理(页面目录 + 页面表)。
  • 想象你要管理一个超大仓库(内存),仓库有 400 万个储物格(4GB 空间)。直接记录每个储物格的位置太费劲,于是你:
    • 先准备一个“大区目录本”(页面目录),把仓库分成 1024 个大区。
    • 每个大区对应一个“小区明细本”(页面表),每个明细本记录 1024 个具体储物格的位置。

1.1 智能节省空间

  • 实际使用中,很多大区是空的。例如当发现第 5 大区完全空置,就直接在目录本上标注“第 5 大区不存在”。
  • 省去了维护这个空大区的明细本(相当于省了 1024 条记录的空间)。
  • 类比:就像快递公司不会为无人居住的片区准备快递柜。

1.2 32 位地址的黄金组合

  • 这个设计特别适合 32 位地址(相当于 4GB 的仓库容量):
    • 前 10 位查大区目录 → 中间 10 位查小区明细 → 后 12 位定位具体位置。
  • 类比:写地址时省-市(前10位)+ 区-街道(中间10位)+ 门牌号(后12位)。

1.3 更大地址的问题

  • 如果地址长度超过 32 位(比如 64 位地址),相当于仓库扩大到了整个地球的规模:
    • 原有的 1024×1024 分区方式不够用了。
    • 需要更大的目录本(占用更多内存),或增加分拣层级(如省-市-区-街道四级分拣)。
  • 类比:快递公司面对全球物流时,必须设计更复杂的分拣系统。
  • 总结:这种设计既保证了小规模地址(32 位)的高效管理,又暴露了处理更大地址空间时的局限性,后来新一代 CPU 都采用了多级分页机制来解决这个问题。

2. Linux 内存管理的层级

  • Linux 不仅考虑 32 位,还要考虑 64 位等其他 CPU 架构的实现。
  • 使用 3 层模型,由高到低分别为 PGD、PMD、PT,三者均为数据,而 PT 中的表项称为 PTE(Page Table Entry)。

2.1 分层结构与地址位段划分

  • 线性地址分为 4 个位段,分别是:
    • PGD 的下标 → 指向 PMD;
    • PMD 的下标 → 指向 PT;
    • PT 的下标 → 指向 PTE(物理页面的指针);
    • 最低位段 → 物理页面内的偏移量。

图片示意:
三层地址映射示意图

2.2 分页层级灵活配置

  • 实际编译过程中,Linux 源码根据 CPU 架构的不同,使用不同的分页层级:
    • 36 位地址使用三层映射,配置项为 CONFIG_X86_PAE = 1,对应文件为 include/asm-i386/pgtable-3level.h。
    • 32 位地址使用两层映射,对应文件为 include/asm-i386/pgtable-2level.h。

2.3 代码示例:pgtable-2level.h

/*
 * traditional i386 two-level paging structure:
 */

#define PGDIR_SHIFT	22
#define PTRS_PER_PGD	1024

/*
 * the i386 is two-level, so we don't really have any
 * PMD directory physically.
 */
#define PMD_SHIFT	22
#define PTRS_PER_PMD	1

#define PTRS_PER_PTE	1024
  • PGD 地址从 Bit22 开始,到 Bit31 共 10 位,可分为 1024 个表项(2^10)。
  • 由于使用两层地址映射,PMD 只有一个无效指针(2^0=1),等于没有 PMD 这层映射。
  • 类比结构:PGD 将图书馆分为 1024 个大区域,本应使用 PMD 再划分为中区域,最后是 PT 和 PTE。但在两级结构中,PMD 被省略,PGD 下直接对应 PT。

2.4 优点

  • 通过宏定义设定,可灵活适配不同架构的 CPU,实现不同的内存映射层级。

3. 用户空间与系统空间的划分

以 32 位 Linux 系统为例:

3.1 虚地址管理

  • 32 位地址可管理 4GB 虚存空间:
    • 系统空间:最高的 1GB,虚地址 0xC0000000 到 0xFFFFFFFF,用于内核本身;
    • 用户空间:0x00000000 到 0xBFFFFFFF,用于各个进程。
  • 注意事项:
    • 所有进程共享同一份内核代码和数据结构(如进程列表、内存映射表),即内核空间是共享的;
    • 每个进程都可以管理自己的 0x0 到 0xBFFFFFFF 空间,但最终通过 MMU 映射到不同的物理地址;
    • 虽然每个进程可以管理 3GB 虚拟空间,但是按需分配的,若不足会触发缺页中断进行分配。

3.2 虚地址与物理地址的映射

  • 系统空间在物理地址中实际上是从地址 0 开始,即虚地址与物理地址之间有一个 0xC0000000 的偏移量。

代码来源:include/asm-i386/page.h

/*
 * This handles the memory map.. We could make this a config
 * option, but too many people screw it up, and too few need
 * it.
 *
 * A __PAGE_OFFSET of 0xC0000000 means that the kernel has
 * a virtual address space of one gigabyte, which limits the
 * amount of physical memory you can use to about 950MB. 
 *
 * If you want more physical memory than this then see the CONFIG_HIGHMEM4G
 * and CONFIG_HIGHMEM64G options in the kernel configuration.
 */

#define __PAGE_OFFSET		(0xC0000000)

快速地址转换宏定义

#define PAGE_OFFSET		((unsigned long)__PAGE_OFFSET)
#define __pa(x)			((unsigned long)(x)-PAGE_OFFSET)
#define __va(x)			((void *)((unsigned long)(x)+PAGE_OFFSET))
  • 应用示例:切换进程时需要将寄存器 CR3 设置为新进程的 PGD 地址,但 CR3 需要的是物理地址,因此使用 __pa(x) 进行转换。

示例代码:include/asm-i386/mmu_context.h

/* Re-load page tables */
asm volatile("movl %0,%%cr3": :"r" (__pa(next->pgd)));

留下评论