嵌入式Linux内核内存管理与DMA缓冲区实战
内核内存区域怎么划分?kmalloc和vmalloc到底选谁?DMA一致性映射和流式映射有什么区别?本文从ZONE分区到DMA API,配合Mermaid流程图和完整驱动示例,把嵌入式Linux内存与DMA的底层逻辑讲清楚。
一、原理简析
Linux内核将物理内存划分为多个ZONE区域,每个区域有不同的用途和约束,直接决定了内存分配API和DMA的使用方式。
在32位系统中,典型的ZONE划分为:ZONE_DMA覆盖0\~16MB(ISA设备DMA寻址限制)、ZONE_DMA32覆盖0\~4GB(32位DMA设备)、ZONE_NORMAL为直接映射的常规内存、ZONE_HIGHMEM为超出内核线性映射范围的高端内存。64位系统由于虚拟地址空间充足,通常不再需要ZONE_HIGHMEM。
内核通过GFP(Get Free Pages)标志控制分配行为:GFP_KERNEL用于进程上下文可睡眠场景,GFP_ATOMIC用于中断上下文等不可睡眠场景,GFP_DMA/GFP_DMA32限制从指定ZONE分配,GFP_USER用于用户空间分配。选错GFP标志是驱动开发中最常见的坑之一。
二、内存分配API
2.1 kmalloc
kmalloc是最常用的内核内存分配器,基于SLUB分配器(Linux 6.8起SLUB成为唯一slab分配器)。它返回物理连续的内存,因此可用于DMA操作。
关键特性:分配的内存物理连续且虚拟连续,属于内核直接映射区域,可通过__pa()直接获取物理地址。最大分配大小取决于KMALLOC_MAX_SIZE,通常为4MB(MAX_ORDER=11时,2^(11+12-1) = 4MB),SLAB分配器上限为32MB。
void *buf = kmalloc(4096, GFP_KERNEL);
if (!buf)
return -ENOMEM;
/* 使用buf */
kfree(buf);
2.2 vmalloc
vmalloc分配虚拟连续但物理不连续的内存,适合大块内存分配(如帧缓冲、模块加载)。由于需要建立页表映射,性能低于kmalloc。
注意:vmalloc内部使用GFP_KERNEL且可能睡眠,不能在中断上下文或原子上下文中调用。vmalloc分配的内存也不能直接用于DMA,因为物理页面不连续。
void *buf = vmalloc(1024 * 1024); /* 分配1MB */
if (!buf)
return -ENOMEM;
/* 使用buf */
vfree(buf);
2.3 页级分配
当需要整页内存时,直接使用伙伴系统分配器:
/* 分配1页(order=0),返回struct page指针 */
struct page *page = alloc_pages(GFP_KERNEL, 0);
if (!page)
return -ENOMEM;
/* 获取虚拟地址 */
void *vaddr = page_address(page);
/* 释放 */
__free_pages(page, 0);
对于高端内存(32位系统),需使用kmap()/kunmap()临时映射。
2.4 kvmalloc
kvmalloc先尝试kmalloc,失败后自动回退到vmalloc。适用于不确定分配大小是否超出kmalloc能力范围的场景。
void *buf = kvmalloc(buf_size, GFP_KERNEL);
if (!buf)
return -ENOMEM;
kvfree(buf);
三、DMA缓冲区管理
DMA(Direct Memory Access)允许设备直接访问系统内存,无需CPU介入。但DMA使用的地址与CPU虚拟地址不同,需要通过DMA映射API进行转换和管理。
3.1 DMA地址与掩码设置
使用DMA前,必须先设置设备的DMA寻址掩码:
/* 同时设置流式和一致性DMA掩码(推荐) */
int ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32));
if (ret) {
dev_err(dev, "failed to set DMA mask\n");
return ret;
}
dma_set_mask_and_coherent()等价于分别调用dma_set_mask()和dma_set_coherent_mask(),一次搞定两个掩码。如果设备支持64位DMA,使用DMA_BIT_MASK(64)。
3.2 一致性DMA映射
一致性映射(Coherent Mapping)在驱动初始化时建立,卸载时释放。内核保证CPU和设备看到的数据一致,驱动无需手动维护Cache一致性。
/* 分配一致性DMA缓冲区 */
dma_addr_t dma_handle;
void *virt_addr = dma_alloc_coherent(dev, BUF_SIZE, &dma_handle, GFP_KERNEL);
if (!virt_addr)
return -ENOMEM;
/* virt_addr: CPU访问的虚拟地址 */
/* dma_handle: 设备使用的DMA总线地址 */
/* 释放 */
dma_free_coherent(dev, BUF_SIZE, virt_addr, dma_handle);
技术说明:dma_alloc_coherent()在内核源码中实际是对dma_alloc_attrs()的封装(传入attrs=0)。dma_alloc_attrs()是更底层的接口,支持通过DMA属性控制分配行为(如DMA_ATTR_NO_WARN、DMA_ATTR_WRITE_COMBINE等)。大多数驱动直接用dma_alloc_coherent()就够了,需要细粒度控制时再用dma_alloc_attrs()。
注意:dma_free_coherent()只能在IRQ开启的上下文中调用。
3.3 流式DMA映射
流式映射(Streaming Mapping)用于临时映射,每次DMA传输前映射、传输后取消映射。驱动需要显式维护Cache一致性。
/* 单缓冲区映射 */
dma_addr_t dma_addr = dma_map_single(dev, virt_buf, buf_len, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma_addr)) {
dev_err(dev, "DMA map failed\n");
return -EIO;
}
/* DMA传输完成后,交回CPU所有权 */
dma_unmap_single(dev, dma_addr, buf_len, DMA_TO_DEVICE);
注意:dma_map_single()可能映射失败,必须使用dma_mapping_error()检查返回值,不能简单判断是否为0或NULL。这是内核DMA API文档明确要求的。
当CPU和设备需要交替访问同一缓冲区时,使用同步API:
/* CPU写入数据后,刷出Cache让设备可见 */
dma_sync_single_for_device(dev, dma_addr, buf_len, DMA_TO_DEVICE);
/* 设备写入数据后,使Cache无效让CPU可见 */
dma_sync_single_for_cpu(dev, dma_addr, buf_len, DMA_FROM_DEVICE);
3.4 DMA方向
| 方向常量 | 含义 |
|---|---|
DMA_BIDIRECTIONAL |
双向传输,性能开销最大 |
DMA_TO_DEVICE |
CPU写→设备读 |
DMA_FROM_DEVICE |
设备写→CPU读 |
DMA_NONE |
调试用,不应出现在正常代码中 |
应尽量使用精确的方向而非DMA_BIDIRECTIONAL,以减少不必要的Cache维护操作。
3.5 DMA池
DMA池用于分配大量小型一致性DMA缓冲区(如描述符环),比反复调用dma_alloc_coherent()更高效:
/* 创建DMA池 */
struct dma_pool *pool = dma_pool_create("my_desc_pool", dev,
DESC_SIZE, DESC_ALIGN, 0);
if (!pool)
return -ENOMEM;
/* 从池中分配 */
dma_addr_t dma_handle;
void *desc = dma_pool_alloc(pool, GFP_KERNEL, &dma_handle);
if (!desc) {
dma_pool_destroy(pool);
return -ENOMEM;
}
/* 释放回池 */
dma_pool_free(pool, desc, dma_handle);
/* 销毁池 */
dma_pool_destroy(pool);
3.6 内存池mempool
mempool在内核内存分配器之上提供保证分配成功的机制,在内存紧张时使用预分配的备用对象:
/* 创建基于kmalloc的内存池,最少保留4个备用对象 */
mempool_t *pool = mempool_create_kmalloc_pool(4, MY_OBJ_SIZE);
if (!pool)
return -ENOMEM;
/* 分配(保证成功,可能睡眠) */
void *obj = mempool_alloc(pool, GFP_KERNEL);
/* 释放回池 */
mempool_free(obj, pool);
/* 销毁 */
mempool_destroy(pool);
四、对比表格
kmalloc vs vmalloc
| 特性 | kmalloc | vmalloc |
|---|---|---|
| 物理连续性 | 物理连续 | 物理不连续 |
| 虚拟连续性 | 虚拟连续 | 虚拟连续 |
| DMA可用性 | 可直接用于DMA | 不可用于DMA |
| 最大分配 | 通常4MB(SLUB) | 受vmalloc地址空间限制 |
| 性能 | 高(直接映射) | 较低(需页表映射) |
| 原子上下文 | GFP_ATOMIC可用 | 不可用(会睡眠) |
| 典型场景 | 小对象、DMA缓冲 | 大块内存、模块加载 |
一致性DMA vs 流式DMA
| 特性 | 一致性映射 | 流式映射 |
|---|---|---|
| API | dma_alloc_coherent | dma_map_single |
| 生命周期 | 驱动初始化→卸载 | 传输前映射→传输后取消 |
| Cache一致性 | 自动维护 | 驱动手动维护 |
| 分配内存 | 是(分配+映射) | 否(仅映射已有内存) |
| 性能开销 | 较高(可能禁用Cache) | 较低(按需同步) |
| 典型场景 | 描述符环、控制寄存器 | 数据缓冲区、scatter-gather |
五、核心流程图
内存区域与分配策略选择
DMA映射选择流程
六、完整驱动示例
以下是一个使用一致性DMA映射的字符设备驱动示例,展示了从DMA掩码设置到缓冲区分配、传输、释放的完整流程:
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/dma-mapping.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/slab.h>
#define BUF_SIZE 4096
struct my_dma_dev {
struct device *dev;
void *virt_buf;
dma_addr_t dma_handle;
struct dma_pool *desc_pool;
};
static ssize_t my_dma_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
struct my_dma_dev *dma_dev = filp->private_data;
if (count > BUF_SIZE)
count = BUF_SIZE;
if (copy_from_user(dma_dev->virt_buf, buf, count))
return -EFAULT;
/* CPU写入完成,刷出Cache让设备可见 */
dma_sync_single_for_device(dma_dev->dev, dma_dev->dma_handle,
count, DMA_TO_DEVICE);
/* 此处启动硬件DMA传输(写设备寄存器触发) */
return count;
}
static ssize_t my_dma_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos)
{
struct my_dma_dev *dma_dev = filp->private_data;
if (count > BUF_SIZE)
count = BUF_SIZE;
/* 设备DMA写入完成,使Cache无效让CPU可见 */
dma_sync_single_for_cpu(dma_dev->dev, dma_dev->dma_handle,
count, DMA_FROM_DEVICE);
if (copy_to_user(buf, dma_dev->virt_buf, count))
return -EFAULT;
return count;
}
static int my_dma_open(struct inode *inode, struct file *filp)
{
struct miscdevice *mdev = filp->private_data;
struct my_dma_dev *dma_dev = dev_get_drvdata(mdev->parent);
filp->private_data = dma_dev;
return 0;
}
static const struct file_operations my_dma_fops = {
.owner = THIS_MODULE,
.open = my_dma_open,
.read = my_dma_read,
.write = my_dma_write,
};
static struct miscdevice my_dma_misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = "my_dma",
.fops = &my_dma_fops,
};
static int my_dma_probe(struct platform_device *pdev)
{
struct my_dma_dev *dma_dev;
int ret;
dma_dev = devm_kzalloc(&pdev->dev, sizeof(*dma_dev), GFP_KERNEL);
if (!dma_dev)
return -ENOMEM;
dma_dev->dev = &pdev->dev;
/* 第一步:设置DMA掩码 */
ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32));
if (ret) {
dev_err(&pdev->dev, "DMA mask set failed\n");
return ret;
}
/* 第二步:分配一致性DMA缓冲区 */
dma_dev->virt_buf = dma_alloc_coherent(&pdev->dev, BUF_SIZE,
&dma_dev->dma_handle, GFP_KERNEL);
if (!dma_dev->virt_buf) {
dev_err(&pdev->dev, "DMA alloc failed\n");
return -ENOMEM;
}
dev_info(&pdev->dev, "DMA buffer: virt=%p dma=%pad size=%d\n",
dma_dev->virt_buf, &dma_dev->dma_handle, BUF_SIZE);
/* 第三步:创建DMA池(用于描述符等小对象) */
dma_dev->desc_pool = dma_pool_create("my_desc_pool", &pdev->dev,
64, 64, 0);
if (!dma_dev->desc_pool) {
ret = -ENOMEM;
goto err_free_coherent;
}
platform_set_drvdata(pdev, dma_dev);
my_dma_misc.parent = &pdev->dev;
ret = misc_register(&my_dma_misc);
if (ret)
goto err_destroy_pool;
return 0;
err_destroy_pool:
dma_pool_destroy(dma_dev->desc_pool);
err_free_coherent:
dma_free_coherent(&pdev->dev, BUF_SIZE,
dma_dev->virt_buf, dma_dev->dma_handle);
return ret;
}
static void my_dma_remove(struct platform_device *pdev)
{
struct my_dma_dev *dma_dev = platform_get_drvdata(pdev);
misc_deregister(&my_dma_misc);
dma_pool_destroy(dma_dev->desc_pool);
dma_free_coherent(&pdev->dev, BUF_SIZE,
dma_dev->virt_buf, dma_dev->dma_handle);
}
static const struct of_device_id my_dma_of_match[] = {
{ .compatible = "myvendor,my-dma-device" },
{ }
};
MODULE_DEVICE_TABLE(of, my_dma_of_match);
static struct platform_driver my_dma_driver = {
.probe = my_dma_probe,
.remove = my_dma_remove,
.driver = {
.name = "my_dma",
.of_match_table = my_dma_of_match,
},
};
module_platform_driver(my_dma_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux Developer");
MODULE_DESCRIPTION("DMA buffer management demo driver");
对应的设备树节点:
my_dma: dma@40000000 {
compatible = "myvendor,my-dma-device";
reg = <0x40000000 0x1000>;
interrupts = <0 42 4>;
};
七、常见问题解决
Q1: kmalloc分配失败,返回NULL怎么办?
内存碎片化是大块kmalloc失败的常见原因。尽量在驱动初始化时提前分配,避免运行时分配大块内存。超过2页的分配考虑使用kvmalloc或vmalloc。检查是否在中断上下文中误用GFP_KERNEL。
Q2: dma_map_single返回的地址是否需要检查?
必须检查。使用dma_mapping_error()检测映射是否成功,直接比较返回值是否为0或NULL是错误的。映射失败通常发生在IOMMU资源耗尽或地址超出设备寻址范围时。
Q3: 为什么vmalloc分配的内存不能用于DMA?
vmalloc分配的内存物理页面不连续,DMA控制器需要连续的物理地址(除非使用IOMMU的scatter-gather)。如果设备支持IOMMU,可使用dma_map_sg()映射vmalloc页面,但这不是常规做法。
Q4: GFP_ATOMIC和GFP_KERNEL怎么选?
在中断处理函数、tasklet、持有自旋锁、RCU读临界区等不可睡眠的上下文中,必须使用GFP_ATOMIC。在进程上下文且不持锁时可使用GFP_KERNEL。GFP_ATOMIC不保证分配成功(不会触发回收),应尽量减少使用。
Q5: dma_alloc_coherent分配的内存性能为什么差?
一致性DMA内存可能在某些平台上被映射为不可Cache的(Write-Combine或Uncached),CPU访问速度低于普通内存。对于频繁CPU访问的数据,优先使用流式映射,仅在CPU和设备需要同时访问的控制结构上使用一致性映射。
八、总结
ZONE分区决定了内存从哪取,kmalloc/vmalloc/页级分配决定了怎么取,DMA映射决定了设备怎么用。几条硬规则:kmalloc物理连续可用于DMA,vmalloc仅虚拟连续不可DMA;一致性映射自动维护Cache一致性但有性能代价,流式映射需要手动同步但更灵活;dma_mapping_error()是必须的检查步骤;dma_set_mask_and_coherent()一次搞定两个掩码。下期聊IOMMU与scatter-gather DMA。