关于dma_alloc_coherent的用法

本文详细介绍了Linux内核中DMA映射的原理,包括一致性映射、流式映射、DMA池和PCI双重地址周期映射,以及ISA设备的DMA使用案例。重点讲解了dma_alloc_coherent函数及其作用,以及如何通过dma_pool_create和相关函数进行内存管理和映射操作。

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

目录

概述

DMA映射

建立一致性DMA映射

DMA池

建立流式DMA映射

单页流式映射

分散/聚集映射

PCI双重地址周期映射 DAC

简单的PCI DMA例子

ISA设备DMA


概述

DMA的操作是需要物理地址的,但是在linux内核中使用的都是虚拟地址,如果想要用DMA对一段内存进行操作,如何得到这一段内存的物理地址和虚拟地址的映射呢?dma_alloc_coherent这个函数实现了这种机制。

1、函数原型: void *dma_alloc_coherent( struct device *dev, size_t size,dma_addr_t *dma_handle,gfp_t gfp);

2、调用

A = dma_alloc_writecombine(B,C,D,GFP_KERNEL);

含义:

A: 内存的虚拟起始地址,在内核要用此地址来操作所分配的内存

B: struct device指针,可以平台初始化里指定,主要是dma_mask之类,可参考framebuffer

C: 实际分配大小,传入dma_map_size即可

D: 返回的内存物理地址,dma就可以用。

所以,A和D是一一对应的,只不过,A是虚拟地址,而D是物理地址。对任意一个操作都将改变缓冲区内容。

此函数的理解是,调用此函数将会分配一段内存,D将返回这段内存的实际物理地址供DMA来使用,A将是D对应的虚拟地址供操作系统调用,对A和D的的任意一个进行操作,都会改变这段内存缓冲区的内容。

DMA映射

一个DMA映射是要分配的DMA缓冲区与为该缓冲区生成的、设备可访问地址的组合。

DMA映射建立了一个新的结构类型---dma_addr_t来表示总线地址。

dma_addr_t类型的变量对驱动程序是不透明的,唯一允许的操作是将它们传递给DMA支持例程以及设备本身。

根据DMA缓冲区期望保留的时间长短,PCI代码有两种DMA映射:
1)一致性映射
2)流式DMA映射(推荐)

建立一致性DMA映射

void *dma_alloc_coherent(struct device *dev,size_t size, dma_addr_t *dma_handle,int flag);
该函数处理了缓冲区的分配和映射。

前两个参数是device结构和所需缓冲区的大小。
函数在两处返回结果:
1) 函数的返回值时缓冲区的内核虚拟地址,可以被驱动程序使用。
2) 相关的总线地址则保存在dma_handle中。

向系统返回缓冲区
void  dma_free_coherent(struct device *dev,size_t size, void *vaddr,dma_addr_t dma_handle);

DMA池

DMA池是一个生成小型、一致性DMA映射的机制。
调用dma_alloc_coherent函数获得的映射,可能其最小大小为单个页。
如果设备需要的DMA区域比这还小,就要用DMA池了。

<linux/dmapool.h>

struct dma_pool *dma_pool_create(const char *name,struct device *dev, size_t size,size_t align, size_t allocation);
name是DMA池的名字,dev是device结构,size是从该池中分配的缓冲区大小,align是该池分配操作所必须遵守的硬件对齐原则。

销毁DMA池
void dma_pool_destroy(struct dma_pool *pool);

DMA池分配内存
void *dma_pool_alloc(struct dma_pool *pool,int mem_flags, dma_addr_t *handle);
释放内存
void dma_pool_free(struct dma_pool *pool,void *vaddr,dma_addr_t addr);


建立流式DMA映射

流式映射具有比一致性映射更为复杂的接口。
这些映射希望能与已经由驱动程序分配的缓冲区协同工作,
因而不得不处理那些不是它们选择的地址。

当建立流式映射时,必须告诉内核数据流动的方向。
枚举类型dma_data_direction:
DMA_TO_DEVICE          数据发送到设备(如write系统调用)
DMA_FROM_DEVICE    数据被发送到CPU
DMA_BIDIRECTIONAL   数据可双向移动
DMA_NONE                    出于调试目的。

当只有一个缓冲区要被传输的时候,使用dma_map_single函数映射它:
dma_addr_t dma_map_single(struct device *dev,void *buffer,size_t size, enum dma_data_direction direction);
返回值是总线地址,可以把它传递给设备。

当传输完毕后,使用dma_unmap_single函数删除映射:
void dma_unmap_single(struct device *dev,dma_addr_t dma_addr,size_t size, enum dma_data_direction direction);

