dma_alloc_coherent和dma_map_single到底该选哪个,问题出在哪?

因为官方文档和源码分析都回避了最核心的问题:这两种方式到底有什么区别?什么时候该用哪个?


第一章:两种DMA方式的"基因"差异

1.1 从CPU视角看内存属性

在ARM64架构中,页表项有4种内存属性(在arch/arm64/include/asm/memory.h中定义):

/*
 * Memory types available.
 * 
 * 这4种属性决定了CPU访问内存时的行为
 * 直接决定了你的DMA驱动会不会"抽风"
 */
#define MT_NORMAL         0  // 正常缓存:CPU读写都经过cache,性能最好
#define MT_NORMAL_NC      1  // 非缓存:CPU读写直接访问内存,性能最差
#define MT_NORMAL_WT      2  // 写通:写直达内存,读可能缓存
#define MT_DEVICE_nGnRnE  3  // 设备内存:严格顺序,用于寄存器
​
// 在arch/arm64/mm/proc.S中设置MAIR寄存器
#define MAIR(attr, mt)    ((attr) << ((mt) * 8))
​
/*
 * 最终设置:
 * mair_el1 = 0x00ff044c00000000
 * 其中:
 * - MT_NORMAL:     0xff (正常缓存,读分配写回)
 * - MT_NORMAL_NC:  0x44 (非缓存)
 * - MT_DEVICE:     0x00 (设备内存)
 */

真相来了: dma_alloc_coherent()分配的内存,页表属性被设置为MT_NORMAL_NC——非缓存。这意味着CPU每次读写都直接操作物理内存。

1.2 用实验证明差异

写个小模块验证:

// test_dma_attrs.c - 测试两种DMA方式的内存属性
#include <linux/module.h>
#include <linux/dma-mapping.h>
#include <linux/slab.h>
​
static struct device *test_dev;
static void *coherent_virt;
static dma_addr_t coherent_dma;
static void *stream_virt;
static dma_addr_t stream_dma;
​
static int __init test_init(void)
{
    int ret;
    u32 *ptr;
    u32 val;
    unsigned long flags;
    
    // 创建虚拟设备
    test_dev = root_device_register("dma_test");
    if (IS_ERR(test_dev))
        return PTR_ERR(test_dev);
    
    // 设置DMA掩码(告诉内核设备能访问多少位地址)
    ret = dma_set_mask_and_coherent(test_dev, DMA_BIT_MASK(32));
    if (ret) {
        printk("Failed to set DMA mask\n");
        return ret;
    }
    
    // 方式1:一致性DMA
    coherent_virt = dma_alloc_coherent(test_dev, PAGE_SIZE, 
                                      &coherent_dma, GFP_KERNEL);
    printk("Coherent: virt=%p, dma=%pad\n", 
           coherent_virt, &coherent_dma);
    
    // 写入数据(会直接到内存,不经过cache)
    ptr = coherent_virt;
    *ptr = 0xdeadbeef;
    
    // 读取数据(直接从内存读)
    val = *ptr;
    printk("Coherent read: 0x%x\n", val);
    
    // 方式2:流式DMA(先分配普通内存)
    stream_virt = kmalloc(PAGE_SIZE, GFP_KERNEL);
    
    // 映射为流式DMA
    stream_dma = dma_map_single(test_dev, stream_virt, 
                                PAGE_SIZE, DMA_BIDIRECTIONAL);
    printk("Stream: virt=%p, dma=%pad\n", 
           stream_virt, &stream_dma);
    
    // 写入数据(会写入cache,还没到内存)
    ptr = stream_virt;
    *ptr = 0xdeadc0de;
    
    // 重要:告诉设备数据准备好了
    dma_sync_single_for_device(test_dev, stream_dma, 
                               PAGE_SIZE, DMA_TO_DEVICE);
    
    // 现在数据在内存中
    
    return 0;
}
​
// 编译后加载,查看/proc/iomem和printk输出

在ARM64平台上运行,会发现:

  • 一致性内存:虚拟地址对应的页属性是"非缓存",写入后立即在内存中可见

  • 流式内存:虚拟地址对应的页属性是"正常缓存",写入后在cache中,需要显式冲刷

1.3 为什么要有两种方式?——性能与便利的权衡

