linux内核 - 内存管理单元(MMU)与地址翻译(二)
在linux内核中,通俗来说,PAGE_SHIFT
就是一个用来计算页内偏移和页号的“换算规则”:它告诉我们偏移量需要多少位二进制来表示,同时也说明把逻辑地址或物理地址右移多少位可以得到对应的页号或页帧号,从而方便在页表中查找和访问内存。
#ifdef CONFIG_ARM64_64K_PAGES
#define PAGE_SHIFT 16
#elif defined(CONFIG_ARM64_16K_PAGES)
#define PAGE_SHIFT 14
#else
#define PAGE_SHIFT 12
#endif
#define PAGE_SIZE (_AC(1, UL) << PAGE_SHIFT)
默认情况下(无论是在 ARM 还是 ARM64 上),PAGE_SHIFT
的值是 12,这意味着页面大小为 4 KB。在 ARM64 上,如果选择 16 KB 或 64 KB 的页面大小,PAGE_SHIFT
分别是 14 或 16。
在理解了地址转换机制后,可以发现单级页表只是一个“部分解决方案”。原因在于:大多数 32 位架构需要 32 位(4 字节)来表示一个页表项。在这样的系统上(32 位),每个进程拥有独立的 3 GB 用户地址空间,我们需要 786,432 个页表项来覆盖并描述一个进程的整个地址空间。这意味着仅仅存储内存映射就要为每个进程消耗过多的物理内存。而实际上,一个进程通常只会使用其虚拟地址空间中的一小部分,并且分布是零散的。
为了解决这个问题,引入了“多级”的概念。页表按照层级(page level)组织。存储多级页表所需的空间只依赖于进程实际使用的虚拟地址空间大小,而不再与虚拟地址空间的最大值成正比。这样,未使用的内存空间就不再需要对应的页表项,同时也缩短了页表遍历的时间。此外,在这种结构中,第 N 级页表中的每个表项都会指向第 N+1 级页表中的某个表项,其中第 1 级是最高层。
Linux 最多支持四级分页,但实际使用的分页层数取决于具体的体系结构。以下是各级的说明:
1. 页全局目录(PGD, Page Global Directory):这是第一级(level 1)页表。内核中每个条目类型为 pgd_t(通常是 unsigned long),指向第二级页表中的一个条目。在 Linux 内核中,struct task_struct 表示一个进程的描述,其中有一个成员 mm,类型为 struct mm_struct,用于描述和表示进程的内存空间。在 struct mm_struct 中,有一个与处理器相关的字段 pgd,它指向该进程一级(PGD)页表的第一个条目(entry 0)。每个进程只有一个 PGD,最多可包含 1,024 个条目。
2. 页上级目录(PUD, Page Upper Directory):表示第二级页表,是第二层间接索引。
3. 页中间目录(PMD, Page Middle Directory):表示第三级页表,是第三级间接索引。
4. 页表项(PTE, Page Table Entry):这是树的叶子节点,是 pte_t 类型的数组,每个条目指向一个物理页。
MMU 本身不存储映射信息,它只是用来做地址转换的硬件。实际的映射信息存在内存中的页表里。CPU 有一个特殊寄存器(PTBR 或 TTBR0),指向当前进程的顶级页表(PGD)。
当进程被调度到 CPU 时,内核会把这个寄存器指向新进程的 PGD。之后,当 MMU 收到一个虚拟地址,它就从这个顶级页表开始查:先根据地址的高位找到一级页表条目,再找二级条目……直到找到最终的页表项(PTE),获得对应的物理页。最后,再加上页内偏移量,就能访问到物理内存中具体的位置。
当一个进程需要读取或写入某个内存位置时(这里指的是虚拟内存),MMU 会通过该进程的页表进行地址转换,找到对应的页表项(PTE)。处理器会从虚拟地址中提取虚拟页号,并将其作为索引在进程的页表中查找对应的页表项。如果在该偏移处存在有效的页表项,处理器就会从中获取页帧号(PFN)。如果没有有效的页表项,说明进程访问了未映射的虚拟内存区域,此时会产生一个缺页异常(page fault),操作系统需要进行处理。
缺页异常发生后,操作系统会接管处理:它首先检查访问的虚拟地址是否合法,如果非法则终止进程;如果合法但未映射物理页,操作系统会分配一个物理页并在页表中建立映射,随后让 CPU 重新访问该地址,从而完成内存访问。