序章:DMA缓存处理的"灵魂三问"

在Linux内核中,DMA缓存处理是连接CPU虚拟世界与硬件物理世界的桥梁。

  1. 一致性去哪了? —— CPU的缓存(cache)让数据"漂移",硬件DMA直击物理内存,两者如何达成一致?

  2. 映射是什么? —— 如何让硬件"看见"CPU分配的内存?

  3. 同步怎么做? —— 什么时候该冲刷缓存,什么时候该失效缓存?


第一章:DMA API的"总司令部" —— dma-mapping.h

1.1 架构无关的顶层设计

// 文件:include/linux/dma-mapping.h
// 这是DMA API的"宪法",定义了对外的所有接口
​
#ifndef _LINUX_DMA_MAPPING_H
#define _LINUX_DMA_MAPPING_H
​
#include <linux/types.h>
#include <linux/mm.h>
#include <linux/scatterlist.h>
#include <linux/dma-direction.h>
​
/**
 * 统一DMA操作接口 - 每个架构必须实现这个虚函数表
 * 
 * 形象比喻:这是DMA操作的"驱动层",每个架构(ARM64/x86/RISC-V)都要
 * 提供自己的实现,就像每个USB设备要有自己的驱动一样
 */
struct dma_map_ops {
    // 分配一致性DMA内存(永不需同步,天生一致)
    void*   (*alloc)(struct device *dev, size_t size,
                     dma_addr_t *dma_handle, gfp_t gfp,
                     unsigned long attrs);
    
    // 释放一致性DMA内存
    void    (*free)(struct device *dev, size_t size,
                    void *vaddr, dma_addr_t dma_handle,
                    unsigned long attrs);
    
    // 映射单个缓冲区用于流式DMA
    dma_addr_t (*map_page)(struct device *dev, struct page *page,
                           unsigned long offset, size_t size,
                           enum dma_data_direction dir,
                           unsigned long attrs);
    
    // 取消映射单个缓冲区
    void    (*unmap_page)(struct device *dev, dma_addr_t dma_handle,
                          size_t size, enum dma_data_direction dir,
                          unsigned long attrs);
    
    // 映射散列表(用于SG-DMA,一次性传输多个不连续的内存块)
    int     (*map_sg)(struct device *dev, struct scatterlist *sg,
                      int nents, enum dma_data_direction dir,
                      unsigned long attrs);
    
    // 取消映射散列表
    void    (*unmap_sg)(struct device *dev, struct scatterlist *sg,
                        int nents, enum dma_data_direction dir,
                        unsigned long attrs);
    
    // 同步操作:让CPU和DMA看到一致的数据
    void    (*sync_single_for_cpu)(struct device *dev,
                                   dma_addr_t dma_handle, size_t size,
                                   enum dma_data_direction dir);
    void    (*sync_single_for_device)(struct device *dev,
                                      dma_addr_t dma_handle, size_t size,
                                      enum dma_data_direction dir);
    
    // ... 其他方法
};
​
// 核心的dma_addr_t类型:这是硬件能直接看到的"物理地址"
// 但它可能经过IOMMU转换,所以不是直接的物理地址
typedef u64 dma_addr_t;
​
#endif /* _LINUX_DMA_MAPPING_H */

开发者视角:这个头文件定义了DMA世界的"宪法",每个架构必须遵守并实现自己的dma_map_ops。就像每个国家有自己的法律但都遵守国际公约。

1.2 内存方向语义 —— 给数据流"贴标签"

// 文件:include/linux/dma-direction.h
// DMA方向定义 - 这是程序员最容易出错的地方
​
#ifndef _LINUX_DMA_DIRECTION_H
#define _LINUX_DMA_DIRECTION_H
​
/**
 * enum dma_data_direction - DMA内存传输方向
 * 
 * 形象比喻:这就像快递单上的"寄件人"和"收件人"
 * 搞错方向,数据就会"寄丢"
 */
