4、驱动中的内存管理:mmap实现、物理地址映射、DMA缓冲区申请与释放
内存管理这块,说实在的,是驱动开发里最容易出bug的地方之一。我早年做视频采集卡驱动时,就因为DMA缓冲区没对齐,导致数据错位,排查了整整三天。嗯,今天咱们就把这块彻底讲透。
4.1 为什么驱动要操心内存?
用户态程序申请内存,基本就是malloc一把梭。但驱动不行。原因很简单:
- 物理地址连续性:DMA操作需要连续的物理内存,而用户态看到的是虚拟地址
- 内存访问权限:硬件设备需要直接访问内存,这需要建立物理地址到内核空间的映射
- 性能要求:数据拷贝开销大,最好让用户态直接操作设备内存
说白了,驱动就是用户态和硬件之间的「翻译官」。内存管理就是翻译官手里的字典,翻错了就出大事。
4.2 mmap实现:让用户态直接操作设备内存
mmap是字符设备驱动里最常用的内存共享机制。用户态调用mmap,驱动里对应的是mmap文件操作接口。
4.2.1 驱动侧mmap实现框架
#include <sys/mman.h>
#include <stdint.h>
// 设备结构体
typedef struct {
void *hw_regs; // 硬件寄存器基地址
size_t regs_size; // 寄存器区域大小
void *dma_buf; // DMA缓冲区
size_t dma_size; // DMA缓冲区大小
} my_device_t;
// mmap实现
int my_dev_mmap(resmgr_context_t *ctp, io_mmap_t *msg,
RESMGR_OCB_T *ocb) {
my_device_t *dev = ocb->attr;
uint64_t offset = msg->offset;
uint64_t len = msg->len;
// 判断映射区域
if (offset >= dev->regs_size &&
offset < dev->regs_size + dev->dma_size) {
// 映射DMA缓冲区
uint64_t dma_offset = offset - dev->regs_size;
void *phys_addr = get_dma_phys(dev->dma_buf) + dma_offset;
return mmap_device_memory(
NULL, len,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_PHYS,
(uint64_t)phys_addr, 0);
}
// 映射硬件寄存器
return mmap_device_io(len, offset);
}
我个人习惯把寄存器区域和DMA缓冲区放在连续的偏移空间里。这样用户态只需要一次mmap调用,就能访问所有资源。你想想看,少一次系统调用,性能就多一分。
小提示:mmap_device_memory和mmap_device_io的区别在于,前者映射的是普通内存,后者映射的是IO空间。硬件寄存器通常用mmap_device_io,DMA缓冲区用mmap_device_memory。
4.2.2 用户态调用示例
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
int main() {
int fd = open("/dev/mydev", O_RDWR);
// 映射寄存器区域(偏移0,大小4KB)
void *regs = mmap(NULL, 4096,
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
// 映射DMA缓冲区(偏移4KB,大小1MB)
void *dma = mmap(NULL, 1024*1024,
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 4096);
// 直接操作硬件寄存器
*(volatile uint32_t *)(regs + 0x00) = 0x1; // 启动DMA
// 直接读写DMA缓冲区
memcpy(dma, data, size);
munmap(regs, 4096);
munmap(dma, 1024*1024);
close(fd);
return 0;
}
4.3 物理地址映射:从虚拟到物理的桥梁
QNX是微内核系统,驱动跑在用户态。这意味着驱动看到的地址都是虚拟地址。但硬件设备只认物理地址。怎么办?
嗯,这里就要用到物理地址映射了。
4.3.1 常用API
| 函数 | 用途 | 注意事项 |
|---|---|---|
| mmap_device_memory | 将物理内存映射到进程空间 | 需要MAP_PHYS标志 |
| mmap_device_io | 映射IO端口或寄存器 | 通常用于PCIe BAR空间 |
| as_phys2virt | 物理地址转虚拟地址 | 仅限内核态使用 |
| as_virt2phys | 虚拟地址转物理地址 | 需要进程权限 |
4.3.2 实战:PCIe设备BAR空间映射
// 假设我们已经通过PCI枚举拿到了BAR0的物理地址
uint64_t bar0_phys = 0xFE000000;
size_t bar0_size = 0x1000; // 4KB
// 映射到进程空间
void *bar0_virt = mmap_device_memory(
NULL, // 系统选择地址
bar0_size, // 映射大小
PROT_READ | PROT_WRITE, // 读写权限
MAP_SHARED | MAP_PHYS, // 物理地址映射
bar0_phys, // 物理地址
0); // 偏移
if (bar0_virt == MAP_FAILED) {
perror("mmap_device_memory failed");
return -1;
}
// 现在可以直接操作硬件寄存器了
uint32_t id = *(volatile uint32_t *)(bar0_virt + 0x00);
printf("Device ID: 0x%x\n", id);
注意:我在项目中遇到过一个问题——mmap_device_memory返回的地址是虚拟地址,但如果你在驱动里把这个地址传给DMA引擎,那就完蛋了。DMA需要的是物理地址!所以一定要用as_virt2phys或者从设备树/PCI配置空间里获取物理地址。
4.4 DMA缓冲区申请与释放
DMA缓冲区是驱动开发的核心难点。为什么?因为DMA要求:
- 物理连续:DMA引擎通常不支持分散-聚集
- 对齐要求:通常需要页对齐(4KB),有些设备要求更高
- 缓存一致性:CPU和DMA同时访问时,数据可能不一致
4.4.1 申请DMA缓冲区
#include <sys/dma.h>
dma_addr_t dma_handle;
void *cpu_addr;
size_t buf_size = 1024 * 1024; // 1MB
// 申请DMA缓冲区
int ret = dma_alloc(&dma_handle, buf_size,
DMA_PHYS_CONTIGUOUS | DMA_ALIGN_4K);
if (ret != EOK) {
perror("dma_alloc failed");
return -1;
}
// 获取CPU可访问的虚拟地址
cpu_addr = dma_addr_cpu(&dma_handle);
if (cpu_addr == NULL) {
dma_free(&dma_handle);
return -1;
}
// 获取DMA引擎需要的物理地址
uint64_t phys_addr = dma_addr_phys(&dma_handle);
printf("DMA buffer: CPU=%p, PHYS=0x%llx\n",
cpu_addr, phys_addr);
4.4.2 缓存同步
DMA操作完成后,CPU看到的数据可能还是旧的。这是因为CPU有缓存。需要手动同步:
// DMA写入数据到缓冲区后,CPU读取前
dma_cache_sync(&dma_handle,
DMA_CACHE_INVALIDATE);
// CPU写入数据到缓冲区后,DMA读取前
dma_cache_sync(&dma_handle,
DMA_CACHE_FLUSH);
// 或者两者都做
dma_cache_sync(&dma_handle,
DMA_CACHE_FLUSH_INVALIDATE);
核心要点:
- DMA写完后,CPU读之前:INVALIDATE(让CPU缓存失效,从内存重新读)
- CPU写完后,DMA读之前:FLUSH(把CPU缓存刷到内存)
- 不确定时:FLUSH_INVALIDATE(先刷再失效,最安全但性能稍差)
4.4.3 释放DMA缓冲区
// 使用完毕后释放
dma_free(&dma_handle);
dma_handle = NULL;
cpu_addr = NULL;
我曾经犯过一个低级错误——忘记在驱动卸载时释放DMA缓冲区。结果系统运行几天后,内存碎片化严重,新的DMA申请总是失败。嗯,从那以后我养成了习惯:申请时就在注释里写好释放的位置。
4.5 避坑指南
做内存管理,坑是真的多。我列几个常见的:
- 忘记页对齐:mmap要求偏移和大小都是页对齐的(4KB)。不对齐的话,mmap会返回EINVAL。
- DMA缓冲区太小:有些设备要求最小缓冲区大小,比如网卡通常需要2KB以上的缓冲区。
- 缓存同步遗漏:这是最常见的bug。数据明明写进去了,读出来却是旧的。八成是缓存没同步。
- 物理地址泄露:用户态通过mmap拿到了物理地址,如果驱动不做好权限控制,恶意程序可能直接读写物理内存。
我的习惯:在驱动初始化时,一次性申请好所有DMA缓冲区。运行时只做分配和释放的记账,不做实际的内存操作。这样能避免运行时内存碎片化的问题。
4.6 总结
内存管理是驱动开发的基石。mmap让用户态直接操作设备内存,物理地址映射连接了虚拟和物理世界,DMA缓冲区则是高性能数据传输的保障。
记住三个原则:
- 物理地址给硬件,虚拟地址给软件
- DMA操作前后一定要做缓存同步
- 申请的资源一定要释放,否则内存泄漏等着你
下一章我们会讲中断处理。嗯,那又是另一个精彩的话题了。