4. DMA内存分配与映射
任务说明
本案例面向NPU驱动开发场景,需要为设备DMA传输准备一块物理连续的内存缓冲区,并建立该缓冲区物理地址到内核虚拟地址的映射。同时需要处理DMA一致性,确保CPU与设备访问数据时不会因缓存不一致而读到脏数据。完成目标后,驱动模块应能通过字符设备接口向用户态暴露该缓冲区的内核虚拟地址映射信息,供后续读写验证。
核心实现
采用Linux内核DMA API完成内存分配与映射。使用dma_alloc_coherent()一次性完成物理连续内存分配、内核虚拟地址映射及一致性处理(该接口默认关闭CPU cache,保证一致性)。通过platform device模型获取设备指针,并在模块卸载时释放资源。字符设备仅用于验证映射有效性,不实现复杂IO逻辑。
// npu_dma_map.c
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/dma-mapping.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#define DMA_BUF_SIZE (64 * 1024) // 64KB
#define DEV_NAME "npu_dma_map"
static struct platform_device *pdev;
static struct cdev cdev;
static dev_t dev_num;
static struct class *dev_class;
static struct device *dev;
static void *cpu_addr; // 内核虚拟地址
static dma_addr_t dma_addr; // 设备可用的DMA地址(物理地址)
static int dma_dev_open(struct inode *inode, struct file *filp) { return 0; }
static ssize_t dma_dev_read(struct file *filp, char __user *buf,
size_t count, loff_t *offset)
{
// 将内核虚拟地址和DMA地址以十六进制字符串形式返回给用户态
char kbuf[128];
int len;
if (*offset != 0)
return 0;
len = snprintf(kbuf, sizeof(kbuf), "cpu=%px dma=%pad size=%d\n",
cpu_addr, &dma_addr, DMA_BUF_SIZE);
if (copy_to_user(buf, kbuf, len))
return -EFAULT;
*offset = len;
return len;
}
static const struct file_operations dma_fops = {
.owner = THIS_MODULE,
.open = dma_dev_open,
.read = dma_dev_read,
};
static int __init dma_map_init(void)
{
int ret;
// 1. 注册platform device(模拟NPU设备)
pdev = platform_device_register_simple("npu-dma", -1, NULL, 0);
if (IS_ERR(pdev))
return PTR_ERR(pdev);
// 2. 分配一致DMA缓冲区
cpu_addr = dma_alloc_coherent(&pdev->dev, DMA_BUF_SIZE,
&dma_addr, GFP_KERNEL);
if (!cpu_addr) {
pr_err("dma_alloc_coherent failed\n");
ret = -ENOMEM;
goto err_pdev;
}
// 3. 创建字符设备用于验证
ret = alloc_chrdev_region(&dev_num, 0, 1, DEV_NAME);
if (ret)
goto err_dma;
cdev_init(&cdev, &dma_fops);
ret = cdev_add(&cdev, dev_num, 1);
if (ret)
goto err_chrdev;
dev_class = class_create(DEV_NAME);
if (IS_ERR(dev_class)) {
ret = PTR_ERR(dev_class);
goto err_cdev;
}
dev = device_create(dev_class, NULL, dev_num, NULL, DEV_NAME);
if (IS_ERR(dev)) {
ret = PTR_ERR(dev);
goto err_class;
}
pr_info("DMA buffer allocated: cpu=%px dma=%pad size=%d\n",
cpu_addr, &dma_addr, DMA_BUF_SIZE);
return 0;
err_class:
class_destroy(dev_class);
err_cdev:
cdev_del(&cdev);
err_chrdev:
unregister_chrdev_region(dev_num, 1);
err_dma:
dma_free_coherent(&pdev->dev, DMA_BUF_SIZE, cpu_addr, dma_addr);
err_pdev:
platform_device_unregister(pdev);
return ret;
}
static void __exit dma_map_exit(void)
{
device_destroy(dev_class, dev_num);
class_destroy(dev_class);
cdev_del(&cdev);
unregister_chrdev_region(dev_num, 1);
dma_free_coherent(&pdev->dev, DMA_BUF_SIZE, cpu_addr, dma_addr);
platform_device_unregister(pdev);
pr_info("DMA buffer released\n");
}
module_init(dma_map_init);
module_exit(dma_map_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("NPU DMA coherent allocation and mapping");
一致性处理说明
dma_alloc_coherent()返回的cpu_addr是内核虚拟地址,dma_addr是设备视角的DMA地址(物理地址)。该接口内部已对分配的内存区域关闭CPU cache(或配置为不可缓存),因此CPU与设备访问该区域时不会发生缓存一致性问题,无需手动调用dma_sync_*系列函数。若后续改用dma_alloc_attrs()或非一致性映射,则需在每次CPU/设备访问切换时显式同步。
编译与加载
# Makefile
obj-m += npu_dma_map.o
# 编译命令
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
# 加载与验证
sudo insmod npu_dma_map.ko
cat /dev/npu_dma_map
# 预期输出示例(地址值因系统而异):
# cpu=00000000a1b2c3d4 dma=0x10000000 size=65536
sudo rmmod npu_dma_map
dmesg | tail -5 # 应看到分配与释放日志
验证方法
加载模块后执行cat /dev/npu_dma_map,观察输出中cpu与dma地址均为非空值,且size为65536。通过dmesg确认内核日志打印的地址与字符设备输出一致。卸载模块时无错误提示,且dmesg显示释放日志,即判定通过。若需进一步验证物理连续性,可在代码中遍历页表检查相邻物理页的PFN是否连续,但当前最小实现以地址映射成功为验证目标。