通用设备的动态DMA映射
by JHJ([email protected])
本文描述DMA API。更详细的介绍请参看Documentation/DMA-API-HOWTO.txt。
API分为两部分,第一部分描述API,第二部分描述可以支持非一致性内存机器的扩展API。你应该使用第一部分所描述的API,除非你知道你的驱动必须要支持非一致性平台。
第一部分 DMA API
为了可以引用DMA API,你必须 #include <linux/dma-mapping.h>
1-1 使用大块DMA一致性缓冲区(dma-coherent buffers)
void *
dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flag)
一致性内存:设备对一块内存进行写操作,处理器可以立即进行读操作,而无需担心处理器高速缓存(cache)的影响。同样的,处理器对一块内存进行些操作,设备可以立即进行读操作。(在告诉设备读内存时,你可能需要确定刷新处理器的写缓存。)
此函数申请一段大小为size字节的一致性内存,返回两个参数。一个是dma_handle,它可以用作这段内存的物理地址。 另一个是指向被分配内存的指针(处理器的虚拟地址)。
注意:由于在某些平台上,使用一致性内存代价很高,比如最小的分配长度为一个页。因此你应该尽可能合并申请一致性内存的请求。最简单的办法是使用dma_pool函数调用(详见下文)。
参数flag(仅存在于dma_alloc_coherent中)运行调用者定义申请内存时的GFP_flags(详见kmalloc)。
void *
dma_zalloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flag)
对dma_alloc_coherent()的封装,如果内存分配成功,则返回清零的内存。
void
dma_free_coherent(struct device *dev, size_t size, void *cpu_addr,
dma_addr_t dma_handle)
释放之前申请的一致性内存。dev, size及dma_handle必须和申请一致性内存的函数参数相同。cpu_addr必须为申请一致性内存函数的返回虚拟地址。
注意:和其他内存分配函数不同,这些函数必须要在中断使能的情况下使用。
1-2 使用小块DMA一致性缓冲区
如果要使用这部分DMA API,必须#include <linux/dmapool.h>。
许多驱动程序需要为DMA描述符或者I/O内存申请大量小块DMA一致性内存。你可以使用DMA 内存池,而不是申请以页为单位的内存块或者调用dma_alloc_coherent()。这种机制有点像struct kmem_cache,只是它利用了DMA一致性内存分配器,而不是调用 __get_free_pages()。同样地,DMA 内存池知道通用硬件的对齐限制,比如队列头需要N字节对齐。
struct dma_pool *
dma_pool_create(const char *name, struct device *dev,
size_t size, size_t align, size_t alloc);
create( )函数为设备初始化DMA一致性内存的内存池。它必须要在可睡眠上下文调用。
name为内存池的名字(就像struct kmem_cache name一样)。dev及size就如dma_alloc_coherent()参数一样。align为设备硬件需要的对齐大小(单位为字节,必须为2的幂次方)。如果设备没有边界限制,可以设置该参数为0。如果设置为4096,则表示从内存池分配的内存不能超过4K字节的边界。
void *
dma_pool_alloc(struct dma_pool *pool, gfp_t gfp_flags,
dma_addr_t *dma_handle);
从内存池中分配内存。返回的内存同时满足申请的大小及对齐要求。设置GFP_ATOMIC可以确保内存分配被block,设置GFP_KERNEL(不能再中断上下文,不会保持SMP锁)允许内存分配被block。和dma_alloc_coherent()一样,这个函数会返回两个值:一个值是cpu可以使用的虚拟地址,另一个值是内存池设备可以使用的dma物理地址。
void
dma_pool_free(struct dma_pool *pool, void *vaddr,
dma_addr_t addr);
返回内存给内存池。参数pool为传递给dma_pool_alloc()的pool,参数vaddr及addr为dma_pool_alloc()的返回值。
void
dma_pool_destroy(struct dma_pool *pool);
内存池析构函数用于释放内存池的资源。这个函数在可睡眠上下文调用。请确认在调用此函数时,所有从该内存池申请的内存必须都要归还给内存池。
1-3 DMA寻址限制
int
dma_supported(struct device *dev, u64 mask)
用来检测该设备是否支持掩码所表示的DMA寻址能力。比如mask为0x0FFFFFF,则检测该设备是否支持24位寻址。
返回1表示支持,0表示不支持。
注意:该函数很少用于检测是否掩码为可用的,它不会改变当前掩码设置。它是一个内部API而非供驱动者使用的外部API。
int
dma_set_mask(struct device *dev, u64 mask)
检测该掩码是否合法,如果合法,则更新设备参数。即更新设备的寻址能力。
返回0表示成功,返回负值表示失败。
int
dma_set_coherent_mask(struct device *dev, u64 mask)
检测该掩码是否合法,如果合法,则更新设备参数。即更新设备的寻址能力。
返回0表示成功,返回负值表示失败。
u64
dma_get_required_mask(struct device *dev)
该函数返回平台可以高效工作的掩码。通常这意味着返回掩码是可以寻址到所有内存的最小值。检查该值可以让DMA描述符的大小尽量的小。
请求平台需要的掩码并不会改变当前掩码。如果你想利用这点,可以利用改返回值通过dma_set_mask()设置当前掩码。
1-4 流式DMA映射
dma_addr_t
dma_map_single(struct device *dev, void *cpu_addr, size_t size,
enum dma_data_direction direction)
映射一块处理器的虚拟地址,这样可以让外设访问。该函数返回内存的物理地址。
在dma_API中强烈建议使用表示DMA传输方向的枚举类型。
DMA_NONE 仅用于调试目的
DMA_TO_DEVICE 数据从内存传输到设备,可认为是写操作。
DMA_FROM_DEVICE 数据从设备传输到内存,可认为是读操作。
DMA_BIDIRECTIONAL 不清楚传输方向则可用该类型。
请注意:并非一台机器上所有的内存区域都可以用这个API映射。进一步说,对于内核连续虚拟地址空间所对应的物理地址并不一定连续(比如这段地址空间由vmalloc申请)。因为这种函数并未提供任何分散/聚集能力,因此用户在企图映射一块非物理连续的内存时,会返回失败。基于此原因,如果想使用该函数,则必须确保缓冲区的物理内存连续(比如使用kmalloc)。
更进一步,所申请内存的物理地址必须要在设备的dma_mask寻址范围内(dma_mask表示与设备寻址能力对应的位)。为了确保由kmalloc申请的内存在dma_mask中,驱动程序需要定义板级相关的标志位来限制分配的物理内存范围(比如在x86上,GFP_DMA用于保证申请的内存在可用物理内存的前16Mb空间,可以由ISA设备使用)。
同时还需注意,如果平台有IOMMU(设备拥有MMU单元,可以进行I/O内存总线和设备的映射,即总线地址和内存物理地址的映射),则上述物理地址连续性及外设寻址能力的限制就不存在了。当然为了方便起见,设备驱动开发者可以假设不存在IOMMU。
警告:内存一致性操作基于高速缓存行(cache line)的宽度。为了可以正确操作该API创建的内存映射,该映射区域的起始地址和结束地址都必须是高速缓存行的边界(防止在一个高速缓存行中有两个或多个独立的映射区域)。因为在编译时无法知道高速缓存行的大小,所以该API无法确保该需求。因此建议那些对高速缓存行的大小不特别关注的驱动开发者们,在映射虚拟内存时保证起始地址和结束地址都是页对齐的(页对齐会保证高速缓存行边界对齐的)。
DMA_TO_DEVICE 软件对内存区域做最后一次修改后,且在传输给设备前,需要做一次同步。一旦该使用该原语,内存区域可被视作设备只读缓冲区。如果设备需要对该内存区域进行写操作,则应该使用DMA_BIDIRECTIONAL(如下所示)
DMA_FROM_DEVICE 驱动在访问数据前必须做一次同步,因为数据可能被设备修改了。内存缓冲区应该被当做驱动只读缓冲区。如果驱动需要进行写操作,应该使用DMA_BIDIRECTIONAL(如下所示)。
DMA_BIDIRECTIONAL 需要特别处理:这意味着驱动并不确定内存数据传输到设备前,内存是否被修改了,同时也不确定设备是否会修改内存。因此,你必须需要两次同步双向内存:一次在内存数据传输到设备前(确保所有缓冲区数据改变都从处理器的高速缓存刷新到内存中),另一次是在设备可能访问该缓冲区数据前(确保所有处理器的高速缓存行都得到了更新,设备可能改变了缓冲区数据)。即在处理器写操作完成时,需要做一次刷高速缓存的操作,以确保数据都同步到了内存缓冲区中。在处理器读操作前,需要更新高速缓冲区的行,已确保设备对内存缓冲区的改变都同步到了高速缓冲区中。
void
dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size,
enum dma_data_direction direction)
取消先前的内存映射。传入该函数的所有参数必须和映射API函数的传入(包括返回)参数相同。
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)
对页进行映射/取消映射的API。对其他映射API的注意事项及警告对此都使用。同样的,参数<offset>及<size>用于部分页映射,如果你对高速缓存行的宽度不清楚的话,建议你不要使用这些参数。
int
dma_mapping_error(struct device *dev, dma_addr_t dma_addr)
在某些场景下,通过dma_map_single及dma_map_page创建映射可能会失败。驱动程序可以通过此函数来检测这些错误。一个非零返回值表示未成功创建映射,驱动程序需要采取适当措施(比如降低当前DMA映射使用率或者等待一段时间再尝试)。
int
dma_map_sg(struct device *dev, struct scatterlist *sg,
int nents, enum dma_data_direction direction)
返回值:被映射的物理内存块的数量(如果在分散/聚集链表中一些元素是物理地址或虚拟地址相邻的,切IOMMU可以将它们映射成单个内存块,则返回值可能比输入值<nents>小)。
请注意如果sg已经映射过了,其不能再次被映射。再次映射会销毁sg中的信息。
如果返回0,则表示dma_map_sg映射失败,驱动程序需要采取适当措施。驱动程序在此时做一些事情显得格外重要,一个阻塞驱动中断请求或者oopsing都总比什么都不做导致文件系统瘫痪强很多。
下面是个分散/聚集映射的例子,假设scatterlists已经存在。
int i, count = dma_map_sg(dev, sglist, nents, direction);
struct scatterlist *sg;
for_each_sg(sglist, sg, count, i) {
hw_address[i] = sg_dma_address(sg);
hw_len[i] = sg_dma_len(sg);
}
其中nents为sglist条目的个数。
这种实现可以很方便将几个连续的sglist条目合并成一个(比如在IOMMU系统中,或者一些页正好是物理连续的)。
然后你就可以循环多次(可能小于nents次)使用sg_dma_address() 及sg_dma_len()来获取sg的物理地址及长度。
void
dma_unmap_sg(struct device *dev, struct scatterlist *sg,
int nhwentries, enum dma_data_direction direction)
取消先前分散/聚集链表的映射。所有参数和分散/聚集映射API的参数相同。
注意:<nents>是传入的参数,不一定是实际返回条目的数值。
void dma_sync_single_for_cpu(struct device *dev, dma_addr_t dma_handle, size_t size,
enum dma_data_direction direction)
void dma_sync_single_for_device(struct device *dev, dma_addr_t dma_handle, size_t size,
enum dma_data_direction direction)
void dma_sync_sg_for_cpu(struct device *dev, struct scatterlist *sg, int nelems,
enum dma_data_direction direction)
void dma_sync_sg_for_device(struct device *dev, struct scatterlist *sg, int nelems,
enum dma_data_direction direction)
为CPU及外设同步single contiguous或分散/聚集映射。
注意:你必须要做这个工作,
-
在CPU读操作前,此时缓冲区由设备通过DMA写入数据(DMA_FROM_DEVICE)
-
在CPU写操作后,缓冲区数据将通过DMA传输到设备(DMA_TO_DEVICE)
-
在传输数据到设备前后(DMA_BIDIRECTIONAL)
dma_addr_t
dma_map_single_attrs(struct device *dev, void *cpu_addr, size_t size,
enum dma_data_direction dir,
struct dma_attrs *attrs)
void
dma_unmap_single_attrs(struct device *dev, dma_addr_t dma_addr,
size_t size, enum dma_data_direction dir,
struct dma_attrs *attrs)
int
dma_map_sg_attrs(struct device *dev, struct scatterlist *sgl,
int nents, enum dma_data_direction dir,
struct dma_attrs *attrs)
void
dma_unmap_sg_attrs(struct device *dev, struct scatterlist *sgl,
int nents, enum dma_data_direction dir,
struct dma_attrs *attrs)
这四个函数除了传入可选的struct dma_attrs*之外,其他和不带_attrs后缀的函数一样。
struct dma_attrs概述了一组DMA属性。struct dma_attrs详细定义请参见linux/dma-attrs.h。
DMA属性的定义是和体系结构相关的,并且Documentation/DMA-attributes.txt有详细描述。
如果struct dma_attrs* 为空,则这些函数可以认为和不带_attrs后缀的函数相同。
下面给出一个如何使用*_attrs 函数的例子,当进行DMA内存映射时,如何传入一个名为DMA_ATTR_FOO的属性:
#include <linux/dma-attrs.h>
/* DMA_ATTR_FOO should be defined in linux/dma-attrs.h and
* documented in Documentation/DMA-attributes.txt */
...
DEFINE_DMA_ATTRS(attrs);
dma_set_attr(DMA_ATTR_FOO, &attrs);
....
n = dma_map_sg_attrs(dev, sg, nents, DMA_TO_DEVICE, &attr);
....
在映射/取消映射的函数中,可以检查DMA_ATTR_FOO是否存在:
void whizco_dma_map_sg_attrs(struct device *dev, dma_addr_t dma_addr,
size_t size, enum dma_data_direction dir,
struct dma_attrs *attrs)
{
....
int foo = dma_get_attr(DMA_ATTR_FOO, attrs);
....
if (foo)
/* twizzle the frobnozzle */
....
第二部分 高级DMA使用方法
警告:下面这些DMA API在大多数情况下不应该被使用。因为它们为一些特殊的需求而准备的,大部分驱动程序并没有这些需求。
如果你不清楚如何确保桥接处理器和I/O设备之间的高速缓存行的一致性,你就根本不应该使用该部分所提到的API。
void *
dma_alloc_noncoherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flag)
平台会根据自身适应条件来选择返回一致性或非一致性内存,其他和dma_alloc_coherent()相同。在使用该函数时,你应该确保在驱动程序中对该内存做了正确的和必要的同步操作。
注意,如果返回一致性内存,则它会确保所有同步操作都变成空操作。
警告:处理非一致性内存是件痛苦的事情。如果你确信你的驱动要在非常罕见的平台上(通常是非PCI平台)运行,这些平台无法分配一致性内存时,你才可以使用该API。
void
dma_free_noncoherent(struct device *dev, size_t size, void *cpu_addr,
dma_addr_t dma_handle)
释放由非一致性API申请的内存。
int
dma_get_cache_alignment(void)
返回处理器高速缓存对齐值。应该注意在你打算映射内存或者做局部映射时,该值为最小对齐值。
注意:该API可能返回一个比实际缓存行的大的值。通常为了方便对齐,该值为2的幂次方。
void
dma_cache_sync(struct device *dev, void *vaddr, size_t size,
enum dma_data_direction direction)
对由dma_alloc_noncoherent()申请的内存做局部映射,其实虚拟地址为vaddr。在做该操作时,请注意缓存行的边界。
int
dma_declare_coherent_memory(struct device *dev, dma_addr_t bus_addr,
dma_addr_t device_addr, size_t size, int flags)
当设备需要一段一致性内存时,申请由dma_alloc_coherent分配的一段内存区域。
flag 可以由下面这些标志位进行或操作。
DMA_MEMORY_MAP 请求由dma_alloc_coherent()申请的内存为直接可写。
DMA_MEMORY_IO 请求由dma_alloc_coherent()申请的内存可以通过read/write/memcpy_toio等函数寻址到。
flag必须包含上述其中一个或者两个标志位。
DMA_MEMORY_INCLUDES_CHILDREN
DMA_MEMORY_EXCLUSIVE
为了使操作简单化,每个设备只能申申明一个该内存区域。
处于效率考虑的目的,大多数平台选择页对齐的区域。对于更小的内存分配,可以使用dma_pool() API。
void
dma_release_declared_memory(struct device *dev)
从系统中移除先前申明的内存区域。该函数不会检测当前区域是否在使用。确保该内存区域当前没有被使用这是驱动程序的事情。
void *
dma_mark_declared_memory_occupied(struct device *dev,
dma_addr_t device_addr, size_t size)
该函数用于覆盖特殊内存区域(dma_alloc_coherent()会分配出第一个可用内存区域)。
返回值为指向该内存的处理器虚拟地址,或者如果其中福分区域被覆盖,则返回一个错误(通过PRT_ERR())。
第三部分 调试驱动程序对DMA-API的使用情况
DMA-API如前文所述有一些限制。在支持硬件IOMMU的系统中,驱动程序不能违反这些限制将变得更加重要。最糟糕的情况是,如果违反了这些限制准则,会导致数据出错知道摧毁文件系统。
为了debug驱动程序及发现使用DMA-API时的bug,检测代码可以编译到kernel中,它们可以告诉开发者那些违规行为。如果你的体系结构支持,你可以选择编译选项“Enable debugging of DMA-API usage”,使能这个选项会影响系统性能,所以请勿在产品内核中加入该选项。
如果你用使能debug选项的内核启动,那么它会记录哪些设备会使用什么DMA内存。如果检测到错误信息,则会在内核log中打印一些警告信息。下面是一个警告提示的例子:
------------[ cut here ]------------
WARNING: at /data2/repos/linux-2.6-iommu/lib/dma-debug.c:448
check_unmap+0x203/0x490()
Hardware name:
forcedeth 0000:00:08.0: DMA-API: device driver frees DMA memory with wrong
function [device address=0x00000000640444be] [size=66 bytes] [mapped as
single] [unmapped as page]
Modules linked in: nfsd exportfs bridge stp llc r8169
Pid: 0, comm: swapper Tainted: G W 2.6.28-dmatest-09289-g8bb99c0 #1
Call Trace:
<IRQ> [<ffffffff80240b22>] warn_slowpath+0xf2/0x130
[<ffffffff80647b70>] _spin_unlock+0x10/0x30
[<ffffffff80537e75>] usb_hcd_link_urb_to_ep+0x75/0xc0
[<ffffffff80647c22>] _spin_unlock_irqrestore+0x12/0x40
[<ffffffff8055347f>] ohci_urb_enqueue+0x19f/0x7c0
[<ffffffff80252f96>] queue_work+0x56/0x60
[<ffffffff80237e10>] enqueue_task_fair+0x20/0x50
[<ffffffff80539279>] usb_hcd_submit_urb+0x379/0xbc0
[<ffffffff803b78c3>] cpumask_next_and+0x23/0x40
[<ffffffff80235177>] find_busiest_group+0x207/0x8a0
[<ffffffff8064784f>] _spin_lock_irqsave+0x1f/0x50
[<ffffffff803c7ea3>] check_unmap+0x203/0x490
[<ffffffff803c8259>] debug_dma_unmap_page+0x49/0x50
[<ffffffff80485f26>] nv_tx_done_optimized+0xc6/0x2c0
[<ffffffff80486c13>] nv_nic_irq_optimized+0x73/0x2b0
[<ffffffff8026df84>] handle_IRQ_event+0x34/0x70
[<ffffffff8026ffe9>] handle_edge_irq+0xc9/0x150
[<ffffffff8020e3ab>] do_IRQ+0xcb/0x1c0
[<ffffffff8020c093>] ret_from_intr+0x0/0xa
<EOI> <4>---[ end trace f6435a98e2a38c0e ]---
驱动开发者可以通过DMA-API的栈回溯信息找出什么导致这些警告。
默认情况下只有第一个错误会打印警告信息,其他错误不会打印警告信息。这种机制保证当前警告打印信息不会冲了你的内核信息。为了debug设备驱动,可以通过debugfs禁止该功能。请看下面详细的defbugfs接口文档。
调试DMA-API代码的debugfs目录叫dma-api/。下列文件存在于该个目录下:
dma-api/all_errors 该文件节点包含一个数值。如果该值不为零,则调试代码会在遇到每个错误的时候都打印警告信息。请注意这个选项会轻易覆盖你的内核信息缓冲区。
dma-api/disabled 只读文件节点,如果禁止调试代码则显示字符“Y”。当系统没有足够内存或者在系统启动时禁止调试功能时,该节点显示“Y”。
dma-api/error_count 只读文件节点,显示发现错误的次数。
dma-api/num_errors 该文件节点显示在打印停止前一共打印多少个警告信息。该值在系统启动时初始化为1,通过写该文件节点来设置该值。
dma-api/min_free_entries 只读文件节点,显示分配器记录的可用dma_debug_entries的最小数目。如果该值变为零,则禁止调试代码。
dma-api/num_free_entries 当前分配器可用dma_debug_entries的数目。
dma-api/driver-filter 通过向该文件节点写入驱动的名字来限制特定驱动的调试输出。如果向该节点输入空字符,则可以再次看到全部错误信息。
如果这些代码默认编译到你的内核中,该调试功能被默认打开。如果在启动时你不想使用该功能,则可以设置“dma_debug=off”作为启动参数,该参数会禁止该功能。如果你想在系统启动后再次打开该功能,则必须重启系统。
如果你指向看到特定设备驱动的调试信息,则可以设置“dma_debug_driver=<drivername>”作为参数。它会在系统启动时使能驱动过滤器。调试代码只会打印和该驱动相关的错误信息。过滤器可以通过debugfs来关闭或者改变。
如果该调试功能在系统运行时自动关闭,则可能是超出了dma_debug_entries的最大限制。这些debug条目在启动时就分配好了,条目数量由每个体系结构自己定义。你可以在启动时使用“dma_debug_entries=<your_desired_number>”来重写该值。
参考文献
[1] documentation/DMA-API.txt
DMA动态映射指南
translated by JHJ([email protected])
本文通过伪代码指导驱动开发者如何正确使用DMA API。关于API更精确的描述,请参考DMA-API.txt。
大多是64位平台有一些特殊硬件可以将总线地址(DMA地址)转换为物理地址。这个和CPU如何利用页表或TLB将虚拟地址转换成物理地址有点像。这种地址转换是有必要的,就像PCI设备可以在单个寻址周期里在64位物理地址空间寻址到任何一个页面。以前linux上的64位平台需要人为设置系统的最大内存大小,这样virt_to_bus()就可以正在工作了(DMA地址转换页表在系统启动时简单初始化,通过__pa(bus_to_virt()可以将DMA地址转换为物理页地址)。
为了使linux可以使用DMA动态映射,它需要得到驱动的一些协助,也就是说需要考虑DMA地址只有在使用时才被映射,DMA传输后,需要取消映射。
当然下面这些API可以在没有这些硬件限制的情况下正常工作。
请注意这些DMA API可以在任何总线上工作,和体系结构无关。你应该是用DMA API而不是特定总线的DMA API(比pci_dma_*)。
首先,你需要确定在你的驱动程序中
#include <linux/dma-mapping.h>
该文件定义了类型dma_addr_t(),它作为一个从DMA 映射函数返回的(总线)地址,到处都会使用到。
什么样的内存是DMA可用的?
第一件你要知道的事情是什么样的内核内存可以用作DMA映射。关于此有一些非书面的准则,本文试图将它们以文字的方式整理出来。
如果你通过页分配器(比如__get_free_page*())或者通用内存分配器(比如kmalloc() or kmem_cache_alloc())分配内存,那么你可以使用由这些函数返回的内存地址用作DMA传输。
这意味着你不能使用vmalloc()返回的内存地址用作DMA。DMA使用由vmalloc申请的内存是有可能的,但是需要遍历页表来获取物理地址,然后将这些页通过类似__va()这样的函数转换成内核虚拟地址。
这条规则意味着你不能将内核镜像地址(在data/text/bss段),或者模块镜像地址,或者栈地址用于DMA。即使这些物理内存可以用于DMA,你也要确保I/O缓冲区是缓存行对齐的。如果不是这样,你将会看到由于DMA不一致性缓存导致的缓存行共享问题(数据丢失)。比如处理器可能写一个字,而DMA在同一个缓存行写另一个字,他们两中的一个将被修改。
同样的,这意味着你不能使用由kmap()调用返回的地址,理由与vmalloc()一样。
可阻塞I/O或者网络缓冲区又会怎么样呢?可阻塞I/O及网络子系统可以确保它们使用的缓冲区可以用于DMA传输。
DMA寻址限制
你的设备有DMA寻址限制吗?比如你的设备只有低24位寻址能力?如果是的,那么你就需通知内核。
默认情况下,内核假设设备可以在32位地址空间寻址。对于64位设备,设备的寻址空间将大大增加。对于一个有寻址限制的外设,如前面所讨论的,需要减小寻址空间。
特别注意对于PCI设备:PCI-X规格书要求PCI-X设备对于数据交互要支持64位寻址。至少在一个平台上(SGI SN2)需要64位一致性内存分配,这样才可以在IO总线为PCI-X模式下正常工作。
可以通过调用dma_set_mask()来通知内核相关限制:
int dma_set_mask(struct device *dev, u64 mask);
通过调用dma_set_coherent_mask()通知内核一致性内存分配的限制。
int dma_set_coherent_mask(struct device *dev, u64 mask);
这里devi为一个指向设备的指针,掩码显示与设备寻址能力对应的位。如果使用指定的mask时DMA能正常工作,则返回零。通常来说,设备数据结构是内嵌到特定总线的设备结构中的。比如一个指向PCI设备的指针为pdev->dev(pdev指向PCI设备)。
如果返回非零值,则对应设备不能使用DMA。如果强行使用则会出现一些不确定现象。此时你需要用一个不同的掩码,或者不适用DMA。这意味着如果返回失败,你有三个选择:
1)如果可能的话,使用另一个DMA掩码值;
2)如果可能的话使用非DMA模式传输数据;
3)放弃该设备,不要初始化该设备;
因此如果你不想执行第二步或者第三步时,你应该在驱动中打印一个KERN_WARNING的消息。这样的话,当驱动使用者抱怨性能很差时,或者根本检测不到设备时,你可以让他们保存内核信息来找出准确原因。
标准的32位寻址设备会如下做一些事情:
if (dma_set_mask(dev, DMA_BIT_MASK(32))) {
printk(KERN_WARNING
"mydev: No suitable DMA available.\n");
goto ignore_this_device;
}
另一个常见的场景是拥有64位寻址能力的设备。该方法用于尝试64位寻址,但是会使用32位掩码,这样可以确保不会失败。内核可以在64位掩码中返回失败,不是因为没有64位寻址能力,而是因为32位寻址比64位寻址更加有效率。比如Sparc64 PCI SAC寻址就比DAC寻址更加有效率。
下面的例子告诉你如何处理拥有64位处理能力的设备的流式DMA。
int using_dac;
if (!dma_set_mask(dev, DMA_BIT_MASK(64))) {
using_dac = 1;
} else if (!dma_set_mask(dev, DMA_BIT_MASK(32))) {
using_dac = 0;
} else {
printk(KERN_WARNING
"mydev: No suitable DMA available.\n");
goto ignore_this_device;
}
如果一个设备可以使用64位一致性内存,那么代码如下:
int using_dac, consistent_using_dac;
if (!dma_set_mask(dev, DMA_BIT_MASK(64))) {
using_dac = 1;
consistent_using_dac = 1;
dma_set_coherent_mask(dev, DMA_BIT_MASK(64));
} else if (!dma_set_mask(dev, DMA_BIT_MASK(32))) {
using_dac = 0;
consistent_using_dac = 0;
dma_set_coherent_mask(dev, DMA_BIT_MASK(32));
} else {
printk(KERN_WARNING
"mydev: No suitable DMA available.\n");
goto ignore_this_device;
}
最后,如果你的设备只有低24位寻址能力,那么代码可能如下:
if (dma_set_mask(dev, DMA_BIT_MASK(24))) {
printk(KERN_WARNING
"mydev: 24-bit DMA addressing not available.\n");
goto ignore_this_device;
}
当调用dma_set_mask()成功时,会返回零。内核会保存输入的掩码值。以后再做DMA映射时就会用到该掩码信息。
有一个特殊情况我们需要在这里提及一下。如果设备支持多重功能(比如声卡支持播放和录音功能),不同的功能有不同的DMA寻址寻址限制,你可能想探测每个掩码然后选出一个机器可以处理的值。最后调用dma_set_mask()会成为最特别的掩码值,这点很重要。
下面给出伪代码来展示如何处理该问题。
#define PLAYBACK_ADDRESS_BITS DMA_BIT_MASK(32)
#define RECORD_ADDRESS_BITS DMA_BIT_MASK(24)
struct my_sound_card *card;
struct device *dev;
...
if (!dma_set_mask(dev, PLAYBACK_ADDRESS_BITS)) {
card->playback_enabled = 1;
} else {
card->playback_enabled = 0;
printk(KERN_WARNING "%s: Playback disabled due to DMA limitations.\n",
card->name);
}
if (!dma_set_mask(dev, RECORD_ADDRESS_BITS)) {
card->record_enabled = 1;
} else {
card->record_enabled = 0;
printk(KERN_WARNING "%s: Record disabled due to DMA limitations.\n",
card->name);
}
DMA映射类型
有两种DMA映射类型。一种为一致性DMA映射,另一种为流式DMA映射。
一致性DMA映射通常在驱动初始化时就完成映射,驱动退出时取消映射。硬件应该保证外设及处理器可以并发访问数据,在没有显性软件刷缓存的情况下,双方都可以看到数据的变换。
可以将“一致性”认为“同步的”。
当前默认返回一致性内存为低32位总线地址,但是考虑到未来的兼容性,你应该设置一致性掩码,尽管默认值对于你的驱动来说是可以正常工作的。
使用一致性映射的好的例子如下:
-
网卡DMA环描述符;
-
SCSI适配器邮箱命令数据结构;
-
当超过主内存时的设备固件代码;
这些例子的共性是当处理器存储数据到内存中,设备立即可见,反之亦然。一致性映射可以保证这点。
重要点:一致性DMA内存并不包括使用适当的内存屏障。处理器可能会重新排序存入一致性内存中的数据。比如,设备需要看到一个描述符的第一个字先得到更新了,然后再看到第二个字得到更新,这个顺序非常重要。你应该像下面这样做来保证在所有平台上都可以正常工作:
desc->word0 = address;
wmb();
desc->word1 = DESC_VALID;
DMA流映射通常映射一次DMA传输,传输完后取消映射(除非使用下面所述的dma_sync_*),硬件可以优化访问顺序。
这里的“流”指的是“异步的”或“在一致性区域外的”。
使用流映射好的例子有:
-
设备传输/接受网络缓冲区;
-
SCSI设备的读写文件系统缓冲区;
使用流映射的接口用于实现硬件支持的性能优化。到此为止,当使用这种映射时,你需要明确知道你为什么使用它。
使用DMA一致性映射
申请和映射大型(以页为单位)一致性DMA缓冲区,你应该:
dma_addr_t dma_handle;
cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, gfp);
这里device是struct device *。如果使用GFP_ATOMIC标志,则可以在中断上下文调用。
size是你想申请的缓冲区的大小,单位为bytes。
该函数会在内存中申请一块缓冲区,就像调用__get_free_pages(将size替换成页的阶数)一样。如果你的驱动希望获取小于一个页面的缓冲区,可以调用dma_pool接口,下面有详细描述。
DMA一致性映射接口默认会返回一个32位的DMA地址。即使设备申明(通过DMA掩码)它有超过32位的寻址能力,如果一致性DMA掩码显性的通过调用dma_set_coherent_mask()来设置,那么一致性分配会返回一个大于32位DMA地址。对于dma_pool接口也一样。
dma_alloc_coherent返回两个值:一个处理可以使用的虚拟地址和一个设备卡可以使用的dma_handle。
处理器的返回地址及DMA总线地址是最小的页的幂次方对齐,返回的缓冲区的大小会大于或者等于申请的size。比如你申请的缓存区小于或者等于64KB,那么返回的缓冲区大小不会超过64KB。
如果想取消映射和释放该DMA缓冲区,可以调用下面的函数:
dma_free_coherent(dev, size, cpu_addr, dma_handle);
如果你的驱动需要很多小块内存缓冲区,你可以写一些本地代码将dma_alloc_coherent申请的页分成小块,或者可以调用dma_pool 接口函数。dma_pool有点像kmem_cache,但它使用的是dma_alloc_coherent,而不是__get_free_pages。同样地,它也知道通用硬件的对齐限制,比如队列头需要N字节对齐。
创建一个dma_pool:
struct dma_pool *pool;
pool = dma_pool_create(name, dev, size, align, alloc);
name为内存池的名字(就像struct kmem_cache name一样)。dev及size就如dma_alloc_coherent()参数一样。align为设备硬件需要的对齐大小(单位为字节,必须为2的幂次方)。如果设备没有边界限制,可以设置该参数为0。如果设置为4096,则表示从内存池分配的内存不能超过4K字节的边界(这个时候最好直接使用dma_alloc_coherent)。
从DMA内存池中申请内存:
cpu_addr = dma_pool_alloc(pool, flags, &dma_handle);
使用SLAB_KERNEL表示可以阻塞申请内存(不能在中断上下文及持有SMP锁),SLAB_ATOMIC表示无阻塞申请内存。和dma_alloc_coherent一样,它会返回两个值,cpu_addr 及 dma_handle。
释放内存给dma_pool:
dma_pool_free(pool, cpu_addr, dma_handle);
参数pool为传递给dma_pool_alloc()的pool,参数vaddr及addr为dma_pool_alloc()的返回值。
内存池析构函数用于释放内存池的资源:
dma_pool_destroy(pool);
这个函数在可睡眠上下文调用。请确认在调用此函数时,所有从该内存池申请的内存必须都要归还给内存池。
DMA方向
本文提到的DMA方向为一个整型值,如下所示:
DMA_BIDIRECTIONAL
DMA_TO_DEVICE
DMA_FROM_DEVICE
DMA_NONE
如果你清楚DMA方向的话,应该提供一个准确值。
DMA_TO_DEVICE 数据从内存传输到设备。
DMA_FROM_DEVICE 数据从设备传输到内存。
DMA_BIDIRECTIONAL 不清楚传输方向则可用该类型。
DMA_NONE 仅用于调试目的
只有流映射才需要定义方向。一致性内存映射隐性的设置为DMA_BIDIRECTIONAL。
SCSI子系统通过成员'sc_data_direction'来告诉你使用的方向。
网络驱动就更加简单了。对于传输数据,用方向DMA_TO_DEVICE来map/unmap缓冲区。对于接收数据,用方向DMA_FROM_DEVICE来map/unmap缓冲区。
使用DMA流映射
DMA流映射函数可以在中断上下文使用。有两种map/ummap。一种是map/unmap单个内存区域,另一只是map/unmap一个scatterlist。
如何映射单个内存区域:
struct device *dev = &my_dev->dev;
dma_addr_t dma_handle;
void *addr = buffer->ptr;
size_t size = buffer->len;
dma_handle = dma_map_single(dev, addr, size, direction);
取消映射
dma_unmap_single(dev, dma_handle, size, direction);
应该在DMA传输完成时调用dma_unmap_single。比如在中断处理函数中告诉你DMA传输已经完成。
使用像单一映射使用cpu指针做参数有一个劣势,即不能引用高端内存。因此有一对与dma_{map,unmap}_single相同的接口函数。这些函数用于处理页/偏移量而不是cpu指针。特别的:
struct device *dev = &my_dev->dev;
dma_addr_t dma_handle;
struct page *page = buffer->page;
unsigned long offset = buffer->offset;
size_t size = buffer->len;
dma_handle = dma_map_page(dev, page, offset, size, direction);
...
dma_unmap_page(dev, dma_handle, size, direction);
其中offset为页内偏移量。
如果使用scatterlists,则可以将若干个区域合并成一个区域用于映射:
int i, count = dma_map_sg(dev, sglist, nents, direction);
struct scatterlist *sg;
for_each_sg(sglist, sg, count, i) {
hw_address[i] = sg_dma_address(sg);
hw_len[i] = sg_dma_len(sg);
}
其中nents为sglist的条目数量。
这种实现可以很方便将几个连续的sglist条目合并成一个(如果DMA映射是以页为单位,任何连续的sglist条目可以合并成一个,事实上在这种特性在一些没有分散/聚集能力或者处理分散/聚集能力很弱的情况下,有巨大的优势)。返回真正映射的sg条目数量。返回零表示失败。
然后你就可以循环多次(可能小于nents次)使用sg_dma_address() 及sg_dma_len()来获取sg的物理地址及长度。
取消scatterlist映射:
dma_unmap_sg(dev, sglist, nents, direction);
确认此时DMA活动已经完成。
注意:<nents>是传入的参数,不一定是实际返回条目的数值。
每次调用dma_map_{single,sg}都应该相应的调用dma_unmap_{single,sg},因为总线地址空间是共享资源(尽管有些端口的映射到每一条总线上,因此很少设备会在同一个总线地址空间上产生冲突),你可以通过占用所有的总线地址来让系统变得不稳定。
如果你想多次使用DMA流映射,同时在DMA传输过程中访问数据,此时需要合适地同步数据缓冲区,这样可以让处理器及外设可以看到最新的更新和正确的DMA缓冲区数据。
所以第一步,调用dma_map_{single,sg}做一个流映射,在DMA传输完成后适当地调用:
dma_sync_single_for_cpu(dev, dma_handle, size, direction);
或者
dma_sync_sg_for_cpu(dev, sglist, nents, direction);
第二步,你希望将缓冲区传给硬件前,让设备再次获得DMA缓冲区,完成CPU对数据的访问,可以调用
dma_sync_single_for_device(dev, dma_handle, size, direction);
或者
dma_sync_sg_for_device(dev, sglist, nents, direction);
在最后一次DMA传输后调用dma_unmap_{single,sg}。如果你在调用dma_map_*和dma_unmap_*之间并不打算访问数据,根本无需调用dma_sync_*。
下面的伪代码展示了你需要使用dma_sync_*() 的情境。
my_card_setup_receive_buffer(struct my_card *cp, char *buffer, int len)
{
dma_addr_t mapping;
mapping = dma_map_single(cp->dev, buffer, len, DMA_FROM_DEVICE);
cp->rx_buf = buffer;
cp->rx_len = len;
cp->rx_dma = mapping;
give_rx_buf_to_card(cp);
}
...
my_card_interrupt_handler(int irq, void *devid, struct pt_regs *regs)
{
struct my_card *cp = devid;
...
if (read_card_status(cp) == RX_BUF_TRANSFERRED) {
struct my_card_header *hp;
/* Examine the header to see if we wish
* to accept the data. But synchronize
* the DMA transfer with the CPU first
* so that we see updated contents.
*/
dma_sync_single_for_cpu(&cp->dev, cp->rx_dma,
cp->rx_len,
DMA_FROM_DEVICE);
/* Now it is safe to examine the buffer. */
hp = (struct my_card_header *) cp->rx_buf;
if (header_is_ok(hp)) {
dma_unmap_single(&cp->dev, cp->rx_dma, cp->rx_len,
DMA_FROM_DEVICE);
pass_to_upper_layers(cp->rx_buf);
make_and_setup_new_rx_buf(cp);
} else {
/* CPU should not write to
* DMA_FROM_DEVICE-mapped area,
* so dma_sync_single_for_device() is
* not needed here. It would be required
* for DMA_BIDIRECTIONAL mapping if
* the memory was modified.
*/
give_rx_buf_to_card(cp);
}
}
}
驱动程序不应该再用virt_to_bus或者bus_to_virt。有些驱动程序不得不做一些改动,因为动态DMA映射中再也没有与bus_to_virt一样的东西了。因为dma_alloc_coherent, dma_pool_alloc 及 dma_map_single 会返回的DMA地址(如果平台支持动态DMA映射,dma_map_sg将它们存在scatterlist中),你应该将这个DMA地址保存在驱动数据结构或者设备寄存器中。
所有的驱动程序都应该毫无例外的使用这些接口。virt_to_bus() 及 bus_to_virt()完成过时了,未来有计划将它们侧地移除。
错误处理
DMA寻址空间有时在某些体系结构中会受限制,可以通过下面方式来检查内存分配是否失败:
-
检查dma_alloc_coherent是否返回NULL或dma_map_sg返回0.
-
通过dma_mapping_error()来检查 dma_map_single 及 dma_map_page的dma_addr_t返回值:
dma_addr_t dma_handle;
dma_handle = dma_map_single(dev, addr, size, direction);
if (dma_mapping_error(dev, dma_handle)) {
/*
* reduce current DMA mapping usage,
* delay and try again later or
* reset driver.
*/
}
平台相关的问题
如果你只是些一个linux驱动而不需要为内核维护一个体系结构你可以跳过本章。
1)数据结构scatterlist的条件
不要发明新的体系结构相关的scatterlist;使用<asm-generic/scatterlist.h>就可以了。如果体系结构支持IOMMU(包括软件IOMMU),你就需要使能CONFIG_NEED_SG_DMA_LENGTH。
2)ARCH_DMA_MINALIGN
体系结构必须确保kmalloc申请的缓冲区对DMA来说是安全的。驱动及子系统都依赖于此。如果一个体系结构并非是一致性DMA(比如硬件不能保证在处理器缓存中的数据和主内存中的数据时刻保持一致),则必须要设置ARCH_DMA_MINALIGN来确保kmalloc缓冲区不会和其他缓冲区共享一个缓存行。实例请看arch/arm/include/asm/cache.h。
请注意ARCH_DMA_MINALIGN和DMA内存对齐限制相关。你无需担心体系结构数据对齐限制。
3)支持多种类型的IOMMU
如果你的体系结构需要支持多种IOMMU,那么你可以使用 include/linux/asm-generic/dma-mapping-common.h。这是一个支持多种IOMMU的DMA API的库。很多体系结构(x86, powerpc, sh, alpha, ia64, microblaze and sparc)都使用它。选择一个看看如何正确使用该库。如果你想在单一系统中支持多种类型的IOMMU,基于x86 及 powec的实例可以给你帮助。
参考文献
[1] documentation/DMA-API-HOWTO.txt
DMA-BUF API使用指南
by JHJ([email protected])
转载出自:http://blog.csdn.net/crazyjiang
本文将会告诉驱动开发者什么是dma-buf共享缓冲区接口,如何作为一个生产者及消费者使用共享缓冲区。
任何一个设备驱动想要使用DMA共享缓冲区,就必须为缓冲区的生产者或者消费者。
如果驱动A想用驱动B创建的缓冲区,那么我们称B为生成者,A为消费者。
生产者:
-
实现和管理缓冲区的操作函数[1];
-
允许其他消费者通过dma-buf接口函数共享缓冲区;
-
实现创建缓冲区的细节;
-
决定在什么存储设备上申请内存;
-
管理scatterlist的迁徙;
消费者:
-
作为一个缓冲区的消费者;
-
无需担心缓冲区是如何/在哪里创建的;
-
需要一个可以访问缓冲区scatterlist的机制,将其映射到自己的地址空间,这样可以让自己可以访问到内存的同块区域,实现共享内存。
数据结构
dma_buf是核心数据结构,可以理解为生产者对象。
struct dma_buf {
size_t size;
struct file *file;
struct list_head attachments;
const struct dma_buf_ops *ops;
/* mutex to serialize list manipulation and attach/detach */
struct mutex lock;
void *priv;
};
其中
size为缓冲区大小
file为指向共享缓冲区的文件指针
attachments为附着在缓冲区上的设备(消费者)
ops为绑定在该缓冲区的操作函数
priv为生产者的私有数据
dma_buf_attachment可以理解为是消费者对象。
struct dma_buf_attachment {
struct dma_buf *dmabuf;
struct device *dev;
struct list_head node;
void *priv;
};
其中
dmabuf为该消费者附着的共享缓冲区
dev为设备信息
node为连接其他消费者的节点
priv为消费者私有数据
这两个数据结构的关系如下所示。
外设的dma-buf操作函数
dma_buf共享缓冲区接口的使用具体包括以下步骤:
-
生产者发出通知,其可以共享一块缓冲区;
-
用户空间获取与该共享缓冲区关联的文件描述符,将其传递给潜在的消费者;
-
每个消费者将其绑定在这个缓冲区上;
-
如果需要,缓冲区使用者向消费者发出访问请求;
-
当使用完缓冲区,消费者通知生产者已经完成DMA传输;
-
当消费者不再使用该共享内存,可以脱离该缓冲区;
1. 生产者共享缓冲区
消费者发出通知,请求共享一块缓冲区。
struct dma_buf *
dma_buf_export(void *priv, struct dma_buf_ops *ops, size_t size, int flags)
如果函数调用成功,则会创建一个数据结构dma_buf,返回其指针。同时还会创建一个匿名文件绑定在该缓冲区上,因此这个缓冲区可以由其他消费者共享了(实际上此时缓冲区可能并未真正创建,这里只是创建了一个抽象的dma_buf)。
2. 用户空间获取文件句柄并传递给潜在消费者
用户程序请求一个文件描述符(fd),该文件描述符指向和缓冲区关联的匿名文件。用户程序可以将文件描述符共享给驱动程序或者用户进程程序。
int
dma_buf_fd(struct dma_buf *dmabuf)
该函数创建为匿名文件创建一个文件描述符,返回"fd"或者错误。
3. 消费者将其绑定在缓冲区上
现在每个消费者可以通过文件描述符fd获取共享缓冲区的引用。
struct dma_buf *
dma_buf_get(int fd)
该函数返回一个dma_buf的引用,同时增加它的refcount(该值记录着dma_buf被多少消费者引用)。
获取缓冲区应用后,消费者需要将它的设备附着在该缓冲区上,这样可以让生产者知道设备的寻址限制。
struct dma_buf_attachment *
dma_buf_attach(struct dma_buf *dmabuf, struct device *dev)
该函数返回一个attachment的数据结构,该结构会用于scatterlist的操作。
dma-buf共享框架有一个记录位图,用于管理附着在该共享缓冲区上的消费者。
到这步为止,生产者可以选择不在实际的存储设备上分配该缓冲区,而是等待第一个消费者申请共享内存。
4. 如果需要,消费者发出访问该缓冲区的请求
当消费者想要使用共享内存进行DMA操作,那么它就会通过接口dma_buf_map_attachment来访问缓冲区。在调用map_dma_buf前至少有一个消费者与之关联。
struct sg_table *
dma_buf_map_attachment(struct dma_buf_attachment *, enum dma_data_direction);
该函数是dma_buf->ops->map_dma_buf的一个封装,它可以对使用该接口的对象隐藏"dma_buf->ops->"
struct sg_table *
(*map_dma_buf)(struct dma_buf_attachment *, enum dma_data_direction);
生产者必须实现该函数。它返回一个映射到调用者地址空间的sg_table,该数据结构包含了缓冲区的scatterlist。
如果第一次调用该函数,生产者现在可以扫描附着在共享缓冲区上的消费者,核实附着设备的请求,为缓冲区选择一个合适的物理存储空间。
基于枚举类型dma_data_direction,多个消费者可能同时访问共享内存(比如读操作)。
如果被一个信号中断,map_dma_buf()可能返回-EINTR。
5. 当使用完成,消费者通知生成者DMA传输结束
当消费者完成DMA操作,它可以通过接口函数dma_buf_unmap_attachment发送“end-of-DMA”给生产者。
void
dma_buf_unmap_attachment(struct dma_buf_attachment *, struct sg_table *);
该函数是dma_buf->ops->unmap_dma_buf()的封装,对使用该接口的对象隐藏"dma_buf->ops->"。
在dma_buf_ops结构中,unmap_dma_buf定义成
void
(*unmap_dma_buf)(struct dma_buf_attachment *, struct sg_table *);
unmap_dma_buf意味着消费者结束了DMA操作。生产者必须要实现该函数。
6. 当消费者不再使用该共享内存,则脱离该缓冲区;
当消费者对该共享缓冲区没有任何兴趣后,它应该断开和该缓冲区的连接。
a. 首先将其从缓冲区中分离出来。
void
dma_buf_detach(struct dma_buf *dmabuf, struct dma_buf_attachment *dmabuf_attach);
此函数从dmabuf的attachment链表中移除了该对象,如果消费者实现了dma_buf->ops->detach(),那么它会调用该函数。
b. 然后消费者返回缓冲区的引用给生产者。
void
dma_buf_put(struct dma_buf *dmabuf);
该函数减小缓冲区的refcount。
如果调用该函数后refcount变成0,该文件描述符的"release"函数将会被调用。它会调用dmabuf->ops->release(),企图释放生产者为dmabuf申请的内存。
注意事项:
a. attach-detach及{map,unmap}_dma_buf成对执行非常重要。
attach-detach函数调用可以让生产者明确当前消费者对物理内存的限制。如果可能,它会在不同的存储设备上申请或/和移动物理页框。
b. 如果有必要,需要将缓冲区移动到另一个物理地址空间。
如果
-
至少有一个map_dma_buf存在,
-
该缓冲区已经分配了物理内存,
此时另一个消费者打算使用该缓冲区,生产者可能允许其请求。
如果生产者允许其请求:
如果新的消费者有严格的DMA寻址限制,而且生产者可以处理这些限制,那么生产者会在map_dma_buf里等待剩余消费者完成缓冲区访问。一旦所有消费者都完成了访问并且unmap了缓冲区,生产者可以将该缓冲区转移到严格的物理地址空间,然后再次允许{map,unmap}_dma_buf操作移动后的共享缓冲区。
如果生产者不能满足新消费者的寻址限制,调用dma_buf_attach() 则会返回失败。
内核处理器访问dma-buf缓冲区对象
允许处理器在内核空间作为一个消费者访问dma-buf对象的原因如下:
-
撤销/回退操作。比如一个设备连接到USB总线上,在发送数据前内核需要将第一个数据移除。
-
对其他消费者而言这个是全透明的。比如其他用户空间消费者注意不到一个 dma-buf是否做过一次撤销/回退操作。
在内核上下文访问dma_buf需要下面三个步骤:
1. 访问前的准备工作,包括使相关cache无效,使处理器可以访问缓冲区对象;
2. 通过dma_buf map接口函数以页为单位访问对象;
3. 完成访问时,需要刷新必要的处理器cache,释放占用的资源;
1. 访问前的准备工作
处理器在内核空间打算访问dma_buf对象前,需要通知生产者。
int
dma_buf_begin_cpu_access(struct dma_buf *dmabuf, size_t start, size_t len,
enum dma_data_direction direction)
生产者可以确保处理器可以访问这些内存缓冲区,生产者也需要确定处理器在指定区域及指定方向的访问是一致性的。生产者可以使用访问区域及访问方向来优化cache flushing。比如访问指定范围外的区域或者不同的方向(用读操作替换写操作)会导致陈旧的或者不正确的数据(比如生产者需要将数据拷贝到零时缓冲区)。
该函数调用可能会失败,比如在OOM(内存紧缺)的情况下。
2. 访问缓冲区
为了支持处理器可以访问到驻留在高端内存中的dma_buf对象,需要调用一个和kmap类似的接口函数。访问dma_buf需要页对齐。在访问对象前需要先做映射工作,及需要得到一个内核虚拟地址。操作完后,需要取消该对象的映射。
void *
dma_buf_kmap(struct dma_buf *, unsigned long);
void
dma_buf_kunmap(struct dma_buf *, unsigned long, void *);
该函数有对应的原子操作函数,如下所示。在调用原子操作函数时,生产者和消费者都不能被阻塞。
void *
dma_buf_kmap_atomic(struct dma_buf *, unsigned long);
void
dma_buf_kunmap_atomic(struct dma_buf *, unsigned long, void *);
生产者在同一时间不能同时调用原子操作函数(在任何进程空间)。
如果访问缓冲区区域不是页对齐的,虽然kmap对应的区域数据得到了更新,但是在这个区域附近的区域数据也相应得到了更新,这个不是我们所希望的。也就是说kmap更新了自己关心的区域外,还更新了其他区域,对于那些区域的使用者来说,数据就已经失效了。
下图给出了一个例子,一共有四个连续的页,其中kmap没有页对齐获取部分缓冲区,即红色部分,由于会同步cache,其附近的区域数据也会被更新,被更新区域的范围和cache行的大小有关系。
注意这些调用总是成功的,生产者需要在begin_cpu_access中完成所有的准备,在这其中可能才会有失败。
3. 完成访问
当消费者完成对begin_cpu_access指定范围内的缓冲区访问,需要通知生产者(刷新cache,同步数据集释放资源)。
void dma_buf_end_cpu_access(struct dma_buf *dma_buf,
size_t start, size_t len,
enum dma_data_direction dir);
用户空间通过mmap直接访问缓冲区
在用户空间映射一个dma-buf对象,主要有两个原因:
-
处理器回退/撤销操作;
-
支持消费者程序中已经存在的mmap接口;
1. 处理器在一个pipeline中回退/撤销操作
在处理pipeline过程中,有时处理器需要访问dma-buf中的数据(比如创建thumbnail, snapshots等等)。用户空间程序通过使用dma-buf的文件描述符fd调用mmap来访问dma-buf中的数据是一个好办法,这样可以避免用户空间程序对共享内存做一些特殊处理。
进一步说Android的ION框架已经实现了该功能(从用户空间消费者来说它实现了一个和dma-buf很像的东西,使用fds用作文件句柄)。因此实现该功能对于Android用户空间来说是有意义的。
没有特别的接口,用户程序可以直接基于dma-buf的fd调用mmp。
2. 支持消费者程序中已经存在的mmap接口
与处理器在内核空间访问dma-buf对象目的一样,用户空间消费者可以将生产者的dma-buf缓冲区对象当做本地缓冲区对象一样使用。这对drm特别重要,其Opengl,X的用户空间及驱动代码非常巨大,重写这部分代码让他们用其他方式的mmap,工作量会很大。
int
dma_buf_mmap(struct dma_buf *, struct vm_area_struct *, unsigned long);
参考文献
[1] struct dma_buf_ops in include/linux/dma-buf.h
[2] All interfaces mentioned above defined in include/linux/dma-buf.h
[3] https://lwn.net/Articles/236486/
[4] Documentation/dma-buf-sharing.txt