2. Linux 内存管理概述:页表、TLB、内存区域与物理内存分配器
在编写 mmap 驱动之前,必须理解 Linux 内核如何管理物理内存和虚拟内存。本章将梳理四个核心概念:页表(Page Table)、TLB(Translation Lookaside Buffer)、内存区域(ZONE_DMA / ZONE_NORMAL / ZONE_HIGHMEM)以及物理内存分配器(Buddy System)。这些知识是理解 mmap 如何建立用户空间到物理内存直接映射的基础。
2.1 虚拟地址与物理地址的桥梁:页表
页表是 CPU 内存管理单元(MMU)用于将虚拟地址(VA)转换为物理地址(PA)的数据结构。每个进程拥有独立的页表,实现进程隔离。
- 页框(Page Frame):物理内存被划分为固定大小的块,通常为 4KB。
- 页(Page):虚拟地址空间也被划分为同样大小的块。
- 页表项(PTE):页表中的每一行,记录虚拟页到物理页框的映射关系、权限位(读/写/执行)、存在位等。
关键点: 在 ARM64 或 x86_64 架构中,页表通常采用多级结构(如 4 级页表:PGD → PUD → PMD → PTE),以节省内存。驱动开发中,我们常通过
remap_pfn_range() 或 io_remap_pfn_range() 直接操作页表,建立用户虚拟地址到物理页框的映射。
驱动提示: 在
mmap 回调中,内核提供 struct vm_area_struct(VMA)描述用户请求的虚拟地址区间。驱动通过修改该 VMA 对应的页表,实现物理内存的直接映射。
2.2 TLB:地址转换的加速缓存
TLB(Translation Lookaside Buffer)是 CPU 内部的一个硬件缓存,用于存储最近使用的虚拟地址到物理地址的转换结果。每次地址转换时,MMU 优先查询 TLB,若命中则无需遍历多级页表,大幅提升性能。
| 特性 | TLB 命中 | TLB 未命中 |
|---|---|---|
| 访问延迟 | 1-2 个 CPU 周期 | 数十到数百个周期(需遍历页表) |
| 对驱动的影响 | mmap 映射后访问速度快 | 首次访问或上下文切换后可能触发 |
| 刷新时机 | 进程切换、修改页表后需 TLB 刷新 | — |
驱动注意事项: 在
mmap 回调中建立新映射后,内核会自动处理 TLB 刷新。但如果驱动在运行时动态修改了已映射物理页的权限或缓存属性(例如通过 set_memory_uc()),必须显式调用 flush_tlb_kernel_range() 或类似接口,否则可能导致缓存一致性问题。
2.3 物理内存区域:ZONE_DMA / ZONE_NORMAL / ZONE_HIGHMEM
Linux 内核将物理内存划分为不同的区域(Zone),以适应不同硬件设备的 DMA 能力和 CPU 寻址范围。
- ZONE_DMA:低端物理内存区域(通常 < 16MB),用于需要 DMA 访问的旧设备。在 x86 上,ISA 设备只能访问此区域。
- ZONE_NORMAL:内核直接映射区域(线性映射),物理地址与虚拟地址存在固定偏移。大多数内核内存分配来自此区域。
- ZONE_HIGHMEM:高端内存区域(仅 32 位系统),物理地址高于内核线性映射范围,需要临时映射才能访问。64 位系统通常不需要此区域。
mmap 映射区域选择: 驱动在分配用于
mmap 的物理内存时,通常使用 kmalloc()(来自 ZONE_NORMAL)或 __get_free_pages()。如果需要 DMA 缓冲区,应使用 dma_alloc_coherent(),它会自动从合适的区域分配。在 32 位系统上,mmap 高端内存需要额外处理。
2.4 物理内存分配器:Buddy System
Buddy System(伙伴系统)是 Linux 内核管理物理页框的核心算法。它以 2 的幂次方(order)为单位管理连续物理页,解决外部碎片问题。
- 基本单位:一个页框(通常 4KB),order 0 表示 1 页,order 1 表示 2 页,order 2 表示 4 页,依此类推。
- 分配:从满足需求的最小 order 链表中分配。若该链表为空,则从更高 order 链表分裂出两个“伙伴”。
- 释放:释放时检查其伙伴是否空闲,若是则合并回更高 order,减少碎片。
| Order | 连续页数 | 大小 | 典型用途 |
|---|---|---|---|
| 0 | 1 | 4 KB | 小块内存、进程页表 |
| 1 | 2 | 8 KB | 小数据结构 |
| 3 | 8 | 32 KB | DMA 缓冲区 |
| 5 | 32 | 128 KB | 帧缓冲、网络包 |
| 8 | 256 | 1 MB | 大块连续内存 |
驱动编程接口: 驱动中常用
alloc_pages(gfp_mask, order) 或 __get_free_pages(gfp_mask, order) 从 Buddy System 分配连续物理页。例如,分配 4 页(16KB)连续内存:unsigned long addr = __get_free_pages(GFP_KERNEL, 2);。释放时使用 free_pages(addr, 2)。