動態DMA對映指南¶
- 作者:
David S. Miller <davem@redhat.com>
- 作者:
Richard Henderson <rth@cygnus.com>
- 作者:
Jakub Jelinek <jakub@redhat.com>
本指南旨在幫助裝置驅動程式開發者瞭解如何使用DMA API,並附帶虛擬碼示例。有關API的簡明描述,請參見使用通用裝置的動態DMA對映。
CPU和DMA地址¶
DMA API涉及多種地址,理解它們之間的區別至關重要。
核心通常使用虛擬地址。由kmalloc()、vmalloc()及類似介面返回的任何地址都是虛擬地址,可以儲存在void *中。
虛擬記憶體系統(TLB、頁表等)將虛擬地址轉換為CPU物理地址,這些地址儲存為“phys_addr_t”或“resource_size_t”型別。核心將暫存器等裝置資源作為物理地址管理。這些地址位於/proc/iomem中。物理地址對驅動程式沒有直接用途;它必須使用ioremap()來對映該空間並生成一個虛擬地址。
I/O裝置使用第三種地址:“匯流排地址”。如果裝置在MMIO地址處有暫存器,或者它執行DMA來讀寫系統記憶體,則裝置使用的地址是匯流排地址。在某些系統中,匯流排地址與CPU物理地址相同,但通常情況下不同。IOMMU和主機橋接器可以在物理地址和匯流排地址之間產生任意對映。
從裝置的角度來看,DMA使用匯流排地址空間,但它可能僅限於該空間的一個子集。例如,即使系統為主記憶體和PCI BAR支援64位地址,它也可能使用IOMMU,因此裝置只需要使用32位DMA地址。
這是一張圖和一些示例
CPU CPU Bus
Virtual Physical Address
Address Address Space
Space Space
+-------+ +------+ +------+
| | |MMIO | Offset | |
| | Virtual |Space | applied | |
C +-------+ --------> B +------+ ----------> +------+ A
| | mapping | | by host | |
+-----+ | | | | bridge | | +--------+
| | | | +------+ | | | |
| CPU | | | | RAM | | | | Device |
| | | | | | | | | |
+-----+ +-------+ +------+ +------+ +--------+
| | Virtual |Buffer| Mapping | |
X +-------+ --------> Y +------+ <---------- +------+ Z
| | mapping | RAM | by IOMMU
| | | |
| | | |
+-------+ +------+
在列舉過程中,核心會了解I/O裝置及其MMIO空間,以及將它們連線到系統的主機橋接器。例如,如果一個PCI裝置有一個BAR,核心會從BAR讀取匯流排地址(A),並將其轉換為CPU物理地址(B)。地址B儲存在struct resource中,通常透過/proc/iomem暴露。當驅動程式宣告一個裝置時,它通常使用ioremap()將物理地址B對映到虛擬地址(C)。然後,它可以使用例如ioread32(C)來訪問匯流排地址A處的裝置暫存器。
如果裝置支援DMA,驅動程式會使用kmalloc()或類似介面設定一個緩衝區,該介面返回一個虛擬地址(X)。虛擬記憶體系統將X對映到系統RAM中的物理地址(Y)。驅動程式可以使用虛擬地址X訪問緩衝區,但裝置本身不能,因為DMA不經過CPU虛擬記憶體系統。
在一些簡單的系統中,裝置可以直接對物理地址Y進行DMA。但在許多其他系統中,存在IOMMU硬體,它將DMA地址轉換為物理地址,例如,將Z轉換為Y。這就是DMA API的部分原因:驅動程式可以將虛擬地址X傳遞給dma_map_single()等介面,該介面會設定任何所需的IOMMU對映並返回DMA地址Z。然後,驅動程式告訴裝置對Z進行DMA,IOMMU將其對映到系統RAM中地址Y處的緩衝區。
因此,為了讓Linux能夠使用動態DMA對映,它需要驅動程式的一些幫助,即必須考慮到DMA地址只應在實際使用期間進行對映,並在DMA傳輸後取消對映。
當然,即使在沒有此類硬體的平臺上,以下API也能工作。
請注意,DMA API適用於任何匯流排,與底層微處理器架構無關。您應該使用DMA API而不是匯流排特定的DMA API,即使用dma_map_*()介面而不是pci_map_*()介面。
首先,您應該確保
#include <linux/dma-mapping.h>
存在於您的驅動程式中,它提供了dma_addr_t的定義。該型別可以儲存平臺上任何有效的DMA地址,並且應在您儲存從DMA對映函式返回的DMA地址的所有地方使用。
哪些記憶體是可DMA的?¶
您必須瞭解的第一條資訊是哪些核心記憶體可以用於DMA對映功能。對此一直有一套不成文的規則,本文旨在最終將其記錄下來。
如果您透過頁分配器(即__get_free_page*())或通用記憶體分配器(即kmalloc()或kmem_cache_alloc())獲取記憶體,則您可以使用這些例程返回的地址對該記憶體進行DMA讀寫。
這意味著您特別_不_能使用vmalloc()返回的記憶體/地址進行DMA。可以對對映到vmalloc()區域的_底層_記憶體進行DMA,但這需要遍歷頁表以獲取物理地址,然後使用__va()等將每個頁面轉換回核心地址。[ 編輯:當整合Gerd Knorr的通用程式碼時更新此內容。]
此規則還意味著您不能將核心映像地址(data/text/bss段中的項)、模組映像地址或堆疊地址用於DMA。這些地址可能都被對映到與物理記憶體其餘部分完全不同的地方。即使這些記憶體類別在物理上可以與DMA配合使用,您也需要確保I/O緩衝區是快取行對齊的。否則,在具有DMA非一致性快取的CPU上,您會看到快取行共享問題(資料損壞)。(CPU可能寫入一個字,DMA會寫入同一快取行中的不同字,其中一個可能會被覆蓋。)
此外,這意味著您不能使用kmap()呼叫的返回值進行DMA讀寫。這與vmalloc()類似。
塊I/O和網路緩衝區呢?塊I/O和網路子系統確保它們使用的緩衝區對您進行DMA讀寫是有效的。
DMA定址能力¶
預設情況下,核心假定您的裝置可以定址32位DMA地址。對於支援64位的裝置,需要增加此值;對於有侷限性的裝置,需要減小此值。
關於PCI的特別說明:PCI-X規範要求PCI-X裝置支援所有事務的64位定址(DAC)。並且至少有一個平臺(SGI SN2)要求在IO匯流排處於PCI-X模式時,64位一致性分配才能正常執行。
為確保正確操作,您必須設定DMA掩碼以告知核心您的裝置的DMA定址能力。
這透過呼叫dma_set_mask_and_coherent()來執行
int dma_set_mask_and_coherent(struct device *dev, u64 mask);
它將同時設定流式和一致性API的掩碼。如果您有特殊要求,則可以改為使用以下兩個單獨的呼叫:
流式對映的設定透過呼叫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);
此處,dev是指向您裝置的device結構體的指標,mask是一個位掩碼,描述您的裝置支援地址的哪些位。通常,您裝置的device結構體嵌入在您裝置的特定匯流排device結構體中。例如,&pdev->dev是指向PCI裝置的device結構體指標(pdev是指向您裝置的PCI device結構體的指標)。
這些呼叫通常返回零,表示在給定您提供的地址掩碼的情況下,您的裝置可以在機器上正確執行DMA,但如果掩碼太小以至於在給定系統上無法支援,它們可能會返回錯誤。如果返回非零值,您的裝置無法在此平臺上正確執行DMA,嘗試這樣做將導致未定義行為。除非dma_set_mask系列函式返回成功,否則您不得在此裝置上使用DMA。
這意味著在失敗的情況下,您有兩種選擇
如果可能,使用一些非DMA模式進行資料傳輸。
忽略此裝置,不初始化它。
建議您的驅動程式在設定DMA掩碼失敗時列印一個核心KERN_WARNING訊息。這樣,如果您的驅動程式使用者報告效能不佳或裝置甚至未被檢測到,您可以向他們索取核心訊息以查明確切原因。
24位定址裝置會這樣做:
if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(24))) {
dev_warn(dev, "mydev: No suitable DMA available\n");
goto ignore_this_device;
}
標準的64位定址裝置會這樣做:
dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64))
當使用DMA_BIT_MASK(64)時,dma_set_mask_and_coherent()從不返回失敗。典型的錯誤程式碼如下:
/* Wrong code */
if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64)))
dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32))
當掩碼大於32位時,dma_set_mask_and_coherent()從不返回失敗。所以典型的程式碼如下:
/* Recommended code */
if (support_64bit)
dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64));
else
dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32));
如果裝置僅支援一致性分配中描述符的32位定址,但支援流式對映的完整64位,它將如下所示:
if (dma_set_mask(dev, DMA_BIT_MASK(64))) {
dev_warn(dev, "mydev: No suitable DMA available\n");
goto ignore_this_device;
}
一致性掩碼將始終能夠設定為與流式掩碼相同或更小的掩碼。但是,對於裝置驅動程式僅使用一致性分配的罕見情況,則必須檢查dma_set_coherent_mask()的返回值。
最後,如果您的裝置只能驅動地址的低24位,您可能會這樣做:
if (dma_set_mask(dev, DMA_BIT_MASK(24))) {
dev_warn(dev, "mydev: 24-bit DMA addressing not available\n");
goto ignore_this_device;
}
當dma_set_mask()或dma_set_mask_and_coherent()成功並返回零時,核心會儲存您提供的此掩碼。核心將在您進行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;
dev_warn(dev, "%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;
dev_warn(dev, "%s: Record disabled due to DMA limitations\n",
card->name);
}
此處使用音效卡作為示例,因為這類PCI裝置似乎充斥著帶有PCI前端的ISA晶片,因此保留了ISA的16MB DMA定址限制。
DMA對映型別¶
DMA對映有兩種型別:
一致性DMA對映通常在驅動程式初始化時對映,在結束時取消對映,並且硬體應保證裝置和CPU可以並行訪問資料,並能看到彼此的更新而無需任何顯式軟體重新整理。
將“一致性”理解為“同步”或“連貫性”。
當前的預設設定是在DMA空間的低32位中返回一致性記憶體。但是,為了未來的相容性,即使此預設設定對您的驅動程式來說沒問題,您也應該設定一致性掩碼。
使用一致性對映的良好示例包括:
網絡卡DMA環形描述符。
SCSI介面卡郵箱命令資料結構。
在主記憶體中執行的裝置韌體微碼。
這些示例都要求的不變性是:任何CPU對記憶體的寫入都必須立即對裝置可見,反之亦然。一致性對映保證了這一點。
重要
一致性DMA記憶體並不排除使用適當的記憶體屏障。CPU可能會對一致性記憶體進行儲存重排,就像對普通記憶體一樣。示例:如果裝置必須在第二個字更新之前看到描述符的第一個字更新,您必須這樣做:
desc->word0 = address; wmb(); desc->word1 = DESC_VALID;
以便在所有平臺上獲得正確的行為。
此外,在某些平臺上,您的驅動程式可能需要重新整理CPU寫入緩衝區,這與重新整理PCI橋接器中發現的寫入緩衝區的方式非常相似(例如,在寫入暫存器值後讀取它)。
流式DMA對映通常僅為一次DMA傳輸而對映,並在傳輸後立即取消對映(除非您使用下面的dma_sync_*),並且硬體可以針對順序訪問進行最佳化。
將“流式”理解為“非同步”或“超出一致性域”。
使用流式對映的良好示例包括:
裝置傳送/接收的網路緩衝區。
SCSI裝置寫入/讀取的檔案系統緩衝區。
設計這種對映型別的介面時,旨在使實現能夠進行硬體允許的任何效能最佳化。為此,在使用此類對映時,您必須明確說明您希望發生什麼。
這兩種DMA對映型別都沒有來自底層匯流排的對齊限制,儘管某些裝置可能有此類限制。此外,具有非DMA一致性快取的系統在底層緩衝區不與其他資料共享快取行時會更好地工作。
使用一致性DMA對映¶
要分配和對映大型(PAGE_SIZE左右)一致性DMA區域,您應該這樣做:
dma_addr_t dma_handle;
cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, gfp);
其中device是一個struct device *。這可以在中斷上下文中使用GFP_ATOMIC標誌呼叫。
Size是您要分配的區域的長度,以位元組為單位。
此例程將為該區域分配RAM,因此其行為類似於__get_free_pages()(但接受大小而不是頁面順序)。如果您的驅動程式需要小於頁面大小的區域,您可能更喜歡使用下面描述的dma_pool介面。
一致性DMA對映介面預設將返回一個32位可定址的DMA地址。即使裝置指示(透過DMA掩碼)它可以定址高32位,一致性分配也只有在一致性DMA掩碼透過dma_set_coherent_mask()明確更改後,才會返回大於32位的DMA地址。dma_pool介面也同樣如此。
dma_alloc_coherent()返回兩個值:您可以從CPU訪問它的虛擬地址,以及傳遞給網絡卡的dma_handle。
CPU虛擬地址和DMA地址都保證對齊到大於或等於請求大小的最小PAGE_SIZE順序。此不變性(例如)旨在保證如果您分配一個小於或等於64千位元組的塊,您收到的緩衝區的範圍不會跨越64K邊界。
要取消對映和釋放此類DMA區域,您呼叫:
dma_free_coherent(dev, size, cpu_addr, dma_handle);
其中dev、size與上述呼叫相同,cpu_addr和dma_handle是dma_alloc_coherent()返回給您的值。此函式不得在中斷上下文中呼叫。
如果您的驅動程式需要大量較小的記憶體區域,您可以編寫自定義程式碼來細分dma_alloc_coherent()返回的頁面,或者您可以使用dma_pool API來完成。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, boundary);
“name”用於診斷(類似於kmem_cache名稱);dev和size如上所述。裝置對此類資料的硬體對齊要求是“align”(以位元組表示,必須是2的冪)。如果您的裝置沒有跨越邊界的限制,則為boundary傳遞0;傳遞4096表示從該池分配的記憶體不得跨越4KB邊界(但那時最好直接使用dma_alloc_coherent())。
像這樣從DMA池分配記憶體:
cpu_addr = dma_pool_alloc(pool, flags, &dma_handle);
如果允許阻塞(不在中斷中也不持有SMP鎖),則flags為GFP_KERNEL;否則為GFP_ATOMIC。與dma_alloc_coherent()一樣,這返回兩個值:cpu_addr和dma_handle。
像這樣釋放從dma_pool分配的記憶體:
dma_pool_free(pool, cpu_addr, dma_handle);
其中pool是您傳遞給dma_pool_alloc()的值,cpu_addr和dma_handle是dma_pool_alloc()返回的值。此函式可以在中斷上下文中呼叫。
透過呼叫以下函式銷燬dma_pool:
dma_pool_destroy(pool);
在銷燬dma_pool之前,請確保您已對從該池分配的所有記憶體呼叫了dma_pool_free()。此函式不得在中斷上下文中呼叫。
DMA方向¶
本文件後續部分描述的介面接受一個DMA方向引數,它是一個整數,並取以下值之一:
DMA_BIDIRECTIONAL
DMA_TO_DEVICE
DMA_FROM_DEVICE
DMA_NONE
如果您知道確切的DMA方向,則應提供它。
DMA_TO_DEVICE 表示“從主記憶體到裝置” DMA_FROM_DEVICE 表示“從裝置到主記憶體” 它是資料在DMA傳輸期間移動的方向。
強烈建議您儘可能精確地指定此項。
如果您絕對無法知道DMA傳輸的方向,請指定DMA_BIDIRECTIONAL。它意味著DMA可以雙向進行。平臺保證您可以合法指定此項,並且它會起作用,但這可能會以犧牲效能為代價。
值DMA_NONE用於除錯。在您確定精確方向之前,可以將其儲存在資料結構中,這將有助於捕獲您的方向跟蹤邏輯未能正確設定事物的情況。
精確指定此值的另一個優點(除了潛在的平臺特定最佳化之外)是為了除錯。有些平臺實際上有一個寫許可權布林值,DMA對映可以用它來標記,這很像使用者程式地址空間中的頁面保護。當DMA控制器硬體檢測到許可權設定違規時,這些平臺可以並且確實在核心日誌中報告錯誤。
只有流式對映才指定方向,一致性對映隱式具有DMA_BIDIRECTIONAL方向屬性設定。
SCSI子系統會在您的驅動程式正在處理的SCSI命令的‘sc_data_direction’成員中告訴您要使用的方向。
對於網路驅動程式來說,這相當簡單。對於傳送包,使用DMA_TO_DEVICE方向指定符對映/取消對映它們。對於接收包,則相反,使用DMA_FROM_DEVICE方向指定符對映/取消對映它們。
使用流式DMA對映¶
流式DMA對映例程可以在中斷上下文中呼叫。每個對映/取消對映都有兩個版本,一個對映/取消對映單個記憶體區域,另一個對映/取消對映雜湊列表。
要對映單個區域,您這樣做:
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);
if (dma_mapping_error(dev, dma_handle)) {
/*
* reduce current DMA mapping usage,
* delay and try again later or
* reset driver.
*/
goto map_error_handling;
}
要取消對映,您這樣做:
dma_unmap_single(dev, dma_handle, size, direction);
您應該呼叫dma_mapping_error(),因為dma_map_single()可能會失敗並返回錯誤。這樣做將確保對映程式碼在所有DMA實現上都能正確工作,而無需依賴底層實現的具體細節。不檢查錯誤就使用返回的地址可能會導致從恐慌到靜默資料損壞的各種故障。dma_map_page()也同樣適用。
當DMA活動完成時,您應該呼叫dma_unmap_single(),例如,在告訴您DMA傳輸已完成的中斷中呼叫。
像這樣使用CPU指標進行單次對映有一個缺點:您無法以此方式引用HIGHMEM記憶體。因此,存在一個類似於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);
if (dma_mapping_error(dev, dma_handle)) {
/*
* reduce current DMA mapping usage,
* delay and try again later or
* reset driver.
*/
goto map_error_handling;
}
...
dma_unmap_page(dev, dma_handle, size, direction);
此處,“offset”表示給定頁面內的位元組偏移量。
您應該呼叫dma_mapping_error(),因為dma_map_page()可能會失敗並返回錯誤,如dma_map_single()討論中所述。
當DMA活動完成時,您應該呼叫dma_unmap_page(),例如,在告訴您DMA傳輸已完成的中斷中呼叫。
對於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對映以PAGE_SIZE粒度完成,任何連續的sglist條目只要第一個結束和第二個開始於頁面邊界,就可以合併為一個——事實上,這對於那些無法進行雜湊-收集或雜湊-收集條目數量非常有限的卡來說是一個巨大的優勢),並返回實際對映到的sg條目數量。失敗時返回0。
然後,您應該迴圈count次(注意:這可能少於nents次),並使用sg_dma_address()和sg_dma_len()宏,而不是像上面那樣直接訪問sg->address和sg->length。
要取消對映scatterlist,只需呼叫:
dma_unmap_sg(dev, sglist, nents, direction);
再次,確保DMA活動已經完成。
注意
dma_unmap_sg呼叫的‘nents’引數必須與您傳遞給dma_map_sg呼叫的引數_相同_,它_不應該_是dma_map_sg呼叫_返回_的‘count’值。
每個dma_map_{single,sg}()呼叫都應該有其對應的dma_unmap_{single,sg}(),因為DMA地址空間是共享資源,如果消耗了所有DMA地址,可能會導致機器無法使用。
如果您需要多次使用相同的流式DMA區域,並在DMA傳輸之間操作資料,則需要正確同步緩衝區,以便CPU和裝置看到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_sync_sg_for_cpu()和dma_sync_sg_for_device()的‘nents’引數必須與傳遞給dma_map_sg()的引數相同。它_不是_dma_map_sg()返回的計數。
在最後一次DMA傳輸後,呼叫其中一個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);
if (dma_mapping_error(cp->dev, mapping)) {
/*
* reduce current DMA mapping usage,
* delay and try again later or
* reset driver.
*/
goto map_error_handling;
}
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);
}
}
}
錯誤處理¶
在某些架構上,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. */ goto map_error_handling; }在嘗試對映多頁過程中發生對映錯誤時,取消對映已對映的頁面。這些示例也適用於dma_map_page()。
示例1
dma_addr_t dma_handle1;
dma_addr_t dma_handle2;
dma_handle1 = dma_map_single(dev, addr, size, direction);
if (dma_mapping_error(dev, dma_handle1)) {
/*
* reduce current DMA mapping usage,
* delay and try again later or
* reset driver.
*/
goto map_error_handling1;
}
dma_handle2 = dma_map_single(dev, addr, size, direction);
if (dma_mapping_error(dev, dma_handle2)) {
/*
* reduce current DMA mapping usage,
* delay and try again later or
* reset driver.
*/
goto map_error_handling2;
}
...
map_error_handling2:
dma_unmap_single(dma_handle1);
map_error_handling1:
示例2
/*
* if buffers are allocated in a loop, unmap all mapped buffers when
* mapping error is detected in the middle
*/
dma_addr_t dma_addr;
dma_addr_t array[DMA_BUFFERS];
int save_index = 0;
for (i = 0; i < DMA_BUFFERS; i++) {
...
dma_addr = dma_map_single(dev, addr, size, direction);
if (dma_mapping_error(dev, dma_addr)) {
/*
* reduce current DMA mapping usage,
* delay and try again later or
* reset driver.
*/
goto map_error_handling;
}
array[i].dma_addr = dma_addr;
save_index++;
}
...
map_error_handling:
for (i = 0; i < save_index; i++) {
...
dma_unmap_single(array[i].dma_addr);
}
如果DMA對映在傳輸鉤子(ndo_start_xmit)上失敗,網路驅動程式必須呼叫dev_kfree_skb()來釋放socket緩衝區並返回NETDEV_TX_OK。這意味著在失敗情況下,socket緩衝區被直接丟棄。
如果DMA對映在queuecommand鉤子中失敗,SCSI驅動程式必須返回SCSI_MLQUEUE_HOST_BUSY。這意味著SCSI子系統稍後會再次將命令傳遞給驅動程式。
最佳化取消對映狀態空間消耗¶
在許多平臺上,dma_unmap_{single,page}()僅僅是一個空操作(nop)。因此,跟蹤對映地址和長度是空間浪費。為了避免在您的驅動程式中充斥著ifdefs等來“解決”這個問題(這將違背可移植API的初衷),提供了以下設施。
實際上,與其逐一描述宏,不如我們轉換一些示例程式碼。
在狀態儲存結構中使用DEFINE_DMA_UNMAP_{ADDR,LEN}。例如,之前:
struct ring_state { struct sk_buff *skb; dma_addr_t mapping; __u32 len; };之後:
struct ring_state { struct sk_buff *skb; DEFINE_DMA_UNMAP_ADDR(mapping); DEFINE_DMA_UNMAP_LEN(len); };使用dma_unmap_{addr,len}_set()設定這些值。例如,之前:
ringp->mapping = FOO; ringp->len = BAR;
之後:
dma_unmap_addr_set(ringp, mapping, FOO); dma_unmap_len_set(ringp, len, BAR);
使用dma_unmap_{addr,len}()訪問這些值。例如,之前:
dma_unmap_single(dev, ringp->mapping, ringp->len, DMA_FROM_DEVICE);之後:
dma_unmap_single(dev, dma_unmap_addr(ringp, mapping), dma_unmap_len(ringp, len), DMA_FROM_DEVICE);
這應該是自明的。我們分別處理ADDR和LEN,因為對於某個實現來說,可能只需要地址就能執行取消對映操作。
平臺問題¶
如果您只是為Linux編寫驅動程式,而不維護核心的架構移植,您可以安全地跳到“結束”。
Struct scatterlist要求。
如果架構支援IOMMU(包括軟體IOMMU),則需要啟用CONFIG_NEED_SG_DMA_LENGTH。
ARCH_DMA_MINALIGN
架構必須確保kmalloc分配的緩衝區是DMA安全的。驅動程式和子系統都依賴於此。如果一個架構不是完全DMA一致的(即硬體不保證CPU快取中的資料與主記憶體中的資料相同),則必須設定ARCH_DMA_MINALIGN,以便記憶體分配器確保kmalloc分配的緩衝區不與其它緩衝區共享快取行。請參閱arch/arm/include/asm/cache.h作為示例。
請注意,ARCH_DMA_MINALIGN是關於DMA記憶體對齊約束的。您無需擔心架構資料對齊約束(例如,關於64位物件的對齊約束)。
結束¶
本文件以及API本身,若無眾多人士的反饋和建議,將不會以目前的形式存在。我們想特別提及以下人員(排名不分先後):
Russell King <rmk@arm.linux.org.uk>
Leo Dagum <dagum@barrel.engr.sgi.com>
Ralf Baechle <ralf@oss.sgi.com>
Grant Grundler <grundler@cup.hp.com>
Jay Estabrook <Jay.Estabrook@compaq.com>
Thomas Sailer <sailer@ife.ee.ethz.ch>
Andrea Arcangeli <andrea@suse.de>
Jens Axboe <jens.axboe@oracle.com>
David Mosberger-Tang <davidm@hpl.hp.com>