LINUX DMA 解析

本文详细探讨了DMA(直接内存访问)的工作原理,包括DMA控制器硬件结构、软件请求与异步传输、DMA通道地址管理、关键操作函数、映射机制(一致和流式映射)、DMA池的使用,以及一个简单的DMA驱动程序示例。了解这些有助于提升系统性能和驱动程序设计效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

DMA是一种无需CPU的参与就可以让外设和系统内存之间进行双向数据传输的硬件机制。使用DMA可以使系统CPU从实际的I/O数据传输过程中摆脱出来,从而大大提高系统的吞吐率。DMA经常与硬件体系结构特别是外设的总线技术密切相关。

一、DMA控制器硬件结构

DMA允许外围设备和主内存之间直接传输 I/O 数据, DMA 依赖于系统。每一种体系结构DMA传输不同,编程接口也不同。

数据传输可以以两种方式触发:一种软件请求数据,另一种由硬件异步传输。

a -- 软件请求数据

调用的步骤可以概括如下(以read为例):

(1)在进程调用 read 时,驱动程序的方法分配一个 DMA 缓冲区,随后指示硬件传送它的数据。进程进入睡眠。

(2)硬件将数据写入 DMA 缓冲区并在完成时产生一个中断。

(3)中断处理程序获得输入数据,应答中断,最后唤醒进程,该进程现在可以读取数据了。

b -- 由硬件异步传输

在 DMA 被异步使用时发生的。以数据采集设备为例:

(1)硬件发出中断来通知新的数据已经到达。

(2)中断处理程序分配一个DMA缓冲区。

(3)外围设备将数据写入缓冲区,然后在完成时发出另一个中断。

(4)处理程序利用DMA分发新的数据,唤醒任何相关进程。

网卡传输也是如此,网卡有一个循环缓冲区(通常叫做 DMA 环形缓冲区)建立在与处理器共享的内存中。每一个输入数据包被放置在环形缓冲区中下一个可用缓冲区,并且发出中断。然后驱动程序将网络数据包传给内核的其它部分处理,并在环形缓冲区中放置一个新的 DMA 缓冲区。

驱动程序在初始化时分配DMA缓冲区,并使用它们直到停止运行。

二、DMA通道使用的地址

DMA通道用dma_chan结构数组表示,这个结构在kernel/dma.c中,列出如下:

struct dma_chan {
    int lock;
    const char *device_id;
};

static struct dma_chan dma_chan_busy[MAX_DMA_CHANNELS] = {
      [4] = {1,"cascade"}  
};

如果dma_chan_busy[n].lock != 0表示忙,DMA0保留为DRAM更新用,DMA4用作级联。DMA 缓冲区的主要问题是,当它大于一页时,它必须占据物理内存中的连续页。

由于DMA需要连续的内存,因而在引导时分配内存或者为缓冲区保留物理 RAM 的顶部。在引导时给内核传递一个"mem="参数可以保留 RAM 的顶部。例如,如果系统有 32MB 内存,参数"mem=31M"阻止内核使用最顶部的一兆字节。稍后,模块可以使用下面的代码来访问这些保留的内存:

dmabuf = ioremap( 0x1F00000 /* 31M */, 0x100000 /* 1M */);

分配 DMA 空间的方法,代码调用 kmalloc(GFP_ATOMIC) 直到失败为止,然后它等待内核释放若干页面,接下来再一次进行分配。最终会发现由连续页面组成的DMA 缓冲区的出现。

一个使用 DMA 的设备驱动程序通常会与连接到接口总线上的硬件通讯,这些硬件使用物理地址,而程序代码使用虚拟地址。基于 DMA 的硬件使用总线地址而不是物理地址,有时,接口总线是通过将 I/O 地址映射到不同物理地址的桥接电路连接的。甚至某些系统有一个页面映射方案,能够使任意页面在外围总线上表现为连续的。

当驱动程序需要向一个 I/O 设备(例如扩展板或者DMA控制器)发送地址信息时,必须使用 virt_to_bus 转换,在接受到来自连接到总线上硬件的地址信息时,必须使用 bus_to_virt 了。

三、DMA操作函数

写一个DMA驱动的主要工作包括:DMA通道申请、DMA中断申请、控制寄存器设置、挂入DMA等待队列、清除DMA中断、释放DMA通道

因为 DMA 控制器是一个系统级的资源,所以内核协助处理这一资源。内核使用 DMA 注册表为 DMA 通道提供了请求/释放机制,并且提供了一组函数在 DMA 控制器中配置通道信息。

以下具体分析关键函数(linux/arch/arm/mach-s3c2410/dma.c)

int s3c2410_request_dma(const char *device_id,dmach_t channel,
            dma_callback_t write_cb,dma_callback_t read_cb) (s3c2410_dma_queue_buffer);