enum dma_data_direction {
    DMA_BIDIRECTIONAL = 0,  // 双向:CPU<->设备,双方都会修改数据
    DMA_TO_DEVICE = 1,      // 单向:CPU→设备,CPU写,设备读
    DMA_FROM_DEVICE = 2,    // 单向:设备→CPU,设备写,CPU读
    DMA_NONE = 3,           // 无传输
};
​
/**
 * 为什么必须明确方向?
 * 
 * 以ARM架构为例:
 * - DMA_TO_DEVICE: 需要CPU写回缓存,使数据到达内存
 * - DMA_FROM_DEVICE: 需要CPU失效缓存,避免读到旧数据
 * - DMA_BIDIRECTIONAL: 最保守,但性能最差(来回冲刷)
 */
#endif

第二章:ARM64架构的DMA实现 —— 底层"施工队"

2.1 ARM64的DMA操作集

// 文件:arch/arm64/mm/dma-mapping.c
// ARM64架构对dma_map_ops的具体实现
​
#include <linux/dma-iommu.h>
#include <linux/dma-contiguous.h>
#include <linux/swiotlb.h>
​
/**
 * ARM64的DMA操作有两种模式:
 * 1. 直通模式(无IOMMU):直接操作物理地址
 * 2. IOMMU模式:通过SMMU进行地址转换
 * 
 * 内核通过late_initcall动态选择
 */
​
/**
 * swiotlb(软件IO TLB)- DMA的"备胎"
 * 当设备无法访问某些内存区域时,用swiotlb做反弹缓冲区
 * 
 * 形象比喻:swiotlb就像机场的"摆渡车"
 * 当飞机的停机位太远(设备无法访问内存),
 * 就用摆渡车把乘客运过去
 */
static void *arm64_swiotlb_alloc_coherent(struct device *dev, size_t size,
                                          dma_addr_t *dma_handle, gfp_t flags,
                                          unsigned long attrs)
{
    // 如果设备有约束(比如只能访问低32位地址)
    if (IS_ENABLED(CONFIG_ZONE_DMA32) &&
        dev->coherent_dma_mask <= DMA_BIT_MASK(32))
        flags |= GFP_DMA32;
    
    // 实际调用通用swiotlb分配器
    return swiotlb_alloc_coherent(dev, size, dma_handle, flags);
}
​
/**
 * 一致性DMA内存分配 - 最常用的接口
 * 
 * 调用链:dma_alloc_coherent() → arm64_swiotlb_alloc_coherent()
 */
static void *arm64_dma_alloc(struct device *dev, size_t size,
                             dma_addr_t *dma_handle, gfp_t gfp,
                             unsigned long attrs)
{
    // 如果开启了CMA(连续内存分配器),优先使用CMA
    if (IS_ENABLED(CONFIG_DMA_CMA) && dev_get_cma_area(dev)) {
        return cma_alloc_coherent(dev, size, dma_handle, gfp, attrs);
    }
    
    // 否则用swiotlb或通用分配器
    return swiotlb_alloc_coherent(dev, size, dma_handle, gfp);
}
​
/**
 * 映射单个页面的流式DMA
 * 
 * 这是最精细的控制:只映射一页的一部分
 */
static dma_addr_t __swiotlb_map_page(struct device *dev, struct page *page,
                                     unsigned long offset, size_t size,
                                     enum dma_data_direction dir,
                                     unsigned long attrs)
{
    dma_addr_t dev_addr;
    
    /**
     * 物理地址转DMA地址
     * 关键步骤:可能经过IOMMU转换
     */
    dev_addr = swiotlb_map_page(dev, page, offset, size, dir, attrs);
    
    /**
     * 根据方向进行cache维护
     * - DMA_TO_DEVICE: 写回缓存,确保数据到内存
     * - DMA_FROM_DEVICE: 失效缓存,确保CPU读到最新数据
     */
    if (!(attrs & DMA_ATTR_SKIP_CPU_SYNC)) {
        __dma_map_area(phys_to_virt(dma_to_phys(dev, dev_addr) + offset),
                      size, dir);
    }
    
    return dev_addr;
}
​
/**
 * ARM64的cache维护函数
 * 这是最底层的硬件操作
 */
