Linux内核4.19 DMA缓存处理源码深度巡礼
·
序章:DMA缓存处理的"灵魂三问"
在Linux内核中,DMA缓存处理是连接CPU虚拟世界与硬件物理世界的桥梁。
-
一致性去哪了? —— CPU的缓存(cache)让数据"漂移",硬件DMA直击物理内存,两者如何达成一致?
-
映射是什么? —— 如何让硬件"看见"CPU分配的内存?
-
同步怎么做? —— 什么时候该冲刷缓存,什么时候该失效缓存?
第一章: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验证开发者使用是否正确。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)