/*
函数描述 申请其通道的DMA资源,填充s3c2410_dma_t 数据结构内容 ,申请DMA中断。
输入参数 device_id DMA 设备名,chananel 通道号。
write_cb DMA 写操作完成的回调函数,read_cb DMA读操作完成的回调函数
输出参数 若channel 通道已使用,出错返回。否则返回0
*/

int s3c5410_dma_queue_buffer(dmach_t channel,void *buf_id,dma_addr_t data, int size, int write)(s3c2410_dma_stop);
/*
   函数描述 这是DMA操作最关键的函数,它完成了一系列动作,分配并初始化一个DMA内核缓冲区控制结构
   输入参数 channel 通道号:buf_id ,缓冲区标识
   dma_addr_t data DMA数据缓冲区起始物理地址;size DMA数据缓冲区大小;write 是写还是读操作
   输出参数操作成功,返回0;否则返回错误号;
*/

int s3c2410_dma_stop(dmach_t channel)
//函数描述 停止DMA操作

int s3c2410_dma_flush_all(dmach_t channel)
//函数描述 得到DMA通道所申请的所有内存资源

void s3c2410_free_dma(dmach_t channel)
//函数描述 释放DMA通道

四、DMA映射

一个DMA映射就是分配一个 DMA 缓冲区并为该缓冲区生成一个能够被设备访问的地址的组合操作。一般情况下,简单地调用函数virt_to_bus 就设备总线上的地址,但有些硬件映射寄存器也被设置在总线硬件中。映射寄存器(mapping register)是一个类似于外围设备的虚拟内存等价物。在使用这些寄存器的系统上,外围设备有一个相对较小的、专用的地址区段,可以在此区段执行 DMA。通过映射寄存器,这些地址被重映射到系统 RAM。映射寄存器具有一些好的特性,包括使分散的页面在设备地址空间看起来是连续的。但不是所有的体系结构都有映射寄存器,特别地,PC 平台没有映射寄存器。

在某些情况下,为设备设置有用的地址也意味着需要构造一个反弹(bounce)缓冲区。例如,当驱动程序试图在一个不能被外围设备访问的地址(一个高端内存地址)上执行 DMA 时,反弹缓冲区被创建。然后,按照需要,数据被复制到反弹缓冲区,或者从反弹缓冲区复制。

根据 DMA 缓冲区期望保留的时间长短,PCI 代码区分两种类型的 DMA 映射:

a -- 一致 DMA 映射

它们存在于驱动程序的生命周期内。一个被一致映射的缓冲区必须同时可被 CPU 和外围设备访问,这个缓冲区被处理器写时,可立即被设备读取而没有cache效应,反之亦然,使用函数pci_alloc_consistent建立一致映射。

b -- 流式 DMA映射

流式DMA映射是为单个操作进行的设置。它映射处理器虚拟空间的一块地址,以致它能被设备访问。应尽可能使用流式映射,而不是一致映射。这是因为在支持一致映射的系统上,每个 DMA 映射会使用总线上一个或多个映射寄存器。具有较长生命周期的一致映射,会独占这些寄存器很长时间――即使它们没有被使用。使用函数dma_map_single建立流式映射。

1、建立一致 DMA 映射

函数pci_alloc_consistent处理缓冲区的分配和映射,函数分析如下(在include/asm-generic/pci-dma-compat.h中):

 结构dma_coherent_mem定义了DMA一致性映射的内存的地址、大小和标识等。结构dma_coherent_mem列出如下(在arch/i386/kernel/pci-dma.c中):

 函数dma_alloc_coherent分配size字节的区域的一致内存,得到的dma_handle是指向分配的区域的地址指针,这个地址作为区域的物理基地址。dma_handle是与总线一样的位宽的无符号整数。 函数dma_alloc_coherent分析如下(在arch/i386/kernel/pci-dma.c中):

 

当不再需要缓冲区时(通常在模块卸载时),应该调用函数 pci_free_consitent 将它返还给系统。

2、建立流式 DMA 映射

在流式 DMA 映射的操作中,缓冲区传送方向应匹配于映射时给定的方向值。缓冲区被映射后,它就属于设备而不再属于处理器了。在缓冲区调用函数pci_unmap_single撤销映射之前,驱动程序不应该触及其内容。

在缓冲区为 DMA 映射时,内核必须确保缓冲区中所有的数据已经被实际写到内存。可能有些数据还会保留在处理器的高速缓冲存储器中,因此必须显式刷新。在刷新之后,由处理器写入缓冲区的数据对设备来说也许是不可见的。

如果欲映射的缓冲区位于设备不能访问的内存区段时,某些体系结构仅仅会操作失败,而其它的体系结构会创建一个反弹缓冲区。反弹缓冲区是被设备访问的独立内存区域,反弹缓冲区复制原始缓冲区的内容。

