linux之DMA API

通用設備的動態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共享緩衝區接口的使用具體包括以下步驟:
 1.
生產者發出通知,其可以共享一塊緩衝區;
 
2.
用戶空間獲取與該共享緩衝區關聯的文件描述符,將其傳遞給潛在的消費者;
 
3.
每個消費者將其綁定在這個緩衝區上;
 
4.
如果需要,緩衝區使用者向消費者發出訪問請求;
 
5.
當使用完緩衝區,消費者通知生產者已經完成DMA傳輸;
 
6.
當消費者不再使用該共享內存,可以脫離該緩衝區;
 

 
 
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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章