/*
 * 一致性DMA (dma_alloc_coherent)
 * 
 * 优点:
 * - 简单:不需要考虑cache同步
 * - 安全:永远不会有一致性问题
 * - 适合:描述符环、状态寄存器等频繁访问的小块内存
 * 
 * 缺点:
 * - 慢:CPU每次读写都是直接访问内存(慢几十到几百倍)
 * - 例如:在Cortex-A53上,L1 cache命中约2ns,DDR访问约100ns
 * 
 * 形象比喻:就像住酒店,每次都要下楼取东西,虽然麻烦但肯定准确
 */
​
/*
 * 流式DMA (dma_map_single)
 * 
 * 优点:
 * - 快:CPU可以享受cache加速
 * - 适合:大数据块传输(网络包、音视频数据)
 * 
 * 缺点:
 * - 复杂:必须在正确的时间点做sync
 * - 危险:sync错了数据就乱了
 * 
 * 形象比喻:就像在家,东西随手放桌上(cache),但要收拾(sync)后才能给客人
 */

第二章:需要注意的关键——缓存对齐

2.1 缓存行的"魔法"

这是90%的DMA驱动bug的来源。看代码:

// 错误示例 - 网上90%的示例代码都这么写
static int my_dma_transfer_bad(struct device *dev, char *buf, int len)
{
    dma_addr_t dma_handle;
    
    // 假设buf是kmalloc分配的,偏移可能是任意值
    dma_handle = dma_map_single(dev, buf, len, DMA_TO_DEVICE);
    if (dma_mapping_error(dev, dma_handle))
        return -EIO;
    
    // 启动DMA...
    start_dma(dma_handle, len);
    
    return 0;
}
​
// 问题在哪?看ARM64的cache维护代码:
static void __dma_map_area(void *virt, size_t size, int dir)
{
    unsigned long start = (unsigned long)virt;
    unsigned long end = start + size;
    
    // 关键!cache操作是以缓存行为单位的
    // ARM64通常有64字节的缓存行
    start = start & ~(cache_line_size() - 1);  // 向下对齐到缓存行边界
    end = ALIGN(end, cache_line_size());       // 向上对齐到缓存行边界
    
    // 现在操作的范围比原来的大!
    __flush_dcache_area(start, end - start);
}
​
/*
 * 悲剧发生了:
 * 假设buf = 0x1004, len = 8(两个int)
 * 缓存行64字节,对齐后:
 * start = 0x1000, end = 0x1040
 * 你只修改了8字节,但冲刷了64字节!
 * 如果这64字节里有其他重要的数据(比如其他线程的变量),
 * 它们也被写回内存了——可能覆盖了正确值
 */

2.2 正确做法:缓冲区对齐

// 正确示例 - DMA缓冲区必须缓存行对齐
#define DMA_BUFFER_SIZE  2048
​
struct my_device {
    void *rx_buf ____cacheline_aligned;  // 告诉编译器对齐到缓存行
    dma_addr_t rx_dma;
};
​
static int my_dma_init(struct my_device *dev)
{
    // 方法1:使用dma_alloc_coherent(自动对齐到缓存行)
    dev->rx_buf = dma_alloc_coherent(dev->dev, DMA_BUFFER_SIZE,
                                     &dev->rx_dma, GFP_KERNEL);
    
    // 方法2:kmalloc时指定对齐
    dev->rx_buf = kmalloc(DMA_BUFFER_SIZE, GFP_KERNEL | __GFP_DMA);
    // 但还要确保起始地址对齐
    BUG_ON((unsigned long)dev->rx_buf & (cache_line_size() - 1));
    
    return 0;
}

真相: 不是随便一块内存都能做DMA的,必须满足:

  1. 物理地址连续(对大多数DMA控制器)

  2. 对齐到缓存行(对cache一致性的要求)

  3. 在设备的DMA地址范围内(不能超过dma_mask)


第三章:会踩的坑——dma_sync的时机

3.1 所有权移交模型

DMA子系统其实实现了一个"所有权"模型:

/*
 * DMA内存的"所有权"在两个角色间切换:
 * 
 * CPU Owner              Device Owner
 *    │                         │
 *    │   dma_map_single/        │
 *    │   dma_sync_for_device    │
 *    │ ──────────────────────→ │
 *    │                         │
 *    │       DMA传输中          │
 *    │                         │
 *    │   dma_sync_for_cpu/      │
 *    │   dma_unmap_single       │
 *    │ ←────────────────────── │
 *    │                         │
 */
​
// 典型的数据包处理循环
static void my_packet_handler(struct my_device *dev)
{
    // 场景1:准备数据给设备
    prepare_packet(dev->tx_buf);           // CPU写缓冲区
    dma_sync_single_for_device(dev->dev,   // 移交所有权给设备
                               dev->tx_dma, PACKET_SIZE, 
                               DMA_TO_DEVICE);
    start_tx_dma(dev);                      // 设备读取数据
    
    // 场景2:处理接收的数据
    wait_for_rx_complete(dev);
    dma_sync_single_for_cpu(dev->dev,       // 收回所有权给CPU
                            dev->rx_dma, PACKET_SIZE,
                            DMA_FROM_DEVICE);
    process_packet(dev->rx_buf);             // CPU读取数据
    
    // 场景3:再次给设备用(循环缓冲区)
    // 注意:所有权必须完整切换,不能偷懒!
}

3.2 bug案例

这是调试一个网络驱动时遇到的bug:

// 有bug的代码 - 看起来没错,但在压力测试下会崩溃
static void net_tx_complete(void *data)
{
    struct my_netdev *ndev = data;
    
    // DMA完成了,可以重用缓冲区了
    dma_unmap_single(ndev->dev, ndev->tx_dma, 
                     ndev->tx_len, DMA_TO_DEVICE);
    
    // 准备下一个数据包
    ndev->tx_len = build_next_packet(ndev->tx_buf);
    ndev->tx_dma = dma_map_single(ndev->dev, ndev->tx_buf,
                                   ndev->tx_len, DMA_TO_DEVICE);
    start_tx(ndev);
}
​
// 崩溃时的错误信息:
// [  123.456789] DMA-API: device driver tries to map DMA buffer that has not been unmapped
// [  123.456790] ------------[ cut here ]------------
// [  123.456791] WARNING: CPU: 2 PID: 0 at lib/dma-debug.c:1088 check_unmap+0x4a0/0x9c8
​
/*
 * 问题分析:
 * dma_unmap_single和dma_map_single之间没有同步!
 * 如果build_next_packet修改了tx_buf的内容,
 * 这些修改可能在cache中,还没到内存。
 * 当dma_map_single后立即start_tx,
 * 设备可能读到旧数据。
 * 
 * 正确做法:
 */
static void net_tx_complete_fixed(void *data)
{
    struct my_netdev *ndev = data;
    
    // 1. 先同步CPU读取(虽然我们用DMA_TO_DEVICE,但可能缓存中还有数据)
    dma_sync_single_for_cpu(ndev->dev, ndev->tx_dma,
                            ndev->tx_len, DMA_TO_DEVICE);
    
    // 2. 构建新数据(修改缓存中的内容)
    ndev->tx_len = build_next_packet(ndev->tx_buf);
    
    // 3. 同步给设备
    dma_sync_single_for_device(ndev->dev, ndev->tx_dma,
                               ndev->tx_len, DMA_TO_DEVICE);
    
    // 4. 启动DMA
    start_tx(ndev);
}

第四章:DMA调试的"三板斧"

4.1 第一板斧:开启DMA_API_DEBUG

这是第一道防线:

# 编译内核时开启
make menuconfig
# Kernel hacking -> Memory Debugging -> DMA API Debug
# 或者直接在命令行加参数
dma_debug=1 dma_debug_entries=65536
​
# 运行时控制
echo 1 > /sys/module/dma_debug/parameters/debug_driver

开启后,任何DMA API的误用都会被记录:

// 常见错误及提示
[    1.234567] DMA-API: device driver maps memory from stack [addr=ffff800010203040]
// 解释:不能对栈内存做DMA,因为栈可能被回收
​
[    2.345678] DMA-API: device driver tries to free DMA memory it has not allocated
// 解释:dma_unmap了没有dma_map过的地址
​
[    3.456789] DMA-API: device driver maps memory with invalid direction
// 解释:方向参数错误
​
[    4.567890] DMA-API: device driver syncs DMA memory with different direction
// 解释:sync时用的方向和map时不一致