函数pci_map_single映射单个用于传送的缓冲区,返回值是可以传递给设备的总线地址,如果出错的话就为 NULL。一旦传送完成,应该使用函数pci_unmap_single 删除映射。其中,参数direction为传输的方向,取值如下:

PCI_DMA_TODEVICE 数据被发送到设备。

PCI_DMA_FROMDEVICE如果数据将发送到 CPU。

PCI_DMA_BIDIRECTIONAL数据进行两个方向的移动。

PCI_DMA_NONE 这个符号只是为帮助调试而提供。

函数pci_map_single分析如下(在arch/i386/kernel/pci-dma.c中)

 

函数dma_map_single映射一块处理器虚拟内存,这块虚拟内存能被设备访问,返回内存的物理地址,函数dma_map_single分析如下(在include/asm-i386/dma-mapping.h中):

 

3、分散/集中映射

分散/集中映射是流式 DMA 映射的一个特例。它将几个缓冲区集中到一起进行一次映射,并在一个 DMA 操作中传送所有数据。这些分散的缓冲区由分散表结构scatterlist来描述,多个分散的缓冲区的分散表结构组成缓冲区的struct scatterlist数组。

分散表结构列出如下(在include/asm-i386/scatterlist.h):

 每一个缓冲区的地址和长度会被存储在 struct scatterlist 项中,但在不同的体系结构中它们在结构中的位置是不同的。下面的两个宏定义来解决平台移植性问题,这些宏定义应该在一个pci_map_sg 被调用后使用:

 函数pci_map_sg完成分散/集中映射,其返回值是要传送的 DMA 缓冲区数;它可能会小于 nents(也就是传入的分散表项的数量),因为可能有的缓冲区地址上是相邻的。一旦传输完成,分散/集中映射通过调用函数pci_unmap_sg 来撤销映射。 函数pci_map_sg分析如下(在include/asm-generic/pci-dma-compat.h中):

 

五、DMA池

许多驱动程序需要又多又小的一致映射内存区域给DMA描述子或I/O缓存buffer,这使用DMA池比用dma_alloc_coherent分配的一页或多页内存区域好,DMA池用函数dma_pool_create创建,用函数dma_pool_alloc从DMA池中分配一块一致内存,用函数dmp_pool_free放内存回到DMA池中,使用函数dma_pool_destory释放DMA池的资源。

结构dma_pool是DMA池描述结构,列出如下:

 

函数dma_pool_create给DMA创建一个一致内存块池,其参数name是DMA池的名字,用于诊断用,参数dev是将做DMA的设备,参数size是DMA池里的块的大小,参数align是块的对齐要求,是2的幂,参数allocation返回没有跨越边界的块数(或0)。

函数dma_pool_create返回创建的带有要求字符串的DMA池,若创建失败返回null。对被给的DMA池,函数dma_pool_alloc被用来分配内存,这些内存都是一致DMA映射,可被设备访问,且没有使用缓存刷新机制,因为对齐原因,分配的块的实际尺寸比请求的大。如果分配非0的内存,从函数dma_pool_alloc返回的对象将不跨越size边界(如不跨越4K字节边界)。这对在个体的DMA传输上有地址限制的设备来说是有利的。

函数dma_pool_create分析如下(在drivers/base/dmapool.c中):

 

 函数dma_pool_alloc从DMA池中分配一块一致内存,其参数pool是将产生块的DMA池,参数mem_flags是GFP_*位掩码,参数handle是指向块的DMA地址,函数dma_pool_alloc返回当前没用的块的内核虚拟地址,并通过handle给出它的DMA地址,如果内存块不能被分配,返回null。

函数dma_pool_alloc包裹了dma_alloc_coherent页分配器,这样小块更容易被总线的主控制器使用。这可能共享slab分配器的内容。

函数dma_pool_alloc分析如下(在drivers/base/dmapool.c中):

 

六、一个简单的使用DMA 例子

示例:下面是一个简单的使用DMA进行传输的驱动程序,它是一个假想的设备,只列出DMA相关的部分来说明驱动程序中如何使用DMA的。

函数dad_transfer是设置DMA对内存buffer的传输操作函数,它使用流式映射将buffer的虚拟地址转换到物理地址,设置好DMA控制器,然后开始传输数据。

 函数dad_interrupt是中断处理函数,当DMA传输完时,调用这个中断函数来取消buffer上的DMA映射,从而让内核程序可以访问这个buffer。

 函数dad_open打开设备,此时应申请中断号及DMA通道

 在与open 相对应的 close 函数中应该释放DMA及中断号。

 函数dad_dma_prepare初始化DMA控制器,设置DMA控制器的寄存器的值,为 DMA 传输作准备。

 函数dad_dma_isdone用来检查 DMA 传输是否成功结束。

 