static void __dma_map_area(void *virt, size_t size,
                           enum dma_data_direction dir)
{
    // 根据方向决定cache操作
    if (dir == DMA_TO_DEVICE)
        // 写回:CPU修改过的数据→内存
        dcache_clean_inval_poc((unsigned long)virt,
                               (unsigned long)virt + size);
    else if (dir == DMA_FROM_DEVICE)
        // 失效:丢弃过时的缓存行
        dcache_inval_poc((unsigned long)virt,
                        (unsigned long)virt + size);
    else if (dir == DMA_BIDIRECTIONAL)
        // 先写回再失效,最安全但最慢
        dcache_clean_inval_poc((unsigned long)virt,
                               (unsigned long)virt + size);
    /* DMA_NONE 不做任何事 */
}

2.2 关键数据结构:page和scatterlist

// 文件:include/linux/mm_types.h
// struct page - 物理内存的最小管理单元
​
struct page {
    unsigned long flags;      // 页面状态:保留、脏、活跃...
    atomic_t _refcount;       // 引用计数
    
    union {
        struct {               // 普通页面使用
            void *virtual;      // 内核虚拟地址(若已映射)
            unsigned long private;
        };
        
        struct {               // DMA相关
            dma_addr_t dma_addr;// DMA地址缓存
        };
    };
    
    struct address_space *mapping;  // 所属的地址空间
    pgoff_t index;                   // 在文件中的偏移
};
​
// 文件:include/linux/scatterlist.h
// struct scatterlist - 描述不连续的内存块
​
struct scatterlist {
    unsigned long   page_link;   // 指向struct page的低位存标志
    unsigned int    offset;      // 在页内的偏移
    unsigned int    length;      // 长度
    dma_addr_t      dma_address; // DMA地址(映射后填充)
    
#ifdef CONFIG_NEED_SG_DMA_LENGTH
    unsigned int    dma_length;  // DMA映射后的长度(可能被IOMMU改变)
#endif
};
​
/**
 * 使用scatterlist进行SG-DMA的典型模式:
 * 
 * struct scatterlist sg[10];
 * 
 * // 初始化SG列表
 * sg_init_table(sg, 10);
 * sg_set_page(&sg[0], page1, 1024, 0);
 * sg_set_page(&sg[1], page2, 2048, 0);
 * 
 * // 映射
 * nents = dma_map_sg(dev, sg, 10, DMA_BIDIRECTIONAL);
 * 
 * // 遍历映射后的SG(可能被合并)
 * for_each_sg(sg, s, nents, i) {
 *     // 使用s->dma_address 和 s->dma_length
 *     // 注意:映射后可能比原始条目少(因为合并)
 * }
 */

第三章:一致性DMA内存 —— "天生一对"

3.1 dma_alloc_coherent() 源码追踪

// 文件:include/linux/dma-mapping.h
// 对外接口包装
​
static inline void *dma_alloc_coherent(struct device *dev, size_t size,
                                       dma_addr_t *dma_handle, gfp_t flag)
{
    // 调用架构特定的实现
    struct dma_map_ops *ops = get_dma_ops(dev);
    void *memory;
    
    BUG_ON(!ops);
    
    // 带属性标记的分配(可扩展,如DMA_ATTR_NO_WARN)
    memory = ops->alloc(dev, size, dma_handle, flag,
                        DMA_ATTR_NON_CONSISTENT ? 0 : 
                        DMA_ATTR_WRITE_COMBINE);
    
    /**
     * 关键特性:返回的内存是"一致性"的
     * 意味着:
     * 1. 虚拟地址和DMA地址都有效
     * 2. CPU和DMA访问看到的是同一份数据(没有缓存不一致问题)
     * 3. 通常是写合并(Write Combine)或非缓存(non-cached)属性
     */
    return memory;
}
​
// 文件:drivers/base/dma-coherent.c
// 一致性DMA内存池管理
​
struct dma_coherent_mem {
    void        *virt_base;      // 虚拟地址基址
    dma_addr_t  device_base;     // DMA地址基址
    unsigned long   size;        // 大小(页数)
    int         flags;
    unsigned long   *bitmap;     // 分配位图
};
​
/**
 * 预留一致性DMA区域
 * 常用于设备专用内存(如视频解码器的显存)
 */