4.2 第二板斧:使用IOMMU/DMA重映射

如果硬件支持IOMMU(如ARM SMMU),可以强制启用:

// 在内核命令行添加
iommu.passthrough=0
arm-smmu.disable_bypass=0
​
/*
 * 好处:
 * 1. IOMMU会检查每次DMA访问,越界访问会被阻止(生成错误报告)
 * 2. 不需要物理连续内存(IOMMU帮你做映射)
 * 3. 可以通过iommu=debug查看详细日志
 */

4.3 第三板斧:写压力测试脚本

这是最有效的:模拟真实负载

#!/bin/bash
# dma_stress_test.sh
​
# 测试DMA驱动在高并发下的表现
​
DEVICE="/dev/my_uart"
ITERATIONS=10000
THREADS=4
​
# 测试1:多线程并发读写
for i in $(seq 1 $THREADS); do
    (
        for j in $(seq 1 $ITERATIONS); do
            # 写随机数据
            dd if=/dev/urandom of=$DEVICE bs=1024 count=1 2>/dev/null
            # 读回
            dd if=$DEVICE of=/dev/null bs=1024 count=1 2>/dev/null
        done
    ) &
done
wait
​
# 测试2:混合大小
for size in 64 128 256 512 1024 2048 4096; do
    echo "Testing size $size"
    dd if=/dev/zero of=$DEVICE bs=$size count=1000
done
​
# 测试3:边界测试
echo "Boundary test: start at odd address"
./test_odd_addr $DEVICE  # 测试非对齐缓冲区
​
echo "Boundary test: cross page boundary"
./test_cross_page $DEVICE  # 测试跨页缓冲区

第五章:资深工程师的DMA设计决策树

5.1 如何选择DMA方式?

/*
 * 当你站在代码面前,问自己这5个问题:
 */
​
// Q1: 缓冲区大小?
if (size < 64) {
    // 小数据,不值得DMA开销,用PIO
    goto use_pio;
}
​
// Q2: 访问频率?
if (access_frequency > 1000_per_second) {
    // 高频访问,必须用cacheable内存(流式DMA)
    goto streaming_dma;
}
​
// Q3: 是否需要同步?
if (need_hardware_coherency) {
    // 硬件保证一致性(如ACP端口),直接用dma_alloc_coherent
    goto coherent_dma;
}
​
// Q4: 设备是否有地址限制?
if (dev->dma_mask < DMA_BIT_MASK(32)) {
    // 只能访问低端内存,可能需要swiotlb
    check_swiotlb();
}
​
// Q5: 是否可能跨页?
if (size > PAGE_SIZE || !is_phys_contiguous) {
    // 用SG-DMA(scatter-gather)
    goto sg_dma;
}
​
/*
 * 最终决策表:
 * 
 * ┌─────────────────┬───────────────┬───────────────┐
 * │     场景        │   一致性DMA   │   流式DMA     │
 * ├─────────────────┼───────────────┼───────────────┤
 * │ 描述符环        │      √        │               │
 * │ 寄存器状态      │      √        │               │
 * │ 网络包缓冲      │               │      √        │
 * │ 音频缓冲        │               │      √        │
 * │ 视频帧缓冲      │      △        │      √        │
 * │ DMA缓冲区池     │      √        │               │
 * └─────────────────┴───────────────┴───────────────┘
 * △: 取决于是否频繁CPU访问
 */

5.2 真实项目中的配置示例

4G模块驱动:

// 4G模块的USB转串口驱动(简化版)
struct modem_dma {
    /* 
     * 控制通道:使用一致性DMA
     * 因为要频繁读写状态寄存器,且数据量小
     */
    void *ctrl_virt;
    dma_addr_t ctrl_dma;
    
    /* 
     * 数据通道:使用流式DMA + SG列表
     * 因为要传输大块数据,且可能跨页
     */
    struct scatterlist *data_sg;
    int sg_nents;
    
