嵌入式Linux驱动开发中的内存管理与MMU配置工程 - 第1节
1. 内存管理基础概念
在嵌入式 Linux 驱动开发中,理解地址空间是内存管理的基石。CPU 在访问内存时涉及两种地址:物理地址(Physical Address, PA)和虚拟地址(Virtual Address, VA)。
| 地址类型 | 定义 | 特点 | 驱动关注点 |
|---|---|---|---|
| 物理地址 | 硬件总线上的真实地址,直接对应 SDRAM/DDR 的存储单元 | 唯一、固定、由 SoC 内存控制器直接寻址 | DMA 传输、设备寄存器访问必须使用物理地址 |
| 虚拟地址 | CPU 内核(MMU 开启后)看到的地址空间 | 每个进程独立、可映射、可隔离 | 驱动代码中 kmalloc/vmalloc 返回的都是虚拟地址 |
关键认知: 驱动开发中,代码里操作的指针地址 99% 是虚拟地址。只有编写 DMA 控制器、配置 MMU 页表或操作物理保留内存时,才需要显式处理物理地址。
地址转换公式(简化):
实际由 MMU 硬件查页表完成,驱动开发者只需调用
物理地址 = 虚拟地址 - 偏移量 + 物理基址实际由 MMU 硬件查页表完成,驱动开发者只需调用
virt_to_phys() / phys_to_virt() 等内核 API。
1.2 内存映射:建立 VA 与 PA 的桥梁
内存映射(Memory Mapping)是操作系统将虚拟地址与物理地址建立关联的机制。在 Linux 内核中,映射分为两类:
- 直接映射(线性映射):内核虚拟地址空间的一部分直接映射到物理内存,偏移固定。例如 ARM32 上
0xC000_0000映射到0x0000_0000。 - 动态映射(页表映射):通过页表(Page Table)按需建立映射,支持离散物理页组合成连续虚拟空间。
驱动陷阱: 使用
ioremap() 将外设物理寄存器映射到内核虚拟地址空间时,返回的地址不能直接解引用访问——必须通过 readl() / writel() 等访问函数,确保对 MMIO 区域的正确操作。
1.3 内存碎片化:驱动开发的隐形杀手
内存碎片化是指系统经过频繁分配与释放后,可用内存被分割成大量不连续的小块,导致大块连续物理内存分配失败。嵌入式系统中,碎片化直接影响驱动稳定性。
碎片化类型
| 碎片类型 | 产生原因 | 典型场景 | 对驱动的影响 |
|---|---|---|---|
| 外部碎片 | 多次分配/释放后,空闲页散布在已分配页之间 | 频繁 kmalloc/kfree 不同大小对象 | 大块 DMA 缓冲区分配失败 |
| 内部碎片 | 分配粒度大于请求大小,浪费页内空间 | 请求 30 字节,分配 64 字节 slab | 内存利用率下降,OOM 风险增加 |
内核对抗碎片化的机制:
- 伙伴系统(Buddy System):以 2^n 页为单位管理,合并相邻空闲块。
- slab 分配器:为常用数据结构预分配缓存,减少外部碎片。
- CMA(Contiguous Memory Allocator):预留物理连续区域,专供驱动大块分配。
- 内存规整(Memory Compaction):迁移页面以形成连续大块。
1.4 驱动开发中的内存管理实践要点
- 明确地址类型:在代码注释中标注每个指针是 VA 还是 PA,避免混用。
- 使用正确的分配 API:
- 小对象(< 1 页):
kmalloc()/kfree() - 大虚拟连续:
vmalloc()/vfree() - 大物理连续(DMA):
dma_alloc_coherent()或alloc_pages_exact()
- 小对象(< 1 页):
- 警惕碎片化:长时间运行的驱动应避免频繁分配/释放大块内存,必要时使用内存池(mempool)。
- 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);