int dma_declare_coherent_memory(struct device *dev, phys_addr_t phys_addr,
                                dma_addr_t device_addr, size_t size, int flags)
{
    struct dma_coherent_mem *mem;
    void __iomem *mem_base = NULL;
    int pages = size >> PAGE_SHIFT;
    
    // 分配元数据
    mem = kzalloc(sizeof(struct dma_coherent_mem), GFP_KERNEL);
    mem->bitmap = kzalloc(BITS_TO_LONGS(pages) * sizeof(long), GFP_KERNEL);
    
    // 建立映射
    mem->virt_base = ioremap(phys_addr, size);
    mem->device_base = device_addr;
    mem->size = pages;
    
    // 关联到设备
    dev->dma_mem = mem;
    
    return 0;
}

3.2 一致性内存的硬件视角

// 文件:arch/arm64/include/asm/cachetype.h
// 缓存类型定义
​
#define CTR_DMINLINE_SHIFT      16    // 最小DMA缓存行大小
#define CTR_IMINLINE_SHIFT      14
​
// 文件:arch/arm64/mm/proc.S
// 页表属性设置
​
/*
 * 设置页表项的内存属性
 * 
 * MT_NORMAL        - 普通缓存(CPU性能最好)
 * MT_DEVICE_nGnRnE - 设备内存(无缓存,严格顺序)
 * MT_NORMAL_NC     - 非缓存(一致性DMA使用)
 * MT_NORMAL_WT     - 写通(介于两者之间)
 */
ENTRY(__cpu_setup)
    ...
    // 设置MAIR(内存属性间接寄存器)
    ldr     x5, =MAIR(0x00, MT_NORMAL) |          \
                 MAIR(0x04, MT_DEVICE_nGnRnE) |   \
                 MAIR(0x44, MT_NORMAL_NC) |       \
                 MAIR(0xff, MT_NORMAL_WT)
    msr     mair_el1, x5
    ...
ENDPROC(__cpu_setup)

第四章:流式DMA —— "若即若离"

4.1 dma_map_single() 的完整旅程

// 文件:include/linux/dma-mapping.h
// 流式映射入口
​
static inline dma_addr_t dma_map_single_attrs(struct device *dev,
                                              void *ptr, size_t size,
                                              enum dma_data_direction dir,
                                              unsigned long attrs)
{
    struct dma_map_ops *ops = get_dma_ops(dev);
    unsigned long offset = (unsigned long)ptr & ~PAGE_MASK;
    struct page *page = virt_to_page(ptr);
    
    // 调用底层map_page
    return ops->map_page(dev, page, offset, size, dir, attrs);
}
​
// 文件:arch/arm64/mm/dma-mapping.c
// ARM64的具体实现
​
static dma_addr_t __swiotlb_map_page(struct device *dev, struct page *page,
                                     unsigned long offset, size_t size,
                                     enum dma_data_direction dir,
                                     unsigned long attrs)
{
    dma_addr_t dev_addr;
    phys_addr_t phys = page_to_phys(page) + offset;
    
    /**
     * 关键步骤1:检查是否需要使用SWIOTLB
     * 
     * 什么时候需要?
     * - 设备DMA地址受限(如只能访问32位)
     * - 内存区域不可直接DMA访问(如高位内存)
     */
    if (swiotlb_map(dev, &phys, size, dir, attrs)) {
        // 使用SWIOTLB作为反弹缓冲区
        dev_addr = swiotlb_map_page(dev, page, offset, size, dir, attrs);
    } else {
        // 直接映射
        dev_addr = phys_to_dma(dev, phys);
    }
    
    /**
     * 关键步骤2:CPU cache维护
     * 
     * 为什么需要?
     * - 对于DMA_TO_DEVICE:确保CPU修改的数据写回内存
     * - 对于DMA_FROM_DEVICE:确保CPU不会读到过时的缓存数据
     */
    if (!(attrs & DMA_ATTR_SKIP_CPU_SYNC)) {
        void *vaddr = page_address(page) + offset;
        
        if (dir == DMA_TO_DEVICE || dir == DMA_BIDIRECTIONAL)
            // 写回并失效:确保数据到达内存
            __flush_dcache_area(vaddr, size);
        else if (dir == DMA_FROM_DEVICE)
            // 只失效:为硬件写入做准备
            __inval_dcache_area(vaddr, size);
    }
    
    return dev_addr;
}
​
/**
 * 同步操作的时机
 * 
 * 典型场景:
 * 1. 映射后:CPU停止访问,交给DMA
 * 2. DMA完成后:CPU重新访问前需要同步
 */