    /* 
     * 紧急缓冲区:当DMA失败时回退到PIO
     * 可靠性设计
     */
    u8 fallback_buf[2048] ____cacheline_aligned;
};
​
static int modem_probe(struct platform_device *pdev)
{
    struct modem_dma *mdma;
    
    // 1. 设置DMA掩码
    dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32));
    
    // 2. 分配一致性内存(控制通道)
    mdma->ctrl_virt = dma_alloc_coherent(dev, 1024,
                                         &mdma->ctrl_dma,
                                         GFP_KERNEL | GFP_DMA);
    
    // 3. 预分配SG列表(数据通道)
    mdma->sg_nents = 32;
    mdma->data_sg = kmalloc_array(mdma->sg_nents,
                                   sizeof(struct scatterlist),
                                   GFP_KERNEL);
    sg_init_table(mdma->data_sg, mdma->sg_nents);
    
    // 4. 注册DMA通道
    mdma->rx_chan = dma_request_slave_channel(dev, "rx");
    mdma->tx_chan = dma_request_slave_channel(dev, "tx");
    
    // 5. 设置DMA回调
    dma_async_issue_pending(mdma->rx_chan);
    
    return 0;
}

第六章:DMA与内存屏障

6.1 为什么需要屏障?

看这个例子:

// 描述符环的典型处理
struct desc_ring {
    u32 buffer_addr;
    u32 length;
    u32 flags;
    u32 status;  // 硬件写入
};
​
static void process_completed_descs(struct desc_ring *ring, int cnt)
{
    for (i = 0; i < cnt; i++) {
        // 这里有个隐藏的bug
        if (ring[i].status & DESC_COMPLETE) {
            // 处理完成的数据
            process_data(ring[i].buffer_addr);
        }
    }
}
​
/*
 * 问题:编译器和CPU可能重排指令!
 * 编译器可能为了优化,先读取status,再读取buffer_addr
 * 但硬件写入的顺序是:先写buffer_addr,再写status
 * 
 * 如果status先被读到(CPU推测执行),但buffer_addr还没写到内存
 * 就会处理错误的数据
 */

6.2 正确使用DMA屏障

static void process_completed_descs_fixed(struct desc_ring *ring, int cnt)
{
    u32 status;
    u32 addr;
    
    for (i = 0; i < cnt; i++) {
        // 确保顺序
        status = READ_ONCE(ring[i].status);  // 防止编译器优化
        
        if (status & DESC_COMPLETE) {
            /* 
             * DMA屏障:
             * dma_rmb() 确保在读取buffer_addr之前,
             * status的读取已经完成,且所有DMA写入对CPU可见
             */
            dma_rmb();
            
            addr = READ_ONCE(ring[i].buffer_addr);
            process_data(addr);
            
            // 清理状态
            WRITE_ONCE(ring[i].status, 0);
        }
    }
}
​
/*
 * ARM64中dma_rmb()的实现:
 * #define dma_rmb()   dsb(ishld)
 * 
 * dsb(ishld):数据同步屏障,确保所有之前的加载指令完成
 */

终章:

7.1 新手最容易犯的3个错误

  1. 忘记同步:在CPU和DMA之间切换所有权时忘记sync

  2. 错误的对齐:缓冲区不是缓存行对齐

  3. 忽略dma_mask:设备只能访问32位地址,却给了64位地址

7.2 调试DMA驱动的标准流程

# 阶段1:基本验证
insmod my_dma.ko
dmesg | grep DMA
cat /proc/iomem | grep my_dma
​
# 阶段2:开启调试
echo 1 > /sys/module/dma_debug/parameters/enable
echo 8 > /proc/sys/kernel/printk
​
# 阶段3:压力测试
./dma_stress_test.sh 2>&1 | tee test.log
​
# 阶段4:分析错误
grep "DMA-API" /var/log/kern.log
grep "Call Trace" /var/log/kern.log
​
# 阶段5:如果崩溃,分析vmcore
crash vmlinux vmcore
bt  # 查看调用栈
log # 查看最后的内核消息

7.3 最后

写DMA驱动就像走钢丝:

  • dma_alloc_coherent是安全网,但慢

  • dma_map_single是快速通道,但容易摔

  • dma_debug是你的安全带,永远别拆

注意:DMA驱动里,能跑通的代码不一定对,压力测试不崩溃才可能是对的。

  1. 先用dma_alloc_coherent把功能调通

  2. 确认所有逻辑正确后,再优化到流式DMA

  3. 每一步优化都要经过压力测试

  4. 永远保留回退路径

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