流式DMA映射的几条原则:
*  缓冲区只能用于这样的传送,即其传送方向匹配于映射时给定的方向。
*  一旦缓冲区被映射,它将属于设备,而不是处理器。 直到缓冲区被撤销映射前,驱动程序不能以任何方式访问其中的内容。
* 在DMA处于活动期间内,不能撤销对缓冲区映射,否则会严重破坏系统的稳定性。

驱动程序需要不经过撤销映射就访问流式DMA缓冲区的内容,有如下调用:
void dma_sync_single_for_cpu(struct device *dev,dma_handle_t bus_addr, size_t size,enum dma_data_direction direction);
将缓冲区所有权交还给设备:
void dma_sync_single_for_device(struct device *dev,dma_handle_t bus_addr, size_t size,enum dma_data_direction direction);


单页流式映射

有时候,要为page结构指针指向的缓冲区建立映射,比如为get_user_pages获得的用户空间缓冲区。

dma_addr_t dma_map_page(struct device *dev,struct page *page, unsigned long offset,size_t size, enum dma_data_direction direction);
void dma_unmap_page(struct device *dev,dma_addr_t dma_address, size_t size,enum dma_data_direction direction);


分散/聚集映射

有几个缓冲区,它们需要与设备双向传输数据。

可以简单地依次映射每一个缓冲区并且执行请求的操作, 但是一次映射整个缓冲区表还是很有利的。

映射分散表的第一步是建立并填充一个描述被传输缓冲区的scatterlist结构的数组。
<linux/scatterlist.h>

scatterlist结构的成员:
struct page *page;
unsigned int length;
unsigned int offset;

映射
int dma_map_sg(struct device *dev,struct scatterlist *sg,int nents, enum dma_data_direction direction);
解除
void dma_unmap_sg(struct device *dev,struct scatterlsit *list, int nents,enum dma_data_direction direction);


PCI双重地址周期映射 DAC

通用DMA支持层使用32位总线地址,然而PCI总线还支持64位地址模式,即双重地址周期(DAC)。
<linux/pci.h>
通用DMA层并不支持该模式。

要使用PCI总线的DAC,必须设置一个单独的DMA掩码:
int pci_dac_set_dma_mask(struct pci_dev *pdev,u64 mask);

建立映射
dma64_addr_t pci_dac_page_to_dma(struct pci_dev *pdev,struct page *page,unsigned long offset,int direction);


简单的PCI DMA例子

这里提供了一个PCI设备的DMA例子dad(DMA Acquisition Device)的一部分,说明如何使用DMA映射:

int dad_transfer(struct dad_dev *dev,int write,void *buffer,size_t count)
{
 	dma_addr_t bus_addr;
	dev->dma_dir = (write ? DMA_TO_DEVICE : DMA_FROM_DEVICE);
	dev->dma_size = count;
	/*映射DMA需要的缓冲区*/
 	bus_addr = dma_map_single(&dev->pci_dev->dev,buffer,count,dev->dma_dir);
	writeb(dev->registers.command,DAD_CMD_DISABLEDMA);
 	writeb(dev->registers.command,write ? DAD_CMD_WR : DAD_CMD_RD);
 	writeb(dev->registers.addr,cpu_to_le32(bus_addr)); /*设置*/
 	writeb(dev->registers.len,cpu_to_le32(count));
	/*开始操作*/
 	writeb(dev->registers.command,DAD_CMD_ENABLEDMA);
	return 0; 
}

该函数映射了准备进行传输的缓冲区并且启动设备操作。

另一半工作必须在中断服务例程中完成,如:

void dad_interrupt(int irq,void *dev_id,struct pt_regs *regs)
{
 	struct dad_dev *dev = (struct dad_dev *)dev_id;
	/* 确定中断是由对应的设备发来的*/
	dma_unmap_single(dev->pci_dev->dev,dev->dma_addr, dev->dma_size,dev->dma_dir);
	/* 释放之后,才能访问缓冲区,把它拷贝给用户 */
 	...
}

ISA设备DMA

ISA总线允许两种DMA传输:本地DMA和ISA总线控制DMA。

只讨论本地(native)DMA。***********************非常重要!!!!!

本地DMA使用主板上的标准DMA控制器电路来驱动ISA总线上的信号线。

本地DMA,要关注三种实体:

*8237 DMA控制器(DMAC)
控制器保存了有关DMA传输的信息,如方向、内存地址、传输数据量大小等。
还包含了一个跟踪传送状态的计数器。
当控制器接收到一个DMA请求信号时,它将获得总线控制权并驱动信号线,这样设备就能读写数据了。