​
static void __swiotlb_sync_single_for_cpu(struct device *dev,
                                          dma_addr_t dma_handle, size_t size,
                                          enum dma_data_direction dir)
{
    phys_addr_t phys = dma_to_phys(dev, dma_handle);
    void *vaddr = phys_to_virt(phys);
    
    // CPU准备读取设备写入的数据
    if (dir == DMA_FROM_DEVICE || dir == DMA_BIDIRECTIONAL)
        __inval_dcache_area(vaddr, size);  // 失效缓存,确保读到新数据
}
​
static void __swiotlb_sync_single_for_device(struct device *dev,
                                             dma_addr_t dma_handle, size_t size,
                                             enum dma_data_direction dir)
{
    phys_addr_t phys = dma_to_phys(dev, dma_handle);
    void *vaddr = phys_to_virt(phys);
    
    // 设备准备读取CPU修改的数据
    if (dir == DMA_TO_DEVICE || dir == DMA_BIDIRECTIONAL)
        __flush_dcache_area(vaddr, size);  // 写回缓存,确保设备读到最新数据
}

4.2 一个完整的数据传输生命周期

// 这是驱动开发者的视角,展示所有API的正确使用顺序
​
struct my_device_data {
    void *cpu_addr;          // CPU虚拟地址
    dma_addr_t dma_addr;      // DMA地址
    size_t size;              // 缓冲区大小
};
​
/**
 * 数据传输的完整生命周期
 * 
 * 形象比喻:这就像借书的过程
 * 1. 借出前:整理书架(flush cache)
 * 2. 借出中:读者看书(DMA传输)
 * 3. 归还后:检查有没有标记(invalidate cache)
 */
int my_dma_transfer(struct my_device_data *data, size_t count)
{
    // 阶段1:准备数据给设备(CPU→DMA)
    memcpy(data->cpu_addr, user_buffer, count);
    
    // 告诉设备:数据准备好了
    dma_sync_single_for_device(dev, data->dma_addr, count, DMA_TO_DEVICE);
    
    // 启动DMA传输
    writel(data->dma_addr, dev->regs + DMA_ADDR);
    writel(count, dev->regs + DMA_COUNT);
    writel(1, dev->regs + DMA_START);
    
    // 等待DMA完成(中断或轮询)
    wait_for_completion(&dev->dma_done);
    
    // 阶段2:读取设备写入的数据(DMA→CPU)
    dma_sync_single_for_cpu(dev, data->dma_addr, count, DMA_FROM_DEVICE);
    
    // 现在可以安全读取数据
    process_data(data->cpu_addr, count);
    
    return 0;
}

第五章:SWIOTLB —— DMA的"安全网"

5.1 SWIOTLB的工作原理

// 文件:include/linux/swiotlb.h
// 软件IO TLB的声明
​
extern phys_addr_t swiotlb_tbl_phys_addr;  // SWIOTLB物理地址
extern unsigned long swiotlb_size;          // SWIOTLB大小
​
// 文件:lib/swiotlb.c
// 软件IO TLB的实现
​
/**
 * struct io_tlb_slot - SWIOTLB槽位
 * 
 * SWIOTLB是一块预留的连续物理内存
 * 它充当"摆渡车",将数据在不同地址空间之间搬运
 */