<think>好的,用户想了解dma_alloc_coherent()的详细内容。首先,我得回忆一下这个函数的作用和用法。记得它在Linux内核中用于分配DMA可用的内存,确保物理地址连续,对吧?可能需要先介绍DMA的基本概念,为什么需要这样的函数。 然后,用户可能需要知道函数的参数和返回值。参数通常包括设备指针、大小、方向标志之类的。返回值是虚拟地址和DMA地址,这里要注意虚拟地址在CPU端使用,而DMA地址是设备访问的物理地址。 接着,应该解释内存一致性的问题,比如为什么需要coherent内存,和流式DMA的区别。可能还要提到cache一致性的机制,比如硬件自动处理cache,这样设备和CPU看到的内存内容是一致的。 还有,使用场景很重要。用户可能想知道什么时候该用这个函数,比如设备需要长时间访问的内存,或者小缓冲区的情况。同时,注意事项也不能少,比如内存大小限制、性能问题,以及释放内存的正确方法。 最后,示例代码会帮助用户理解如何实际调用这个函数,包括错误处理的部分。可能还要对比dma_alloc_coherent和dma_map_single的区别,这样用户能更清楚在不同情况下选择哪个函数。 需要确保回答结构清晰,分点说明,同时用中文表达,避免技术术语使用错误。还要检查LaTeX格式是否符合要求,行内公式用$...$,独立公式用$$...$$,并且语法正确。可能用户是驱动开发者或者嵌入式系统工程师,所以需要详细但不过于冗长,重点放在实际应用和常见问题上。</think>$$ \text{Linux DMA内存分配核心函数详解} $$ 一、功能定义 `dma_alloc_coherent()`是Linux内核中用于分配一致性DMA映射的核心函数,主要解决以下问题: 1. 分配物理地址连续的内存区域 2. 保证CPU和设备访问的内存一致性 3. 自动处理cache同步问题 二、函数原型 ```c void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag); ``` 参数说明: - $dev$:关联的设备结构体指针 - $size$:请求分配的内存大小(字节) - $dma_handle$:输出参数,存储分配的DMA总线地址 - $flag$:内存分配标志(如GFP_KERNEL, GFP_ATOMIC) 三、内存特性 $$ \text{物理地址对齐} = \begin{cases} dev->coherent_dma_mask & \text{设备有DMA掩码} \\ dma_get_required_mask(dev) & \text{默认情况} \end{cases} $$ 四、一致性机制 1. 写直达(Write-through)缓存策略 2. 硬件自动维护cache一致性 3. 适用场景: - 设备频繁访问的小缓冲区 - 需要长期保持映射的内存区域 - 无法预测访问时机的DMA操作 五、典型使用场景 ```c // 网卡驱动接收缓冲区分配示例 struct device *ndev = &netdev->dev; dma_addr_t dma_addr; void *vaddr = dma_alloc_coherent(ndev, BUF_SIZE, &dma_addr, GFP_KERNEL); if (!vaddr) { // 错误处理 } ``` 六、注意事项 1. 内存限制: - 单次分配最大尺寸通常为4MB - 频繁分配可能引发内存碎片 2. 性能优化: - 小尺寸分配优先考虑dma_pool - 大块内存建议使用分散/聚集列表 3. 资源释放必须使用配对的: ```c dma_free_coherent(dev, size, cpu_addr, dma_handle); ``` 七、与流式DMA对比 | 特性 | 一致性DMA | 流式DMA | |---------------------|-------------------|--------------------| | 映射方式 | 长期固定 | 临时映射 | | 缓存维护 | 自动 | 手动同步 | | 适用场景 | 频繁访问 | 单次传输 | | 性能开销 | 较高 | 较低 | 八、常见问题解决方案 1. 分配失败检查: - 确认dma_mask设置正确 - 检查CMA(Contiguous Memory Allocator)配置 2. 缓存一致性问题: ```bash # 通过/proc/meminfo检查CMA使用情况 $ grep Cma /proc/meminfo ``` 3. 调试技巧: - 启用CONFIG_DMA_API_DEBUG - 使用dma-debug工具 九、内核实现演进 从Linux 2.6.12开始引入统一的DAPI: 1. 早期版本使用`pci_alloc_consistent()` 2. 现代内核采用设备拓扑感知分配 3. 支持IOMMU的透明重映射 十、性能优化建议 $$ \text{最佳分配尺寸} = 2^n \times \text{页大小} $$ 例如在x86架构上: $$ 32KB = 8 \times 4KB \quad \text{(更易获得连续物理内存)} $$
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Psyduck_zhi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值