*外围设备
当设备准备传送数据时,必须激活DMA请求信号。
DMAC负责管理实际的传输工作;当控制器选通设备后,硬件设备就可以顺序地读/写总线上的数据。
当传输结束时,设备通常会产生一个中断。

*设备驱动程序
设备驱动程序完成的工作很少,它只是负责提供DMA控制器的方向、总线地址、传输量的大小等。
它还与外围设备通信,做好传输数据的准备,当DMA传输完毕后,响应中断。
在PC中使用的早期DMA控制器能够管理四个“通道”,每个通道都与一套DMA寄存器相关联。
DMA控制器是系统资源,因此,内核协助处理这一资源。
内核使用DMA注册表为DMA通道提供了请求/释放机制,并且提供了一组函数在DMA控制器中配置通道信息。

 

<think>嗯,用户让我分析一下dma_alloc_attrs这个函数的代码。首先,我需要先理解这个函数的作用是什么。DMA(直接内存访问)是设备直接访问内存的机制,而dma_alloc_attrs应该就是用来分配DMA可用的内存的函数。用户可能是在学习内核中的DMA相关代码,或者是遇到了相关问题需要调试,所以想了解这个函数的内部实现细节。 看一下函数参数,struct device *dev应该是指向设备的指针,size是分配的大小,dma_handle是返回的DMA地址,flag是内存分配的标志,attrs是属性。返回值是CPU可以访问的虚拟地址。 接下来,函数内部首先通过get_dma_ops获取设备的DMA映射操作结构体,这说明不同的设备可能有不同的DMA操作实现,比如有的可能使用直接映射,有的需要IOMMU或者SMMU的支持。 然后有一个WARN_ON_ONCE检查dev->coherent_dma_mask是否设置,这是因为DMA分配需要知道设备的DMA地址掩码,以确保分配的内存地址在设备可寻址的范围内。如果没有设置,内核会发出警告。 接下来尝试从设备的coherent内存池中分配,如果成功就直接返回。这部分可能是处理一些设备特定的连续内存分配,比如预分配的内存池,这样可以提高效率或者满足某些硬件的特殊需求。 然后调整分配的flag,清除掉与DMA区域相关的标志,比如__GFP_DMA、__GFP_DMA32和__GFP_HIGHMEM,这是因为DMA分配的区域可能由底层的DMA操作决定,不需要上层指定,或者这些标志已经被包含在设备的DMA掩码中。 接下来判断ops是否是直接映射,如果是,调用dma_direct_alloc,否则检查ops->alloc是否存在,存在的话调用它。否则返回NULL。这说明DMA分配有两种主要路径:直接映射和使用特定于设备的分配方法。直接映射可能用于没有IOMMU的情况,而特定方法可能用于有IOMMU或需要复杂映射的情况。 最后调用debug_dma_alloc_coherent进行调试,记录分配的信息,方便调试DMA相关的问题。 用户可能需要了解这个函数如何选择不同的DMA分配方式,或者为什么在某些情况下分配失败。此外,可能对内存属性attrs的作用、coherent内存池的使用,或者DMA操作的抽象层结构感兴趣。 用户可能的实际需求是解决DMA内存分配的问题,比如分配失败、性能问题,或者理解不同设备之间的差异。他们可能是在开发内核模块时遇到问题,需要深入理解这个函数的逻辑,或者在进行驱动调试时,需要知道如何追踪DMA分配的过程。 需要进一步解释各个部分的作用,比如get_dma_ops是如何工作的,dma_is_direct的条件判断,以及不同路径下的分配函数具体做了什么。同时,提到EXPORT_SYMBOL说明这个函数是内核导出的,可以被模块使用,这也是重要的信息。 另外,attrs参数可能控制一些DMA属性,比如是否强制使用一致性的内存,或者是否使用非缓存内存等,这些属性会影响分配的行为和性能。 总结来说,用户希望了解这个函数的工作流程,各个步骤的作用,以及背后的设计考虑,比如抽象层的作用、不同路径的选择逻辑,以及调试和错误处理的机制。需要将这些内容用清晰的结构组织起来,帮助用户全面理解这个函数的实现。</think>以下是`dma_alloc_attrs`函数的深度分析(基于Linux内核实现): --- ### **一、函数定位** 这是DMA一致性内存分配的核心接口,用于获取**CPU可访问的虚拟地址**和**设备可用的DMA地址**,具有以下特性: - 保证分配的内存符合`dev->coherent_dma_mask` - 内存具有缓存一致性(Coherent DMA Mapping) - 支持多平台抽象(通过`dma_map_ops`) --- ### **二、参数解析** | 参数 | 类型 | 作用 | |------|------|------| | dev | struct device* | 目标设备指针(决定DMA寻址能力) | | size | size_t | 请求分配的内存大小 | | dma_handle | dma_addr_t* | 输出参数,存储DMA总线地址 | | flag | gfp_t | 内存分配标志(GFP_* flags) | | attrs | unsigned long | DMA属性(如DMA_ATTR_NON_CONSISTENT) | --- ### **三、代码逻辑分解** #### **1. 操作集获取** ```c const struct dma_map_ops *ops = get_dma_ops(dev); ``` - **作用**:通过设备获取平台特定的DMA操作集 - **实现细节**: - x86:返回`dma_direct_ops`(直接物理地址映射) - ARM:返回`iommu_dma_ops`(通过IOMMU映射) - 可通过`dev->dma_ops`覆盖默认实现 #### **2. 一致性掩码校验** ```c WARN_ON_ONCE(!dev->coherent_dma_mask); ``` - **设计意图**:确保设备已设置合理的DMA地址掩码 - **典型值**: ```c dev->coherent_dma_mask = DMA_BIT_MASK(32); // 32位地址设备 ``` #### **3. 设备相干内存优先分配** ```c if (dma_alloc_from_dev_coherent(dev, size, dma_handle, &cpu_addr)) return cpu_addr; ``` - **优化路径**:尝试从设备专属的`dev->coherent_mem`分配 - **适用场景**: - 频繁的小内存分配 - 需要避免完整DMA API开销的场景 #### **4. 内存区域标志净化** ```c flag &= ~(__GFP_DMA | __GFP_DMA32 | __GFP_HIGHMEM); ``` - **原理**:DMA区域选择由底层实现决定,例如: - 32位设备强制使用DMA_ZONE - IOMMU存在时可使用任意区域 #### **5. 分配路径选择** ```c if (dma_is_direct(ops)) cpu_addr = dma_direct_alloc(...); else if (ops->alloc) cpu_addr = ops->alloc(...); ``` - **直接映射路径**(dma_direct_alloc): ```c // 典型实现: page = alloc_pages(flag, order); *dma_handle = phys_to_dma(dev, page_to_phys(page)); ``` - **IOMMU路径**: ```c // 通过IOMMU创建映射: iommu_dma_alloc(dev, size, dma_handle, flag, attrs); ``` #### **6. 调试支持** ```c debug_dma_alloc_coherent(dev, size, *dma_handle, cpu_addr); ``` - **调试手段**: - 通过`CONFIG_DMA_API_DEBUG`启用 - 记录分配历史到环形缓冲区 - 可检测内存泄漏和越界访问 --- ### **四、关键数据结构** ```c struct dma_map_ops { void* (*alloc)(struct device *, size_t, dma_addr_t *, gfp_t, unsigned long); void (*free)(struct device *, size_t, void *, dma_addr_t, unsigned long); // ... 其他映射方法 }; ``` --- ### **五、典型调用栈** ``` 驱动层调用 ↓ dma_alloc_coherent() ↓ dma_alloc_attrs() ├─→ dma_direct_alloc() // 无IOMMU情况 └─→ iommu_dma_alloc() // 有IOMMU情况 ↓ swiotlb_alloc() // 当物理内存不连续时 ``` --- ### **六、性能优化点** 1. **预分配策略**: ```c // 启动时预分配大块内存 dma_declare_coherent_memory(dev, phys_addr, device_addr, size); ``` 2. **分配标志建议**: - `GFP_ATOMIC`:用于中断上下文分配 - `GFP_KERNEL`:常规异步分配 3. **IOVA优化**: - 使用`DMA_ATTR_ALLOC_SINGLE_PAGES`避免地址连续性要求 --- ### **七、常见问题排查** 1. **分配失败**: - 检查`dma_alloc_from_dev_coherent`返回值 - 确认`dev->coherent_dma_mask`是否足够大 2. **性能下降**: - 使用`ftrace`跟踪`dma_alloc_attrs`耗时 - 检查是否频繁触发SWIOTLB回退 3. **地址对齐异常**: - 确认设备要求的对齐方式(通过`dev->dma_pfn_offset`) 该函数体现了Linux内核DMA子系统的两大设计哲学:**硬件抽象**和**分层架构**,开发者可通过替换`dma_map_ops`实现自定义DMA管理策略。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值