1. Linux内核内存管理概述:物理内存与虚拟内存、MMU的作用、页表与地址转换
内存管理是Linux内核最核心的子系统之一,也是驱动开发者必须掌握的基础。在嵌入式系统中,硬件资源受限,理解物理内存与虚拟内存的映射关系、MMU的地址转换机制以及页表结构,直接决定了驱动代码的正确性与性能。
1.1 物理内存 vs 虚拟内存
| 概念 | 定义 | 驱动开发关注点 |
|---|---|---|
| 物理内存 | 硬件RAM芯片上真实存在的存储单元,由总线地址直接访问 | DMA缓冲区必须使用物理连续内存;外设寄存器映射到物理地址空间 |
| 虚拟内存 | 每个进程独立拥有的线性地址空间,通过MMU映射到物理内存 | 驱动中kmalloc/vmalloc返回的是虚拟地址;用户态指针不能直接用于DMA |
关键认知: 在启用MMU的Linux系统中,CPU执行指令时发出的所有地址都是虚拟地址。物理地址仅存在于MMU与内存控制器之间的总线上。驱动代码中,
__pa() 将内核虚拟地址转换为物理地址,__va() 做反向转换。
1.2 MMU的核心作用
内存管理单元(MMU)是CPU内部负责地址转换的硬件模块。它的三大职责构成了现代操作系统的内存基石:
- 地址转换: 将CPU发出的虚拟地址实时转换为物理地址,使每个进程拥有独立的地址空间。
- 内存保护: 通过页表项中的权限位(读/写/执行、用户/内核)阻止进程越界访问。
- 缓存控制: 页表项中的C/B位控制该页是否可被CPU缓存、是否写合并,这对DMA一致性至关重要。
驱动提示: 在DMA传输中,如果CPU缓存了DMA缓冲区的内容,会导致数据不一致。驱动必须使用
dma_alloc_coherent() 分配非缓存页,或手动调用 dma_map_single() 配合 dma_sync_single_for_cpu() 维护缓存一致性。
1.3 页表与地址转换过程
ARMv8-A 64位架构使用四级页表(TTBR0_EL1 指向PGD),Linux内核在此基础上实现了通用的四层页表模型:
| 层级 | 名称 | 每项位数 | 覆盖范围 |
|---|---|---|---|
| PGD | Page Global Directory | 9 bit | 512 GB |
| P4D | Page 4th Level Directory | 9 bit | 1 GB |
| PUD | Page Upper Directory | 9 bit | 2 MB |
| PMD | Page Middle Directory | 9 bit | 4 KB |
| PTE | Page Table Entry | 9 bit | 4 KB(页内偏移12 bit) |
地址转换过程:虚拟地址的高36位被拆分为5个9位索引,依次查找PGD→P4D→PUD→PMD→PTE,最终PTE中存放物理页帧号(PFN),与虚拟地址低12位偏移拼接得到完整物理地址。
1.4 内核中的内存区域划分
Linux内核将物理内存划分为三个管理区域,驱动开发者需要根据用途选择合适的分配接口:
| 区域 | 物理地址范围 | 分配接口 | 典型用途 |
|---|---|---|---|
| ZONE_DMA | 0 ~ 16 MB(x86)或 0 ~ 1 GB(ARM64) | __GFP_DMA |
ISA DMA、某些外设DMA限制 |
| ZONE_NORMAL | 16 MB ~ 896 MB(x86)或 1 GB ~ 3 GB(ARM64) | kmalloc()、__get_free_pages() |
内核栈、slab对象、驱动缓冲区 |
| ZONE_HIGHMEM | 896 MB 以上(x86 32位) | kmap()、vmalloc() |
用户进程页、大块临时映射(64位系统无此区域) |
常见陷阱: 在32位系统上,
kmalloc() 只能从 ZONE_NORMAL 分配物理连续内存,最大分配大小受限于 KMALLOC_MAX_SIZE(通常 4 MB)。如果需要更大的连续缓冲区,应使用 dma_alloc_coherent() 或引导时预留内存(memblock)。64位系统由于地址空间充足,ZONE_HIGHMEM 不再使用。
1.5 驱动开发中的地址转换速查
以下代码片段展示了驱动中常见的地址转换操作:
#include <linux/mm.h>
#include <linux/dma-mapping.h>
/* 内核虚拟地址 → 物理地址 */
unsigned long phys = __pa(kernel_vaddr);
/* 物理地址 → 内核虚拟地址(仅限低端内存) */
void *vaddr = __va(phys_addr);
/* 用户态虚拟地址 → 物理页帧(驱动中处理用户缓冲区时使用) */
struct page *page = virt_to_page(user_vaddr);
unsigned long pfn = page_to_pfn(page);
/* DMA 映射:虚拟地址 → 总线地址(DMA引擎使用) */
dma_addr_t dma_handle;
dma_handle = dma_map_single(dev, cpu_vaddr, size, DMA_TO_DEVICE);
/* 传输完成后必须解映射 */
dma_unmap_single(dev, dma_handle, size, DMA_TO_DEVICE);
最佳实践: 驱动中不要直接使用
__pa() 和 __va() 处理用户空间地址,因为用户页可能被换出或映射到高端内存。始终使用内核提供的 get_user_pages() 或 dma_map_sg() 系列接口来保证跨平台兼容性。