2. Linux 内核内存管理基础:虚拟地址与物理地址、页表与MMU、内核空间与用户空间划分
在嵌入式Linux驱动开发中,理解内存管理是掌握mmap与DMA缓冲区映射的前提。本章从硬件视角出发,系统梳理虚拟地址与物理地址的映射关系、MMU与页表的工作机制,以及内核空间与用户空间的划分原则。
核心认知: 驱动开发者操作的是虚拟地址,而硬件设备(如DMA控制器)直接访问物理地址。理解两者之间的转换桥梁——MMU与页表,是正确实现缓冲区映射的关键。
2.1 虚拟地址与物理地址
在启用MMU的系统中,CPU发出的地址并非直接送往内存总线,而是先经过MMU转换为物理地址。这种间接寻址机制带来了隔离、保护和灵活映射三大优势。
| 地址类型 | 定义 | 访问者 | 特点 |
|---|---|---|---|
| 虚拟地址 (VA) | CPU程序看到的地址空间 | 用户进程、内核线程 | 连续、独立、可隔离 |
| 物理地址 (PA) | 内存硬件单元的真实地址 | MMU、DMA控制器、总线 | 离散、全局、受硬件限制 |
| 总线地址 (BA) | 外设总线视角的地址 | DMA引擎、PCIe设备 | 可能与PA相同或经过IOMMU转换 |
驱动开发提示: 在DMA操作中,驱动程序必须使用物理地址或总线地址来配置DMA控制器。使用
virt_to_phys()和dma_map_single()等API完成地址转换。
2.2 页表与MMU
MMU(Memory Management Unit)是CPU内部负责地址转换的硬件单元,其核心数据结构是页表(Page Table)。页表将虚拟地址划分为固定大小的页(Page),映射到物理内存的页框(Page Frame)。
2.2.1 页表层级结构
现代ARM/Linux系统采用多级页表(通常为2~4级),以节省页表占用的物理内存。以ARM64的4级页表为例:
| 层级 | 名称 | 粒度 | 覆盖范围 |
|---|---|---|---|
| L0 | PGD (Page Global Directory) | 512GB | 全局目录 |
| L1 | P4D (Page 4th Directory) | 1GB | 四级目录 |
| L2 | PUD (Page Upper Directory) | 2MB | 上级目录 |
| L3 | PMD (Page Middle Directory) | 2MB | 中间目录 |
| L4 | PTE (Page Table Entry) | 4KB | 页表项 |
性能注意: 多级页表查询会引入多次内存访问开销。TLB(Translation Lookaside Buffer)作为页表的硬件缓存,可缓存最近使用的VA→PA映射,命中率通常超过99%。驱动开发中频繁切换地址空间(如context switch)会导致TLB刷新,影响实时性。
2.2.2 页表项属性
每个PTE不仅包含物理页框号(PFN),还包含关键的权限和缓存属性位:
- Present (P):页是否在物理内存中(缺页异常时清0)
- Read/Write (R/W):读写权限控制
- User/Supervisor (U/S):用户态/内核态访问权限
- Cache Disable (CD):禁止缓存(DMA缓冲区常用)
- Write-Through (WT):写透缓存策略
- Access/Dirty (A/D):由硬件维护的访问和脏页标志
驱动应用: 在实现mmap时,驱动程序通过
remap_pfn_range()设置PTE属性,将内核物理页框映射到用户空间。对于DMA缓冲区,通常设置Cache Disable或Write-Combine属性,避免缓存一致性问题。
2.3 内核空间与用户空间划分
Linux将4GB(32位)或更大的虚拟地址空间划分为两个部分:用户空间和内核空间。划分比例由CONFIG_PAGE_OFFSET决定,典型配置如下:
| 架构 | 用户空间范围 | 内核空间范围 | 划分比例 |
|---|---|---|---|
| ARM32 (3G/1G) | 0x00000000 - 0xBFFFFFFF | 0xC0000000 - 0xFFFFFFFF | 3GB / 1GB |
| ARM32 (2G/2G) | 0x00000000 - 0x7FFFFFFF | 0x80000000 - 0xFFFFFFFF | 2GB / 2GB |
| ARM64 | 0x0000000000000000 - 0x0000FFFFFFFFFFFF | 0xFFFF000000000000 - 0xFFFFFFFFFFFFFFFF | 48位 / 48位 |
2.3.1 内核空间布局
内核空间进一步划分为多个逻辑区域,每个区域具有不同的映射属性和用途:
- 直接映射区(ZONE_NORMAL/DMA):物理内存线性映射到内核虚拟地址空间,
virt_to_phys()可直接转换 - vmalloc区:用于非连续内存分配,虚拟地址连续但物理地址不连续
- 固定映射区(Fixmap):用于特殊用途(如APIC、IO空间)的固定虚拟地址
- 模块区:用于加载内核模块
- 高端内存区(HighMem):32位系统中访问超过1GB物理内存的临时映射区
2.4 驱动开发中的内存操作要点
在驱动开发中,理解上述内存管理机制有助于正确选择内存分配和映射API:
| 场景 | 推荐API | 内存区域 | 物理连续性 |
|---|---|---|---|
| 小尺寸DMA缓冲区 (< 4KB) | dma_alloc_coherent() |
ZONE_DMA | 连续 |
| 大尺寸DMA缓冲区 | dma_alloc_coherent() 或 __get_free_pages() |
ZONE_NORMAL | 连续 |
| 非连续DMA缓冲区 | dma_map_sg() + sg_table |
任意区域 | 可离散 |
| 用户态mmap映射 | remap_pfn_range() |
内核物理页→用户VA | 连续物理页框 |
| 内核虚拟地址转换 | virt_to_phys() / phys_to_virt() |
直接映射区 | 线性关系 |
常见陷阱:
- 不要对vmalloc返回的地址使用
virt_to_phys(),vmalloc区没有线性映射关系 - DMA缓冲区必须保证物理地址连续(除非使用SG模式),否则DMA控制器无法正确传输
- 用户态mmap映射时,必须确保映射的物理页框在映射期间不会被释放或重用
- ARM架构下DMA缓冲区需要设置PTE的Cache Disable位,否则CPU缓存与DMA数据不一致
2.5 本章小结
本章建立了嵌入式Linux驱动开发所需的内存管理知识框架:
- 虚拟地址与物理地址通过MMU和页表建立映射,驱动开发者必须清楚两者之间的转换关系
- 多级页表在节省内存的同时引入查询开销,TLB是关键的硬件加速器
- 内核空间与用户空间的划分决定了驱动代码的访问权限和地址范围
- 驱动中的内存分配和映射API选择取决于缓冲区大小、连续性要求和DMA操作需求