struct io_tlb_slot {
    phys_addr_t orig_addr;      // 原始数据地址
    size_t      alloc_size;      // 分配大小
    unsigned int list;           // 链表
};
​
static struct io_tlb_slot *io_tlb_list;
static phys_addr_t io_tlb_start; // SWIOTLB起始物理地址
static phys_addr_t io_tlb_end;   // SWIOTLB结束地址
​
/**
 * SWIOTLB初始化
 * 
 * 在启动早期分配一块连续内存
 */
void __init swiotlb_init(int verbose)
{
    size_t bytes = PAGE_ALIGN(default_nslabs << IO_TLB_SHIFT);
    void *vstart;
    
    // 分配物理连续内存
    vstart = memblock_virt_alloc_low(bytes, 0);
    
    // 记录物理地址
    io_tlb_start = virt_to_phys(vstart);
    io_tlb_end = io_tlb_start + bytes;
    
    // 初始化槽位链表
    io_tlb_list = memblock_virt_alloc(io_tlb_nslabs * sizeof(*io_tlb_list), 0);
    
    for (i = 0; i < io_tlb_nslabs; i++) {
        io_tlb_list[i].list = i + 1;
    }
    
    pr_info("software IO TLB: mapped [mem %pap-%pap] (%lu MB)\n",
            &io_tlb_start, &io_tlb_end, bytes >> 20);
}
​
/**
 * 使用SWIOTLB进行映射
 * 
 * 当设备不能直接访问原始内存时,
 * 把数据拷贝到SWIOTLB缓冲区,让设备访问这里
 */
dma_addr_t swiotlb_map_page(struct device *dev, struct page *page,
                            unsigned long offset, size_t size,
                            enum dma_data_direction dir,
                            unsigned long attrs)
{
    phys_addr_t phys = page_to_phys(page) + offset;
    phys_addr_t tlb_addr;
    void *orig = phys_to_virt(phys);
    void *tlb_vaddr;
    
    // 在SWIOTLB中分配一个槽位
    tlb_addr = swiotlb_tbl_map_single(dev, phys, size, size, dir, attrs);
    
    // 拷贝数据到SWIOTLB缓冲区
    tlb_vaddr = phys_to_virt(tlb_addr);
    
    if (dir == DMA_TO_DEVICE || dir == DMA_BIDIRECTIONAL) {
        // CPU→设备:拷贝数据到SWIOTLB
        memcpy(tlb_vaddr, orig, size);
    }
    // 对于DMA_FROM_DEVICE,数据会被设备写入SWIOTLB
    
    return phys_to_dma(dev, tlb_addr);
}
​
/**
 * SWIOTLB取消映射
 * 
 * 把数据从SWIOTLB拷贝回原缓冲区
 */
void swiotlb_unmap_page(struct device *dev, dma_addr_t dev_addr,
                        size_t size, enum dma_data_direction dir,
                        unsigned long attrs)
{
    phys_addr_t tlb_addr = dma_to_phys(dev, dev_addr);
    phys_addr_t orig_addr;
    
    // 获取原始地址
    orig_addr = swiotlb_tbl_unmap_single(dev, tlb_addr, size, dir);
    
    if (orig_addr != INVALID_PHYS_ADDR) {
        if (dir == DMA_FROM_DEVICE || dir == DMA_BIDIRECTIONAL) {
            // 设备→CPU:将数据从SWIOTLB拷贝回原缓冲区
            void *orig_vaddr = phys_to_virt(orig_addr);
            void *tlb_vaddr = phys_to_virt(tlb_addr);
            memcpy(orig_vaddr, tlb_vaddr, size);
        }
    }
}

5.2 SWIOTLB的性能影响

/**
 * SWIOTLB的代价
 * 
 * 优点:让有寻址限制的设备也能工作
 * 缺点:
 * 1. 额外的内存拷贝
 * 2. 占用宝贵的低端内存
 * 3. 限制并发DMA操作数量(槽位有限)
 * 
 * 所以,SWIOTLB应该作为备胎,不是首选
 */

