第四章 内存管理与MMU:虚拟地址与物理地址映射
内存管理,说白了就是让芯片知道「数据该放哪儿、怎么找回来」。
我刚开始接触嵌入式AI芯片驱动时,总觉得内存这块儿太底层了,不就是malloc和free嘛?直到第一次调试DMA传输,数据莫名其妙地错乱,我才意识到——嗯,这里的水很深。
4.1 虚拟地址与物理地址:为什么要有两套地址?
先问个问题:为什么CPU不直接用物理地址?
你想想看,如果多个进程同时跑,每个进程都直接操作物理内存,那不乱套了?A进程写了个数据,B进程不小心就给覆盖了。虚拟地址的出现,就是为了给每个进程一个「独立的内存空间幻觉」。
在我的项目中,AI芯片通常有512MB到2GB的DDR。每个神经网络模型加载时,驱动会为它分配一段连续的虚拟地址空间。模型本身并不知道物理内存长什么样,它只认虚拟地址。
核心概念:
- 虚拟地址(VA):CPU看到的地址,每个进程独立
- 物理地址(PA):硬件总线上的真实地址
- MMU:负责把VA翻译成PA的硬件模块
MMU是怎么工作的?它维护了一张页表。每次CPU访问内存,MMU就查这张表,把虚拟地址映射到物理地址。页表的最小单位叫「页」,通常是4KB大小。
我曾经调试过一个bug:模型推理时偶尔崩溃,查了两天才发现是页表项被意外修改了。从那以后,我养成了一个习惯——每次映射完内存,都做一次完整性校验。
4.2 ioremap:把外设寄存器映射到内核空间
驱动开发中,我们经常要操作硬件寄存器。但这些寄存器在物理地址上,内核不能直接访问。怎么办?用ioremap。
ioremap的作用,就是把一段物理地址映射到内核的虚拟地址空间。这样驱动就能像操作普通内存一样,读写寄存器了。
#include <linux/io.h>
void __iomem *reg_base;
phys_addr_t reg_phys = 0x1A000000; // 假设寄存器基地址
reg_base = ioremap(reg_phys, 0x1000);
if (!reg_base) {
pr_err("ioremap failed\n");
return -ENOMEM;
}
// 读写寄存器
writel(0x01, reg_base + 0x00); // 写控制寄存器
val = readl(reg_base + 0x04); // 读状态寄存器
// 使用完后取消映射
iounmap(reg_base);
个人经验:我习惯在ioremap后立即打印映射结果,方便调试时确认地址是否正确。另外,记得检查返回的指针是不是NULL——别问我怎么知道的。
这里有个坑:ioremap映射的内存,不能直接用memcpy或memset操作。必须用readl/writel这类访问函数。为什么?因为寄存器可能有副作用,读一次和读两次结果不一样。
4.3 mmap:把内核内存映射到用户空间
用户空间的应用程序,怎么访问内核分配的内存?答案是mmap系统调用。
在AI芯片驱动中,我们经常用mmap把模型权重数据映射到用户空间。这样用户程序可以直接读写,省去了数据拷贝的开销。
// 驱动中的mmap实现
static int my_mmap(struct file *filp, struct vm_area_struct *vma)
{
unsigned long size = vma->vm_end - vma->vm_start;
phys_addr_t phys = get_phys_addr_from_buffer(); // 获取物理地址
// 设置内存属性:不缓存(防止Cache一致性问题)
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
if (remap_pfn_range(vma, vma->vm_start, phys >> PAGE_SHIFT,
size, vma->vm_page_prot)) {
return -EAGAIN;
}
return 0;
}
注意:mmap映射时,一定要考虑Cache一致性问题。如果驱动和用户空间同时访问同一块内存,而Cache策略不一致,数据就会错乱。我踩过这个坑,后面会详细讲。
用户空间的调用方式:
int fd = open("/dev/my_ai_device", O_RDWR);
void *buf = mmap(NULL, 1024*1024, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
// 现在可以直接操作buf了
memcpy(buf, model_data, model_size);
munmap(buf, 1024*1024);
close(fd);
4.4 DMA缓冲区分配:别让数据在路上丢了
DMA(直接内存访问)是AI芯片的命脉。模型推理时,数据从DDR搬到NPU(神经网络处理器),全靠DMA。但DMA缓冲区分配,有讲究。
普通的内存分配,物理地址可能不连续。DMA需要连续的物理内存,因为硬件不认虚拟地址。所以要用专门的API:
#include <linux/dma-mapping.h>
dma_addr_t dma_handle;
void *cpu_addr;
// 分配DMA缓冲区
cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
if (!cpu_addr) {
pr_err("DMA buffer allocation failed\n");
return -ENOMEM;
}
// cpu_addr是CPU看到的虚拟地址
// dma_handle是DMA控制器看到的物理地址(总线地址)
// 把dma_handle告诉硬件,它就知道去哪儿搬数据了
// 使用完后释放
dma_free_coherent(dev, size, cpu_addr, dma_handle);
关键点:
dma_alloc_coherent分配的内存,CPU和DMA访问时自动保持Cache一致性- 如果分配大块内存(比如几MB),建议用
dma_alloc_coherent而不是kmalloc - 有些芯片支持SMMU(系统MMU),可以不用连续物理内存,但大部分嵌入式AI芯片还是要求连续
我记得有一次,分配了4MB的DMA缓冲区,结果系统启动时内存碎片化严重,分配失败。后来改用CMA(连续内存分配器)才解决。嗯,大块连续内存分配,永远是个头疼的问题。
4.5 Cache一致性处理:最容易被忽视的坑
Cache一致性,说白了就是「CPU缓存里的数据和内存里的数据不一样了」。这个问题在AI芯片驱动中特别常见。
为什么?因为CPU和DMA(或NPU)访问内存的路径不同。CPU通过Cache,DMA直接访问内存。如果CPU写了一个数据到Cache,还没刷回内存,DMA就去读——读到的就是旧数据。
反过来也一样:DMA写入了新数据,CPU从Cache读——读到的还是旧数据。
我曾经踩过的坑:调试一个图像预处理驱动,CPU把图像数据写到缓冲区,然后通知NPU处理。结果NPU处理出来的图像全是花的。查了两天,发现是CPU写完后没刷Cache,NPU读到了脏数据。从那以后,我每次写完数据都手动刷Cache。
解决Cache一致性的几种方法:
| 方法 | 适用场景 | 性能影响 |
|---|---|---|
使用dma_alloc_coherent |
DMA缓冲区分配 | 低(硬件自动处理) |
手动刷Cache(dma_map_single) |
已有内存需要DMA访问 | 中等 |
| 设置页表为non-cacheable | 共享内存区域 | 高(每次访问都走内存) |
使用dma_sync_single_for_cpu/device |
流式DMA传输 | 低(按需同步) |
手动刷Cache的代码示例:
// 准备DMA传输前,刷Cache
dma_addr_t dma_handle;
void *buffer = kmalloc(size, GFP_KERNEL);
// 把数据写入buffer
memcpy(buffer, data, size);
// 告诉DMA控制器:数据准备好了
dma_handle = dma_map_single(dev, buffer, size, DMA_TO_DEVICE);
// 这里内部会刷Cache
// DMA传输完成后,告诉CPU:数据可能被DMA修改了
dma_unmap_single(dev, dma_handle, size, DMA_FROM_DEVICE);
// 这里内部会无效化Cache
我的建议:能用dma_alloc_coherent就别手动管理Cache。实在不行,也要用dma_map_single这类标准API。自己写刷Cache的代码,十有八九会漏掉某个边界情况。
最后说一句:Cache一致性不是「有或没有」的问题,而是「什么时候不一致」的问题。你写100次代码,可能99次都没事,但第100次就崩了。所以,从一开始就按规范来,别偷懒。
好了,内存管理和MMU这块儿,今天就聊到这儿。下一章我们讲中断处理——嗯,那又是另一个让人又爱又恨的话题。