2. Linux 内存管理基础:虚拟地址空间、页表、TLB、物理内存与虚拟内存的映射关系
在深入 mmap 驱动实现之前,必须理解 Linux 内存管理的核心抽象层。现代操作系统通过虚拟内存机制,为每个进程提供独立的、连续的地址空间,而底层物理内存则可能是不连续的、甚至被换出到磁盘。本章将拆解这一机制的核心组件:虚拟地址空间、页表、TLB 以及物理内存与虚拟内存的映射关系。
核心观点: 虚拟内存是进程的“幻觉”,页表是“翻译官”,TLB 是“加速缓存”。驱动开发者操作物理内存时,必须通过内核提供的接口(如
mmap、remap_pfn_range)来建立或修改这些映射,而不是直接访问物理地址。
2.1 虚拟地址空间布局
在 32 位系统上,虚拟地址空间为 4GB(0x00000000 ~ 0xFFFFFFFF)。Linux 将其划分为用户空间和内核空间两部分,比例通常为 3:1(用户空间 3GB,内核空间 1GB)。在 64 位系统上,地址空间巨大,但划分原则类似。
| 区域 | 地址范围(32位典型值) | 权限 | 内容 |
|---|---|---|---|
| 用户空间 | 0x00000000 ~ 0xBFFFFFFF | 用户态可读写 | 代码、数据、堆、栈、共享库 |
| 内核空间 | 0xC0000000 ~ 0xFFFFFFFF | 内核态可读写 | 内核代码、驱动、页表、物理内存直接映射区 |
驱动开发提示: 用户空间程序无法直接访问内核空间地址。驱动通过
mmap 将内核空间的物理内存或 DMA 缓冲区映射到用户空间,本质是在用户空间的页表中插入新的映射条目,使进程能安全地访问这些物理页面。
2.2 页表:虚拟地址到物理地址的转换
页表是操作系统维护的一种数据结构,用于将虚拟地址转换为物理地址。Linux 采用多级页表结构(如 4 级页表:PGD、PUD、PMD、PTE),以节省内存并支持大地址空间。
- 页框(Page Frame): 物理内存被划分为固定大小的块,通常为 4KB。
- 页(Page): 虚拟地址空间也被划分为同样大小的块。
- 页表项(PTE): 每个虚拟页对应一个页表项,记录其映射的物理页框号及权限属性。
| 页表层级 | 名称 | 作用 |
|---|---|---|
| PGD | Page Global Directory | 顶级目录,每个进程一个 |
| PUD | Page Upper Directory | 第二级目录(64位系统使用) |
| PMD | Page Middle Directory | 第三级目录 |
| PTE | Page Table Entry | 最终页表项,包含物理页框号和标志位 |
性能陷阱: 每次内存访问都需要经过多级页表查询,这会导致多次内存访问开销。TLB(Translation Lookaside Buffer)正是为了解决这一性能瓶颈而设计的硬件缓存。
2.3 TLB:地址转换的硬件缓存
TLB(Translation Lookaside Buffer)是 CPU 内部的一个小型、高速的硬件缓存,用于存储最近使用的虚拟地址到物理地址的映射关系。当 CPU 访问内存时,首先在 TLB 中查找;如果命中,则直接获得物理地址,无需访问内存中的页表。
- TLB 命中: 转换速度极快(通常 1 个 CPU 周期),性能最优。
- TLB 未命中: 硬件需要遍历多级页表(页表遍历),导致数十甚至数百个周期的延迟。
- TLB 刷新: 当页表内容被修改(如建立新的
mmap映射、解除映射、页面换入换出)时,必须使对应的 TLB 条目失效,否则 CPU 会使用过时的缓存映射。
驱动关键操作: 在
mmap 驱动中,当通过 remap_pfn_range 或 io_remap_pfn_range 建立新的物理内存映射后,内核会自动处理 TLB 的刷新。但在某些高级场景(如直接修改页表、使用 kmap 等),驱动开发者可能需要手动调用 flush_tlb_page 或 flush_tlb_mm 来确保 TLB 一致性。
2.4 物理内存与虚拟内存的映射关系
物理内存是硬件实际提供的 RAM,而虚拟内存是操作系统为每个进程提供的抽象。两者通过页表建立映射。一个物理页框可以被多个进程的虚拟页同时映射(共享内存),也可以被一个进程独占。
| 概念 | 说明 | 驱动中的体现 |
|---|---|---|
| 物理页框 | 物理内存的最小管理单元(4KB) | 通过 __pa() 将内核虚拟地址转为物理地址 |
| 虚拟页 | 进程地址空间中的连续区域 | 通过 mmap 系统调用创建 |
| 页表项 | 记录虚拟页到物理页框的映射关系 | 通过 remap_pfn_range 建立 |
| 缺页异常 | 访问未映射的虚拟页时触发 | 驱动可通过 vm_fault 处理自定义缺页 |
mmap 的本质: 驱动中的
mmap 实现,就是在进程的虚拟地址空间中分配一段虚拟地址区域(VMA),然后通过 remap_pfn_range 将这段虚拟区域与指定的物理页框建立页表映射。此后,进程访问该虚拟地址时,CPU 通过页表直接找到物理内存,无需经过内核的 read/write 系统调用。
2.5 驱动开发中的关键数据结构
在编写 mmap 驱动时,以下内核数据结构是必须掌握的:
struct vm_area_struct (VMA): 描述进程虚拟地址空间中一个连续区域的属性,包括起始地址、结束地址、权限标志、以及关联的vm_ops操作函数集。驱动通过mmap回调函数接收并操作 VMA。struct page: 描述一个物理页框的内核结构体。驱动通过virt_to_page()或pfn_to_page()获取物理页框对应的struct page指针。pgprot_t: 页表项的保护标志位,用于控制缓存策略(如是否启用 Cache、Write-Combine 等)。驱动通过pgprot_noncached()、pgprot_writecombine()等宏修改这些标志。
mmap 驱动核心流程:
- 用户进程调用
mmap()系统调用。 - 内核创建新的 VMA,并调用驱动的
mmap文件操作回调。 - 驱动调用
remap_pfn_range(),将 VMA 的虚拟地址范围映射到指定的物理页框。 - 驱动通过
vma->vm_page_prot设置缓存策略(如禁用 Cache 以支持 DMA)。 - 用户进程直接读写映射后的虚拟地址,CPU 通过页表直接访问物理内存。