第六章:DMA调试与诊断 —— 火眼金睛

6.1 DMA API调试

// 文件:kernel/dma/debug.c
// DMA API调试器 - 追查DMA使用错误的利器
​
/**
 * struct dma_debug_entry - 记录每次DMA映射
 * 
 * 内核维护一个哈希表,记录所有活动的DMA映射
 * 这就像DMA操作的"黑匣子"
 */
struct dma_debug_entry {
    struct list_head list;
    struct device    *dev;
    int              type;
    phys_addr_t      paddr;      // 物理地址
    dma_addr_t       dev_addr;    // DMA地址
    size_t           size;        // 大小
    int              direction;   // 方向
    int              sg_mapped;   // 是否SG映射
};
​
/**
 * 检查双重释放
 */
void check_unmap(struct dma_debug_entry *ref)
{
    struct dma_debug_entry *entry;
    
    // 在哈希表中查找
    entry = dma_entry_find(ref);
    if (!entry) {
        // 错误:试图取消一个未映射的地址
        err_printk(ref->dev, "DMA-API: device driver tries to free DMA "
                   "memory it has not allocated [device address=%pad] [size=%zu]\n",
                   &ref->dev_addr, ref->size);
    }
}
​
/**
 * 检查越界访问
 */
void check_for_stack(struct device *dev, void *addr)
{
    if (object_is_on_stack(addr)) {
        // 错误:不能对栈内存进行DMA操作
        WARN(1, "DMA-API: device driver maps memory from stack [addr=%p]\n", addr);
    }
}
​
/**
 * 检查DMA方向是否正确
 */
void check_sync(struct device *dev, dma_addr_t addr, size_t size, int direction)
{
    struct dma_debug_entry ref = {
        .dev = dev,
        .dev_addr = addr,
        .size = size,
        .direction = direction,
    };
    
    // 验证这个映射是否存在且方向正确
    dma_debug_entry_find(&ref);
}
​
// 启用方式:内核参数 "dma_debug=1"

6.2 使用动态调试监控DMA

// 运行时开启DMA调试
echo "file drivers/iommu/* +p" > /sys/kernel/debug/dynamic_debug/control
echo "file arch/arm64/mm/dma-mapping.c +p" > /sys/kernel/debug/dynamic_debug/control
​
// 查看DMA映射统计
cat /sys/kernel/debug/dma-api/dump
​
// 输出示例:
// DMA-API: mapping size 4096, device ffff800010203040, address ffff00001000
// DMA-API: unmap size 4096, device ffff800010203040, address ffff00001000

第七章:实战案例 —— UART驱动的DMA优化

7.1 原始PIO方式的局限

// 文件:drivers/tty/serial/amba-pl011.c (ARM PrimeCell UART)
// 这是一个真实的UART驱动,展示DMA的使用
​
/**
 * PIO模式 - 每字节中断
 * 
 * 缺点:115200bps时,每秒约11520次中断
 * 每个中断都要保存恢复上下文,CPU占用高
 */
static irqreturn_t pl011_int(int irq, void *dev_id)
{
    struct uart_amba_port *uap = dev_id;
    
    // 处理每个字符...
    if (uap->im & UART011_RXIM) {
        // 读一个字节
        unsigned int ch = readw(uap->port.membase + UART01x_DR);
        // 放入FIFO...
    }
    
    return IRQ_HANDLED;
}

7.2 DMA方式的优化实现

/**
 * DMA模式 - 批量传输
 * 
 * 将接收到的数据批量送入内存,大大减少中断次数
 */
struct pl011_dma_data {
    struct dma_chan *rxchan;           // RX DMA通道
    struct dma_chan *txchan;           // TX DMA通道
    struct dma_async_tx_descriptor *rxdesc;  // RX描述符
    dma_cookie_t rx_cookie;
    void *rx_buf_virt;                  // RX缓冲区虚拟地址
    dma_addr_t rx_buf_dma;               // RX缓冲区DMA地址
    struct scatterlist rx_sg;            // RX散列表
};
​
/**
 * 启动DMA接收
 */
