驱动内存映射与mmap性能调优及故障排查 - 第1节

1.1 物理内存与虚拟内存概念

在 Linux 驱动开发中,理解内存映射的首要前提是区分物理内存与虚拟内存。物理内存是硬件实际提供的 RAM 芯片,而虚拟内存是操作系统为每个进程提供的抽象地址空间。

特性 物理内存 虚拟内存
本质 硬件 RAM 芯片上的实际存储单元 操作系统提供的进程地址空间抽象
地址范围 由硬件总线宽度决定(如 4GB、64GB) 由 CPU 架构决定(32位为 4GB,64位极大)
连续性 物理上可能不连续(存在空洞) 对进程表现为连续线性空间
可见性 所有进程共享同一物理地址空间 每个进程拥有独立的虚拟地址空间
驱动关注点 DMA 缓冲区、设备寄存器地址 用户态与内核态地址转换、mmap 映射
驱动开发要点: 驱动通常操作物理地址(如设备寄存器、DMA 缓冲区),但用户程序只能通过虚拟地址访问。mmap 的核心作用就是在两者之间建立映射关系。
关键概念: 虚拟内存使每个进程拥有独立的地址空间,进程间相互隔离。驱动通过 remap_pfn_range() 或 io_remap_pfn_range() 将物理内存映射到用户虚拟空间。
物理内存与虚拟内存关系图 进程 A 虚拟空间 0x00000000 代码段 数据段 堆 mmap 映射区 栈 0xFFFFFFFF 进程 B 虚拟空间 0x00000000 代码段 数据段 堆 mmap 映射区 栈 0xFFFFFFFF 物理内存 页框 0 页框 1 页框 2 (设备寄存器) 页框 3 页框 4 (DMA 缓冲区) 页表映射 页表映射

1.2 MMU 工作原理

内存管理单元(MMU)是 CPU 内部负责虚拟地址到物理地址转换的硬件模块。每次 CPU 访问内存时,MMU 都会介入完成地址翻译。

MMU 地址转换流程

  1. CPU 发出虚拟地址:程序指令访问某个虚拟地址(如 0x00401000)。
  2. MMU 截获地址:MMU 将虚拟地址拆分为页号(VPN)和页内偏移(Offset)。
  3. 查询页表:MMU 以 VPN 为索引,在进程页表中查找对应的页表项(PTE)。
  4. 权限检查:MMU 检查 PTE 中的访问权限(读/写/执行)和特权级。
  5. TLB 加速:如果 TLB(快表)命中,直接返回物理地址,跳过页表查询。
  6. 生成物理地址:将 PTE 中的物理页框号(PFN)与偏移量拼接,得到最终物理地址。
驱动开发陷阱: 如果驱动在中断上下文或原子上下文中访问用户空间虚拟地址,可能触发缺页异常(page fault),导致系统崩溃。必须使用 copy_from_user/copy_to_user 或 get_user_pages 等安全接口。
MMU 地址转换流程 CPU 发出虚拟地址 MMU 截获地址 TLB 查询 (快表) TLB 命中 直接返回物理地址 查询页表 (多级页表遍历) 更新 TLB 并返回物理地址 TLB 未命中时,MMU 硬件自动遍历多级页表,填充 TLB 后返回物理地址 整个过程对软件透明,但驱动可通过 huge page 减少 TLB 压力

1.3 页表与页框

页表是 MMU 进行地址转换的核心数据结构,页框(Page Frame)是物理内存的最小管理单元。Linux 内核以 4KB 为默认页大小(部分架构支持 64KB 或 huge page)。

页表结构(以 x86_64 四级页表为例)

页表级别 名称 索引位数 覆盖范围
L1 PML4(Page Map Level 4) 9 位 512 GB
L2 PDPT(Page Directory Pointer Table) 9 位 1 GB
L3 PD(Page Directory) 9 位 2 MB
L4 PT(Page Table) 9 位 4 KB
驱动视角: 驱动开发中,struct page 结构体代表一个物理页框。通过 virt_to_page() 将内核虚拟地址转换为 struct page *,通过 page_to_pfn() 获取页框号。mmap 驱动中常用 remap_pfn_range() 将连续的物理页框映射到用户空间。

页表项(PTE)关键标志位

  • Present(P):页框是否在物理内存中(缺页时清 0)。
  • Read/Write(R/W):页框是否可写。
  • User/Supervisor(U/S):用户态是否可访问。
  • Dirty(D):页框是否被写入过。
  • Accessed(A):页框是否被访问过。
  • Page Attribute Table(PAT):内存类型(UC/WC/WT/WB)。
四级页表地址转换过程 虚拟地址分解:PML4(9位) | PDPT(9位) | PD(9位) | PT(9位) | 页内偏移(12位) PML4 CR3 寄存器指向 PDPT 512 个表项 PD 512 个表项 PT 512 个表项 物理页框 PFN + 偏移量 每级页表占用 512 个表项 × 8 字节 = 4KB,恰好一页 驱动使用 huge page(2MB/1GB)可减少页表级数,降低 TLB 压力

页框管理关键数据结构

/* Linux 内核页框管理核心结构 */
struct page {
    unsigned long flags;        /* 页框状态标志 */
    atomic_t _refcount;         /* 引用计数 */
    atomic_t _mapcount;         /* 映射计数 */
    unsigned long private;      /* 私有数据(驱动可用) */
    struct address_space *mapping; /* 文件映射或匿名页 */
    pgoff_t index;              /* 页框在文件中的偏移 */
    struct list_head lru;       /* LRU 链表节点 */
    void *virtual;              /* 内核虚拟地址(若已映射) */
};

/* 驱动中常用的页框操作宏 */
#define page_to_pfn(page)   /* struct page → 页框号 */
#define pfn_to_page(pfn)    /* 页框号 → struct page */
#define virt_to_page(kaddr) /* 内核虚拟地址 → struct page */
#define page_address(page)  /* struct page → 内核虚拟地址 */
性能调优提示: 在 mmap 驱动中,如果映射的是大块连续物理内存(如 CMA 或预留内存),建议使用 remap_pfn_range() 配合 huge page 映射,可显著减少 TLB miss 次数。对于非连续物理内存,使用 vm_insert_page() 逐页插入。