Linux之DMA API -- 通用設(shè)備的動態(tài)DMA映射通用設(shè)備的動態(tài)DMA映射by JHJ(jianghuijun211@gmail.com) 本文描述DMA API。更詳細(xì)的介紹請參看Documentation/DMA-API-HOWTO.txt。 API分為兩部分,第一部分描述API,第二部分描述可以支持非一致性內(nèi)存機(jī)器的擴(kuò)展API。你應(yīng)該使用第一部分所描述的API,除非你知道你的驅(qū)動必須要支持非一致性平臺。 第一部分 DMA API為了可以引用DMA API,你必須 #include <linux/dma-mapping.h> 1-1 使用大塊DMA一致性緩沖區(qū)(dma-coherent buffers) void * 一致性內(nèi)存:設(shè)備對一塊內(nèi)存進(jìn)行寫操作,處理器可以立即進(jìn)行讀操作,而無需擔(dān)心處理器高速緩存(cache)的影響。同樣的,處理器對一塊內(nèi)存進(jìn)行些操作,設(shè)備可以立即進(jìn)行讀操作。(在告訴設(shè)備讀內(nèi)存時,你可能需要確定刷新處理器的寫緩存。) 此函數(shù)申請一段大小為size字節(jié)的一致性內(nèi)存,返回兩個參數(shù)。一個是dma_handle,它可以用作這段內(nèi)存的物理地址。 另一個是指向被分配內(nèi)存的指針(處理器的虛擬地址)。 注意:由于在某些平臺上,使用一致性內(nèi)存代價很高,比如最小的分配長度為一個頁。因此你應(yīng)該盡可能合并申請一致性內(nèi)存的請求。最簡單的辦法是使用dma_pool函數(shù)調(diào)用(詳見下文)。 參數(shù)flag(僅存在于dma_alloc_coherent中)運(yùn)行調(diào)用者定義申請內(nèi)存時的GFP_flags(詳見kmalloc)。 void * 對dma_alloc_coherent()的封裝,如果內(nèi)存分配成功,則返回清零的內(nèi)存。 void 釋放之前申請的一致性內(nèi)存。dev, size及dma_handle必須和申請一致性內(nèi)存的函數(shù)參數(shù)相同。cpu_addr必須為申請一致性內(nèi)存函數(shù)的返回虛擬地址。 注意:和其他內(nèi)存分配函數(shù)不同,這些函數(shù)必須要在中斷使能的情況下使用。 1-2 使用小塊DMA一致性緩沖區(qū) 如果要使用這部分DMA API,必須#include <linux/dmapool.h>。 許多驅(qū)動程序需要為DMA描述符或者I/O內(nèi)存申請大量小塊DMA一致性內(nèi)存。你可以使用DMA 內(nèi)存池,而不是申請以頁為單位的內(nèi)存塊或者調(diào)用dma_alloc_coherent()。這種機(jī)制有點(diǎn)像struct kmem_cache,只是它利用了DMA一致性內(nèi)存分配器,而不是調(diào)用 __get_free_pages()。同樣地,DMA 內(nèi)存池知道通用硬件的對齊限制,比如隊(duì)列頭需要N字節(jié)對齊。 struct dma_pool * create( )函數(shù)為設(shè)備初始化DMA一致性內(nèi)存的內(nèi)存池。它必須要在可睡眠上下文調(diào)用。 name為內(nèi)存池的名字(就像struct kmem_cache name一樣)。dev及size就如dma_alloc_coherent()參數(shù)一樣。align為設(shè)備硬件需要的對齊大小(單位為字節(jié),必須為2的冪次方)。如果設(shè)備沒有邊界限制,可以設(shè)置該參數(shù)為0。如果設(shè)置為4096,則表示從內(nèi)存池分配的內(nèi)存不能超過4K字節(jié)的邊界。 void * 從內(nèi)存池中分配內(nèi)存。返回的內(nèi)存同時滿足申請的大小及對齊要求。設(shè)置GFP_ATOMIC可以確保內(nèi)存分配被block,設(shè)置GFP_KERNEL(不能再中斷上下文,不會保持SMP鎖)允許內(nèi)存分配被block。和dma_alloc_coherent()一樣,這個函數(shù)會返回兩個值:一個值是cpu可以使用的虛擬地址,另一個值是內(nèi)存池設(shè)備可以使用的dma物理地址。 void 返回內(nèi)存給內(nèi)存池。參數(shù)pool為傳遞給dma_pool_alloc()的pool,參數(shù)vaddr及addr為dma_pool_alloc()的返回值。 void 內(nèi)存池析構(gòu)函數(shù)用于釋放內(nèi)存池的資源。這個函數(shù)在可睡眠上下文調(diào)用。請確認(rèn)在調(diào)用此函數(shù)時,所有從該內(nèi)存池申請的內(nèi)存必須都要?dú)w還給內(nèi)存池。 1-3 DMA尋址限制 int 用來檢測該設(shè)備是否支持掩碼所表示的DMA尋址能力。比如mask為0x0FFFFFF,則檢測該設(shè)備是否支持24位尋址。 返回1表示支持,0表示不支持。 注意:該函數(shù)很少用于檢測是否掩碼為可用的,它不會改變當(dāng)前掩碼設(shè)置。它是一個內(nèi)部API而非供驅(qū)動者使用的外部API。 int 檢測該掩碼是否合法,如果合法,則更新設(shè)備參數(shù)。即更新設(shè)備的尋址能力。 返回0表示成功,返回負(fù)值表示失敗。 int 檢測該掩碼是否合法,如果合法,則更新設(shè)備參數(shù)。即更新設(shè)備的尋址能力。 返回0表示成功,返回負(fù)值表示失敗。 u64 該函數(shù)返回平臺可以高效工作的掩碼。通常這意味著返回掩碼是可以尋址到所有內(nèi)存的最小值。檢查該值可以讓DMA描述符的大小盡量的小。 請求平臺需要的掩碼并不會改變當(dāng)前掩碼。如果你想利用這點(diǎn),可以利用改返回值通過dma_set_mask()設(shè)置當(dāng)前掩碼。 1-4 流式DMA映射 dma_addr_t 映射一塊處理器的虛擬地址,這樣可以讓外設(shè)訪問。該函數(shù)返回內(nèi)存的物理地址。 在dma_API中強(qiáng)烈建議使用表示DMA傳輸方向的枚舉類型。 DMA_NONE 僅用于調(diào)試目的 請注意:并非一臺機(jī)器上所有的內(nèi)存區(qū)域都可以用這個API映射。進(jìn)一步說,對于內(nèi)核連續(xù)虛擬地址空間所對應(yīng)的物理地 址并不一定連續(xù)(比如這段地址空間由vmalloc申請)。因?yàn)檫@種函數(shù)并未提供任何分散/聚集能力,因此用戶在企圖映射一塊非物理連續(xù)的內(nèi)存時,會返回 失敗。基于此原因,如果想使用該函數(shù),則必須確保緩沖區(qū)的物理內(nèi)存連續(xù)(比如使用kmalloc)。 更進(jìn)一步,所申請內(nèi)存的物理地址必須要在設(shè)備的dma_mask尋址范圍內(nèi)(dma_mask表示與設(shè)備尋址能力對 應(yīng)的位)。為了確保由kmalloc申請的內(nèi)存在dma_mask中,驅(qū)動程序需要定義板級相關(guān)的標(biāo)志位來限制分配的物理內(nèi)存范圍(比如在x86 上,GFP_DMA用于保證申請的內(nèi)存在可用物理內(nèi)存的前16Mb空間,可以由ISA設(shè)備使用)。 同時還需注意,如果平臺有IOMMU(設(shè)備擁有MMU單元,可以進(jìn)行I/O內(nèi)存總線和設(shè)備的映射,即總線地址和內(nèi)存物理地址的映射),則上述物理地址連續(xù)性及外設(shè)尋址能力的限制就不存在了。當(dāng)然為了方便起見,設(shè)備驅(qū)動開發(fā)者可以假設(shè)不存在IOMMU。 警告:內(nèi)存一致性操作基于高速緩存行(cache line)的寬度。為了可以正確操作該API創(chuàng)建的內(nèi)存映射,該映射區(qū)域的起始地址和結(jié)束地址都必須是高速緩存行的邊界(防止在一個高速緩存行中有兩個或 多個獨(dú)立的映射區(qū)域)。因?yàn)樵诰幾g時無法知道高速緩存行的大小,所以該API無法確保該需求。因此建議那些對高速緩存行的大小不特別關(guān)注的驅(qū)動開發(fā)者們, 在映射虛擬內(nèi)存時保證起始地址和結(jié)束地址都是頁對齊的(頁對齊會保證高速緩存行邊界對齊的)。 DMA_TO_DEVICE 軟件對內(nèi)存區(qū)域做最后一次修改后,且在傳輸給設(shè)備前,需要做一次同步。一旦該使用該原語,內(nèi)存區(qū)域可被視作設(shè)備只讀緩沖區(qū)。如果設(shè)備需要對該內(nèi)存區(qū)域進(jìn)行寫操作,則應(yīng)該使用DMA_BIDIRECTIONAL(如下所示) DMA_FROM_DEVICE 驅(qū)動在訪問數(shù)據(jù)前必須做一次同步,因?yàn)閿?shù)據(jù)可能被設(shè)備修改了。內(nèi)存緩沖區(qū)應(yīng)該被當(dāng)做驅(qū)動只讀緩沖區(qū)。如果驅(qū)動需要進(jìn)行寫操作,應(yīng)該使用DMA_BIDIRECTIONAL(如下所示)。 DMA_BIDIRECTIONAL 需要特別處理:這意味著驅(qū)動并不確定內(nèi)存數(shù)據(jù)傳輸?shù)皆O(shè)備前,內(nèi)存是否被修改了,同時也不確定設(shè)備是否會修改內(nèi)存。因此,你必須需要兩次同步雙向內(nèi)存:一次 在內(nèi)存數(shù)據(jù)傳輸?shù)皆O(shè)備前(確保所有緩沖區(qū)數(shù)據(jù)改變都從處理器的高速緩存刷新到內(nèi)存中),另一次是在設(shè)備可能訪問該緩沖區(qū)數(shù)據(jù)前(確保所有處理器的高速緩存 行都得到了更新,設(shè)備可能改變了緩沖區(qū)數(shù)據(jù))。即在處理器寫操作完成時,需要做一次刷高速緩存的操作,以確保數(shù)據(jù)都同步到了內(nèi)存緩沖區(qū)中。在處理器讀操作前,需要更新高速緩沖區(qū)的行,已確保設(shè)備對內(nèi)存緩沖區(qū)的改變都同步到了高速緩沖區(qū)中。 void 取消先前的內(nèi)存映射。傳入該函數(shù)的所有參數(shù)必須和映射API函數(shù)的傳入(包括返回)參數(shù)相同。 dma_addr_t void 對頁進(jìn)行映射/取消映射的API。對其他映射API的注意事項(xiàng)及警告對此都使用。同樣的,參數(shù)<offset>及<size>用于部分頁映射,如果你對高速緩存行的寬度不清楚的話,建議你不要使用這些參數(shù)。 int 在某些場景下,通過dma_map_single及dma_map_page創(chuàng)建映射可能會失敗。驅(qū)動程序可以通過此函數(shù)來檢測這些錯誤。一個非零返回值表示未成功創(chuàng)建映射,驅(qū)動程序需要采取適當(dāng)措施(比如降低當(dāng)前DMA映射使用率或者等待一段時間再嘗試)。 int 返回值:被映射的物理內(nèi)存塊的數(shù)量(如果在分散/聚集鏈表中一些元素是物理地址或虛擬地址相鄰的,切IOMMU可以將它們映射成單個內(nèi)存塊,則返回值可能比輸入值<nents>?。?。 請注意如果sg已經(jīng)映射過了,其不能再次被映射。再次映射會銷毀sg中的信息。 如果返回0,則表示dma_map_sg映射失敗,驅(qū)動程序需要采取適當(dāng)措施。驅(qū)動程序在此時做一些事情顯得格外重要,一個阻塞驅(qū)動中斷請求或者oopsing都總比什么都不做導(dǎo)致文件系統(tǒng)癱瘓強(qiáng)很多。 下面是個分散/聚集映射的例子,假設(shè)scatterlists已經(jīng)存在。 int i, count = dma_map_sg(dev, sglist, nents, direction); 其中nents為sglist條目的個數(shù)。 這種實(shí)現(xiàn)可以很方便將幾個連續(xù)的sglist條目合并成一個(比如在IOMMU系統(tǒng)中,或者一些頁正好是物理連續(xù)的)。 然后你就可以循環(huán)多次(可能小于nents次)使用sg_dma_address() 及sg_dma_len()來獲取sg的物理地址及長度。 void 取消先前分散/聚集鏈表的映射。所有參數(shù)和分散/聚集映射API的參數(shù)相同。 注意:<nents>是傳入的參數(shù),不一定是實(shí)際返回條目的數(shù)值。 void dma_sync_single_for_cpu(struct device *dev, dma_addr_t dma_handle, size_t size, void dma_sync_single_for_device(struct device *dev, dma_addr_t dma_handle, size_t size, void dma_sync_sg_for_cpu(struct device *dev, struct scatterlist *sg, int nelems, void dma_sync_sg_for_device(struct device *dev, struct scatterlist *sg, int nelems, 為CPU及外設(shè)同步single contiguous或分散/聚集映射。 注意:你必須要做這個工作,
dma_addr_t void int void 這四個函數(shù)除了傳入可選的struct dma_attrs*之外,其他和不帶_attrs后綴的函數(shù)一樣。 struct dma_attrs概述了一組DMA屬性。struct dma_attrs詳細(xì)定義請參見linux/dma-attrs.h。 DMA屬性的定義是和體系結(jié)構(gòu)相關(guān)的,并且Documentation/DMA-attributes.txt有詳細(xì)描述。 如果struct dma_attrs* 為空,則這些函數(shù)可以認(rèn)為和不帶_attrs后綴的函數(shù)相同。 下面給出一個如何使用*_attrs 函數(shù)的例子,當(dāng)進(jìn)行DMA內(nèi)存映射時,如何傳入一個名為DMA_ATTR_FOO的屬性: #include <linux/dma-attrs.h> 在映射/取消映射的函數(shù)中,可以檢查DMA_ATTR_FOO是否存在: void whizco_dma_map_sg_attrs(struct device *dev, dma_addr_t dma_addr, 第二部分 高級DMA使用方法警告:下面這些DMA API在大多數(shù)情況下不應(yīng)該被使用。因?yàn)樗鼈優(yōu)橐恍┨厥獾男枨蠖鴾?zhǔn)備的,大部分驅(qū)動程序并沒有這些需求。 如果你不清楚如何確保橋接處理器和I/O設(shè)備之間的高速緩存行的一致性,你就根本不應(yīng)該使用該部分所提到的API。 void * 平臺會根據(jù)自身適應(yīng)條件來選擇返回一致性或非一致性內(nèi)存,其他和dma_alloc_coherent()相同。在使用該函數(shù)時,你應(yīng)該確保在驅(qū)動程序中對該內(nèi)存做了正確的和必要的同步操作。 注意,如果返回一致性內(nèi)存,則它會確保所有同步操作都變成空操作。 警告:處理非一致性內(nèi)存是件痛苦的事情。如果你確信你的驅(qū)動要在非常罕見的平臺上(通常是非PCI平臺)運(yùn)行,這些平臺無法分配一致性內(nèi)存時,你才可以使用該API。 void 釋放由非一致性API申請的內(nèi)存。 int 返回處理器高速緩存對齊值。應(yīng)該注意在你打算映射內(nèi)存或者做局部映射時,該值為最小對齊值。 注意:該API可能返回一個比實(shí)際緩存行的大的值。通常為了方便對齊,該值為2的冪次方。 void 對由dma_alloc_noncoherent()申請的內(nèi)存做局部映射,其實(shí)虛擬地址為vaddr。在做該操作時,請注意緩存行的邊界。 int 當(dāng)設(shè)備需要一段一致性內(nèi)存時,申請由dma_alloc_coherent分配的一段內(nèi)存區(qū)域。 flag 可以由下面這些標(biāo)志位進(jìn)行或操作。 DMA_MEMORY_MAP 請求由dma_alloc_coherent()申請的內(nèi)存為直接可寫。 DMA_MEMORY_IO 請求由dma_alloc_coherent()申請的內(nèi)存可以通過read/write/memcpy_toio等函數(shù)尋址到。 flag必須包含上述其中一個或者兩個標(biāo)志位。 DMA_MEMORY_INCLUDES_CHILDREN DMA_MEMORY_EXCLUSIVE 為了使操作簡單化,每個設(shè)備只能申申明一個該內(nèi)存區(qū)域。 處于效率考慮的目的,大多數(shù)平臺選擇頁對齊的區(qū)域。對于更小的內(nèi)存分配,可以使用dma_pool() API。 void 從系統(tǒng)中移除先前申明的內(nèi)存區(qū)域。該函數(shù)不會檢測當(dāng)前區(qū)域是否在使用。確保該內(nèi)存區(qū)域當(dāng)前沒有被使用這是驅(qū)動程序的事情。 void * 該函數(shù)用于覆蓋特殊內(nèi)存區(qū)域(dma_alloc_coherent()會分配出第一個可用內(nèi)存區(qū)域)。 返回值為指向該內(nèi)存的處理器虛擬地址,或者如果其中福分區(qū)域被覆蓋,則返回一個錯誤(通過PRT_ERR())。 第三部分 調(diào)試驅(qū)動程序?qū)MA-API的使用情況DMA-API如前文所述有一些限制。在支持硬件IOMMU的系統(tǒng)中,驅(qū)動程序不能違反這些限制將變得更加重要。最糟糕的情況是,如果違反了這些限制準(zhǔn)則,會導(dǎo)致數(shù)據(jù)出錯知道摧毀文件系統(tǒng)。 為了debug驅(qū)動程序及發(fā)現(xiàn)使用DMA-API時的bug,檢測代碼可以編譯到kernel中,它們可以告訴開發(fā) 者那些違規(guī)行為。如果你的體系結(jié)構(gòu)支持,你可以選擇編譯選項(xiàng)“Enable debugging of DMA-API usage”,使能這個選項(xiàng)會影響系統(tǒng)性能,所以請勿在產(chǎn)品內(nèi)核中加入該選項(xiàng)。 如果你用使能debug選項(xiàng)的內(nèi)核啟動,那么它會記錄哪些設(shè)備會使用什么DMA內(nèi)存。如果檢測到錯誤信息,則會在內(nèi)核log中打印一些警告信息。下面是一個警告提示的例子: ------------[ cut here ]------------ 驅(qū)動開發(fā)者可以通過DMA-API的?;厮菪畔⒄页鍪裁磳?dǎo)致這些警告。 默認(rèn)情況下只有第一個錯誤會打印警告信息,其他錯誤不會打印警告信息。這種機(jī)制保證當(dāng)前警告打印信息不會沖了你的內(nèi)核信息。為了debug設(shè)備驅(qū)動,可以通過debugfs禁止該功能。請看下面詳細(xì)的defbugfs接口文檔。 調(diào)試DMA-API代碼的debugfs目錄叫dma-api/。下列文件存在于該個目錄下: dma-api/all_errors 該文件節(jié)點(diǎn)包含一個數(shù)值。如果該值不為零,則調(diào)試代碼會在遇到每個錯誤的時候都打印警告信息。請注意這個選項(xiàng)會輕易覆蓋你的內(nèi)核信息緩沖區(qū)。 dma-api/disabled 只讀文件節(jié)點(diǎn),如果禁止調(diào)試代碼則顯示字符“Y”。當(dāng)系統(tǒng)沒有足夠內(nèi)存或者在系統(tǒng)啟動時禁止調(diào)試功能時,該節(jié)點(diǎn)顯示“Y”。 dma-api/error_count 只讀文件節(jié)點(diǎn),顯示發(fā)現(xiàn)錯誤的次數(shù)。 dma-api/num_errors 該文件節(jié)點(diǎn)顯示在打印停止前一共打印多少個警告信息。該值在系統(tǒng)啟動時初始化為1,通過寫該文件節(jié)點(diǎn)來設(shè)置該值。 dma-api/min_free_entries 只讀文件節(jié)點(diǎn),顯示分配器記錄的可用dma_debug_entries的最小數(shù)目。如果該值變?yōu)榱悖瑒t禁止調(diào)試代碼。 dma-api/num_free_entries 當(dāng)前分配器可用dma_debug_entries的數(shù)目。 dma-api/driver-filter 通過向該文件節(jié)點(diǎn)寫入驅(qū)動的名字來限制特定驅(qū)動的調(diào)試輸出。如果向該節(jié)點(diǎn)輸入空字符,則可以再次看到全部錯誤信息。 如果這些代碼默認(rèn)編譯到你的內(nèi)核中,該調(diào)試功能被默認(rèn)打開。如果在啟動時你不想使用該功能,則可以設(shè)置“dma_debug=off”作為啟動參數(shù),該參數(shù)會禁止該功能。如果你想在系統(tǒng)啟動后再次打開該功能,則必須重啟系統(tǒng)。 如果你指向看到特定設(shè)備驅(qū)動的調(diào)試信息,則可以設(shè)置“dma_debug_driver=<drivername>”作為參數(shù)。它會在系統(tǒng)啟動時使能驅(qū)動過濾器。調(diào)試代碼只會打印和該驅(qū)動相關(guān)的錯誤信息。過濾器可以通過debugfs來關(guān)閉或者改變。 如果該調(diào)試功能在系統(tǒng)運(yùn)行時自動關(guān)閉,則可能是超出了dma_debug_entries的最大限制。這些 debug條目在啟動時就分配好了,條目數(shù)量由每個體系結(jié)構(gòu)自己定義。你可以在啟動時使用“dma_debug_entries=< your_desired_number>”來重寫該值。 參考文獻(xiàn) [1] documentation/DMA-API.txt |
|
|