Linux之DMA API -- 通用設備的動態DMA映射

通用設備的動態DMA映射

by JHJ(jianghuijun211@gmail.com)  linux

本文描述DMA API。更詳細的介紹請參看Documentation/DMA-API-HOWTO.txt。 api

API分爲兩部分,第一部分描述API,第二部分描述能夠支持非一致性內存機器的擴展API。你應該使用第一部分所描述的API,除非你知道你的驅動必需要支持非一致性平臺。 緩存

第一部分 DMA API

爲了能夠引用DMA API,你必須 #include <linux/dma-mapping.h> app

1-1 使用大塊DMA一致性緩衝區(dma-coherent buffers) 函數

void *
dma_alloc_coherent
(struct device *dev, size_t size,
                    dma_addr_t *dma_handle, gfp_t flag)
oop

一致性內存:設備對一塊內存進行寫操做,處理器能夠當即進行讀操做,而無需擔憂處理器高速緩存(cache)的影響。一樣的,處理器對一塊內存進行些操做,設備能夠當即進行讀操做。(在告訴設備讀內存時,你可能須要肯定刷新處理器的寫緩存。) 性能

此函數申請一段大小爲size字節的一致性內存,返回兩個參數。一個是dma_handle,它能夠用做這段內存的物理地址。 另外一個是指向被分配內存的指針(處理器的虛擬地址)。 ui

注意:因爲在某些平臺上,使用一致性內存代價很高,好比最小的分配長度爲一個頁。所以你應該儘量合併申請一致性內存的請求。最簡單的辦法是使用dma_pool函數調用(詳見下文)。 spa

參數flag(僅存在於dma_alloc_coherent中)運行調用者定義申請內存時的GFP_flags(詳見kmalloc)。 debug

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

相關文章
相關標籤/搜索