static int pl011_dma_rx_start(struct pl011_dma_data *d)
{
    struct scatterlist *sg = &d->rx_sg;
    
    // 准备散列表条目
    sg_init_one(sg, d->rx_buf_virt, UART_DMA_BUFFER_SIZE);
    
    // 映射DMA缓冲区
    sg_dma_address(sg) = d->rx_buf_dma;
    sg_dma_len(sg) = UART_DMA_BUFFER_SIZE;
    
    // 准备DMA描述符
    d->rxdesc = dmaengine_prep_slave_sg(d->rxchan, sg, 1,
                                        DMA_DEV_TO_MEM,
                                        DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
    
    // 设置回调
    d->rxdesc->callback = pl011_dma_rx_callback;
    d->rxdesc->callback_param = uap;
    
    // 提交DMA传输
    d->rx_cookie = dmaengine_submit(d->rxdesc);
    dma_async_issue_pending(d->rxchan);
    
    return 0;
}
​
/**
 * DMA完成回调
 * 
 * 这个函数每接收UART_DMA_BUFFER_SIZE字节调用一次
 * 而不是每字节一次
 */
static void pl011_dma_rx_callback(void *data)
{
    struct uart_amba_port *uap = data;
    struct pl011_dma_data *d = uap->dma;
    
    // 获取传输的字节数
    enum dma_status status;
    dma_addr_t addr;
    size_t done;
    
    status = dmaengine_tx_status(d->rxchan, d->rx_cookie, &addr);
    done = sg_dma_len(&d->rx_sg) - addr;
    
    // 处理接收到的数据
    tty_insert_flip_string(&uap->port, d->rx_buf_virt, done);
    tty_flip_buffer_push(&uap->port);
    
    // 重新启动DMA接收
    pl011_dma_rx_start(d);
}

终章:DMA架构的哲学思考

Linux内核DMA子系统的设计哲学:

8.1 抽象与性能的平衡

高性能路径 (直通模式)          安全路径 (SWIOTLB)
    │                               │
    │ 直接物理地址                   │ 反弹缓冲区
    │ 零拷贝                        │ 额外拷贝
    │ 无开销                        │ 有开销但兼容
    ▼                               ▼
[硬件支持]                     [软件模拟]

8.2 缓存一致性的三种策略

/**
 * 策略1:完全一致性
 * 使用dma_alloc_coherent分配非缓存内存
 * 优点:无需手动同步
 * 缺点:CPU访问慢
 */
​
/**
 * 策略2:流式同步
 * 使用dma_map_single并正确同步
 * 优点:性能好(使用缓存)
 * 缺点:需要精心设计同步点
 */
​
/**
 * 策略3:IOMMU SVA(共享虚拟地址)
 * 最新技术,CPU和DMA共享页表
 * 优点:无需同步,性能接近硬件极限
 * 缺点:需要硬件支持
 */

8.3 开发者DMA设计清单

当需要设计一个使用DMA的驱动时,请检查:

  • 设备支持的最大DMA地址?用dma_set_mask_and_coherent()设置
  • 缓冲区对齐要求?可能是cache line大小
  • 方向是否正确?DMA_TO_DEVICE vs DMA_FROM_DEVICE
  • 同步点是否完整?每次所有权转换都要同步
  • 错误处理路径?DMA失败时能否回退到PIO
  • 内存屏障?DMA完成后是否需要dma_rmb()
  • 调试信息?开启DMA_API_DEBUG验证使用是否正确

结语:Linux内核的DMA子系统是20多年工程智慧的结晶,它既要处理千奇百怪的硬件限制,又要提供简洁易用的API,还要追求极致的性能。理解这些源码,在约束下做优雅的系统设计。

最后一句忠告:DMA是把双刃剑——用得好,性能飞升;用得不好,数据损坏,系统崩溃。永远记得在关键路径上打开DMA_API_DEBUG验证开发者使用是否正确。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