ESC
输入关键词搜索文章标题和内容

嵌入式Linux内核内存管理与DMA缓冲区实战

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

嵌入式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标志是驱动开发中最常见的坑之一。

flowchart TB A(["物理内存起始"]) --> B["ZONE_DMA<br/>0 ~ 16MB"] B --> C["ZONE_DMA32<br/>0 ~ 4GB"] C --> D["ZONE_NORMAL<br/>直接映射区"] D --> E["ZONE_HIGHMEM<br/>高端内存<br/>仅32位系统"] E --> F(["物理内存结束"]) style A fill:#E8F5E9 style B fill:#E3F2FD style C fill:#E3F2FD style D fill:#E3F2FD style E fill:#FFF8E1 style F fill:#E8F5E9

二、内存分配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一致性。

来自 linuxros.cn · linuxROS
/* 单缓冲区映射 */
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

五、核心流程图

内存区域与分配策略选择

flowchart TB A(["需要分配内存"]) --> B{"需要DMA?"} B -->|"是"| C{"缓冲区大小?"} B -->|"否"| D{"大小 > 几页?"} C -->|"小对象<br/>≤页大小"| E["dma_pool<br/>小型一致性缓冲"] C -->|"大缓冲"| F["dma_alloc_coherent<br/>一致性映射"] C -->|"已有内存<br/>临时DMA"| G["dma_map_single<br/>流式映射"] D -->|"否"| H["kmalloc<br/>物理连续"] D -->|"是"| I{"物理连续?"} I -->|"必须连续"| J["alloc_pages<br/>页级分配"] I -->|"不需要"| K["vmalloc<br/>虚拟连续"] H --> L(["分配完成"]) I --> L E --> L F --> L G --> L J --> L K --> L style A fill:#E8F5E9 style B fill:#FFF8E1 style C fill:#FFF8E1 style D fill:#FFF8E1 style I fill:#FFF8E1 style E fill:#E3F2FD style F fill:#E3F2FD style G fill:#E3F2FD style H fill:#E3F2FD style J fill:#E3F2FD style K fill:#E3F2FD style L fill:#E8F5E9

DMA映射选择流程

flowchart TB A(["DMA传输需求"]) --> B{"缓冲区生命周期?"} B -->|"与驱动同生命周期"| C["一致性映射<br/>dma_alloc_coherent"] B -->|"单次传输"| D{"缓冲区来源?"} D -->|"kmalloc/alloc_pages<br/>分配的内存"| E["流式映射<br/>dma_map_single"] D -->|"scatter-gather<br/>多段不连续内存"| F["scatter-gather映射<br/>dma_map_sg"] C --> G{"设置DMA掩码"} E --> G F --> G G --> H["dma_set_mask_and_coherent"] H --> I{"映射成功?"} I -->|"是"| J["启动DMA传输"] I -->|"否"| K["dma_mapping_error<br/>检查并处理"] K --> L["重试或返回错误"] J --> M{"传输完成?"} M -->|"一致性映射"| N["无需额外操作"] M -->|"流式映射"| O["dma_unmap_single<br/>或dma_sync_single_for_cpu"] M -->|"sg映射"| P["dma_unmap_sg"] N --> Q(["结束"]) O --> Q P --> Q L --> Q style A fill:#E8F5E9 style B fill:#FFF8E1 style D fill:#FFF8E1 style I fill:#FFF8E1 style M fill:#FFF8E1 style C fill:#E3F2FD style E fill:#E3F2FD style F fill:#E3F2FD style G fill:#E3F2FD style H fill:#E3F2FD style J fill:#E3F2FD style K fill:#FFEBEE style L fill:#FFEBEE style N fill:#E8F5E9 style O fill:#E3F2FD style P fill:#E3F2FD style Q fill:#E8F5E9

六、完整驱动示例

以下是一个使用一致性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。

版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页