嵌入式Linux驱动开发中的内存管理与MMU配置 - 第1节

1.1 物理地址与虚拟地址

在嵌入式 Linux 驱动开发中,理解地址空间是内存管理的基石。CPU 在访问内存时涉及两种地址:物理地址(Physical Address, PA)和虚拟地址(Virtual Address, VA)。

地址类型 定义 特点 驱动关注点
物理地址 硬件总线上的真实地址,直接对应 SDRAM/DDR 的存储单元 唯一、固定、由 SoC 内存控制器直接寻址 DMA 传输、设备寄存器访问必须使用物理地址
虚拟地址 CPU 内核(MMU 开启后)看到的地址空间 每个进程独立、可映射、可隔离 驱动代码中 kmalloc/vmalloc 返回的都是虚拟地址
关键认知: 驱动开发中,代码里操作的指针地址 99% 是虚拟地址。只有编写 DMA 控制器、配置 MMU 页表或操作物理保留内存时,才需要显式处理物理地址。
地址转换公式(简化):
物理地址 = 虚拟地址 - 偏移量 + 物理基址
实际由 MMU 硬件查页表完成,驱动开发者只需调用 virt_to_phys() / phys_to_virt() 等内核 API。
CPU 地址空间:虚拟地址 → 物理地址映射 虚拟地址空间 (VA) 进程 A: 0x0000_0000 ~ 0xBFFF_FFFF 进程 B: 0x0000_0000 ~ 0xBFFF_FFFF 内核空间: 0xC000_0000 ~ 0xFFFF_FFFF 每个进程拥有独立的 4GB 虚拟空间 MMU 页表转换 (TLB 缓存加速) 物理地址空间 (PA) DDR 区域: 0x8000_0000 ~ 0x8FFF_FFFF 外设寄存器: 0x4000_0000 ~ 0x4FFF_FFFF 保留内存: 0x9000_0000 ~ 0x90FF_FFFF 物理地址由 SoC 内存映射决定

1.2 内存映射:建立 VA 与 PA 的桥梁

内存映射(Memory Mapping)是操作系统将虚拟地址与物理地址建立关联的机制。在 Linux 内核中,映射分为两类:

  • 直接映射(线性映射):内核虚拟地址空间的一部分直接映射到物理内存,偏移固定。例如 ARM32 上 0xC000_0000 映射到 0x0000_0000。
  • 动态映射(页表映射):通过页表(Page Table)按需建立映射,支持离散物理页组合成连续虚拟空间。
驱动陷阱: 使用 ioremap() 将外设物理寄存器映射到内核虚拟地址空间时,返回的地址不能直接解引用访问——必须通过 readl() / writel() 等访问函数,确保对 MMIO 区域的正确操作。
内存映射类型对比 直接映射(线性映射) VA: 0xC000_0000 PA: 0x0000_0000 偏移量 = 0xC000_0000 物理连续,性能高 用于内核低端内存 动态映射(页表映射) VA: 0xE000_1000 PA: 0x8000_5000 页表项指定映射关系 物理可不连续 用于 vmalloc、mmap

1.3 内存碎片化:驱动开发的隐形杀手

内存碎片化是指系统经过频繁分配与释放后,可用内存被分割成大量不连续的小块,导致大块连续物理内存分配失败。嵌入式系统中,碎片化直接影响驱动稳定性。

碎片化类型

碎片类型 产生原因 典型场景 对驱动的影响
外部碎片 多次分配/释放后,空闲页散布在已分配页之间 频繁 kmalloc/kfree 不同大小对象 大块 DMA 缓冲区分配失败
内部碎片 分配粒度大于请求大小,浪费页内空间 请求 30 字节,分配 64 字节 slab 内存利用率下降,OOM 风险增加
内核对抗碎片化的机制:
  • 伙伴系统(Buddy System):以 2^n 页为单位管理,合并相邻空闲块。
  • slab 分配器:为常用数据结构预分配缓存,减少外部碎片。
  • CMA(Contiguous Memory Allocator):预留物理连续区域,专供驱动大块分配。
  • 内存规整(Memory Compaction):迁移页面以形成连续大块。
内存碎片化示意:碎片化 vs 规整后 碎片化状态 已分配 空闲 空闲 空闲 空闲块分散,无法满足 100 页连续分配 DMA 请求 4MB 连续缓冲区 → 失败 内存规整 规整后状态 已分配 连续空闲块 (170 页) 空闲页被迁移合并,形成大块连续区域 DMA 分配成功,驱动正常运行

1.4 驱动开发中的内存管理实践要点

  1. 明确地址类型:在代码注释中标注每个指针是 VA 还是 PA,避免混用。
  2. 使用正确的分配 API:
    • 小对象(< 1 页):kmalloc() / kfree()
    • 大虚拟连续:vmalloc() / vfree()
    • 大物理连续(DMA):dma_alloc_coherent() 或 alloc_pages_exact()
  3. 警惕碎片化:长时间运行的驱动应避免频繁分配/释放大块内存,必要时使用内存池(mempool)。
  4. MMU 开关影响:在 bootloader 或早期内核代码中,MMU 关闭时只能使用物理地址;驱动入口处 MMU 已开启,默认使用虚拟地址。
调试技巧: 通过 /proc/buddyinfo 查看伙伴系统各阶空闲页数量,判断碎片化程度;通过 /proc/pagetypeinfo 查看迁移类型分布。
// 示例:驱动中安全地进行地址转换
#include <linux/mm.h>
#include <linux/io.h>

void *va;
phys_addr_t pa;

// 分配内核内存(返回虚拟地址)
va = kmalloc(4096, GFP_KERNEL);
if (!va) {
    pr_err("内存分配失败\n");
    return -ENOMEM;
}

// 获取对应的物理地址(用于 DMA 或调试)
pa = virt_to_phys(va);
pr_info("VA: %px -> PA: %pa\n", va, &pa);

// 释放内存
kfree(va);