Dynamic DMA mapping Guide——linux內核doc譯文

轉載自:Dynamic DMA mapping Guide

一、前言

這是一篇指導驅動工程師如何使用DMA API的文檔,爲了方便理解,文檔中給出了僞代碼的例程。另外一篇文檔dma-api.txt給出了相關API的簡明描述,有興趣也可以看看那一篇,這兩份文檔在DMA API的描述方面是一致的。

 

二、從CPU角度看到的地址和從DMA控制器看到的地址有什麼不同?

在DMA API中涉及好幾個地址的概念(物理地址、虛擬地址和總線地址),正確的理解這些地址是非常重要的。

內核通常使用的地址是虛擬地址。我們調用kmalloc()、vmalloc()或者類似的接口返回的地址都是虛擬地址,保存在"void *"的變量中。

虛擬內存系統(TLB、頁表等)將虛擬地址(程序角度)翻譯成物理地址(CPU角度),物理地址保存在“phys_addr_t”或“resource_size_t”的變量中。對於一個硬件設備上的寄存器等設備資源,內核是按照物理地址來管理的。通過/proc/iomem,你可以看到這些和設備IO 相關的物理地址。當然,驅動並不能直接使用這些物理地址,必須首先通過ioremap()接口將這些物理地址映射到內核虛擬地址空間上去。

I/O設備使用第三種地址:“總線地址”。如果設備在MMIO地址空間中有若干的寄存器,或者該設備足夠的智能,它可以通過DMA執行讀寫系統內存的操作,這些情況下,設備使用的地址就是總線地址。在某些系統中,總線地址與CPU物理地址相同,但一般來說它們不是。iommus和host bridge可以在物理地址和總線地址之間進行映射。

從設備的角度來看,DMA控制器使用總線地址空間,不過可能僅限於總線空間的一個子集。例如:即便是一個系統支持64位地址內存和64 位地址的PCI bar,但是DMA可以不使用全部的64 bit地址,通過IOMMU的映射,PCI設備上的DMA可以只使用32位DMA地址。

我們用下面這樣的系統結構來說明各種地址的概念:

address

在PCI設備枚舉(初始化)過程中,內核瞭解了所有的IO device及其對應的MMIO地址空間(MMIO是物理地址空間的子集),並且也瞭解了是PCI主橋設備將這些PCI device和系統連接在一起。PCI設備會有BAR(base address register),表示自己在PCI總線上的地址,CPU並不能通過總線地址A(位於BAR範圍內)直接訪問總線上的PCI設備,PCI host bridge會在MMIO(即物理地址)和總線地址之間進行mapping。因此,對於CPU,它實際上是可以通過B地址(位於MMIO地址空間)訪問PCI設備(反正PCI host bridge會進行翻譯)。地址B的信息保存在struct resource變量中,並可以通過/proc/iomem開放給用戶空間。對於驅動程序,它往往是通過ioremap()把物理地址B映射成虛擬地址C,這時候,驅動程序就可以通過ioread32(C)來訪問PCI總線上的地址A了。

如果PCI設備支持DMA,那麼在驅動中我們可以通過kmalloc或者其他類似接口分配一個DMA buffer,並且返回了虛擬地址X,MMU將X地址映射成了物理地址Y,從而定位了DMA buffer在系統內存中的位置。因此,驅動可以通過訪問地址X來操作DMA buffer,但是PCI 設備並不能通過X地址來訪問DMA buffer,因爲MMU對設備不可見,而且系統內存所在的系統總線和PCI總線屬於不同的地址空間。

在一些簡單的系統中,設備可以通過DMA直接訪問物理地址Y,但是在大多數的系統中,有一個IOMMU的硬件block用來將DMA可訪問的總線地址翻譯成物理地址,也就是把上圖中的地址Z翻譯成Y。理解了這些底層硬件,你也就知道類似dma_map_single這樣的DMA API是在做什麼了。驅動在調用dma_map_single這樣的接口函數的時候會傳遞一個虛擬地址X,在這個函數中會設定IOMMU的頁表,將地址X映射到Z,並且將返回z這個總線地址。驅動可以把Z這個總線地址設定到設備上的DMA相關的寄存器中。這樣,當設備發起對地址Z開始的DMA操作的時候,IOMMU可以進行地址映射,並將DMA操作定位到Y地址開始的DMA buffer。

根據上面的描述我們可以得出這樣的結論:Linux可以使用動態DMA 映射(dynamic DMA mapping)的方法,當然,這需要一些來自驅動的協助。所謂動態DMA 映射是指只有在使用的時候,才建立DMA buffer虛擬地址到總線地址的映射,一旦DMA傳輸完畢,就將之前建立的映射關係銷燬。

雖然上面的例子使用IOMMU爲例描述,不過本文隨後描述的API也可以在沒有IOMMU硬件的平臺上運行。

順便說明一點:DMA API適用於各種CPU arch,各種總線類型,DMA mapping framework已經屏蔽了底層硬件的細節。對於驅動工程師而言,你應該使用通用的DMA API(例如dma_map_*() 接口函數),而不是和特定總線相關的API(例如pci_map_*() 接口函數)。

驅動想要使用DMA mapping framework的API,需要首先包含相關頭文件:

#include <linux/dma-mapping.h>

這個頭文件中定義了dma_addr_t這種數據類型,而這種類型的變量可以保存任何有效的DMA地址,不管是什麼總線,什麼樣的CPU arch。驅動調用了DMA API之後,返回的DMA地址(總線地址)就是這種類型的。

 

三、什麼樣的系統內存可以被DMA控制器訪問到?

既然驅動想要使用DMA mapping framework提供的接口,我們首先需要知道的就是是否所有的系統內存都是可以調用DMA API進行mapping?還是隻有一部分?那麼這些可以DMA控制器訪問系統內存有什麼特點?關於這一點,一直以來有一些不成文的規則,在本文中我們看看是否能夠將其全部記錄下來。

如果驅動是通過夥伴系統的接口(例如__get_free_page*())或者類似kmalloc() or kmem_cache_alloc()這樣的通用內存分配的接口來分配DMA buffer,那麼這些接口函數返回的虛擬地址可以直接用於DMA mapping接口API,並通過DMA操作在外設和dma buffer中交換數據。

使用vmalloc() 分配的DMA buffer可以直接使用嗎?最好不要這樣,雖然強行使用也沒有問題,但是終究是比較麻煩。首先,vmalloc分配的page frame是不連續的,如果底層硬件需要物理內存連續,那麼vmalloc分配的內存不能滿足硬件要求。即便是底層DMA硬件支持scatter-gather,vmalloc分配出來的內存仍然存在其他問題。我們知道vmalloc分配的虛擬地址和對應的物理地址沒有線性關係(kmalloc或者__get_free_page*這樣的接口,其返回的虛擬地址和物理地址有一個固定偏移的關係),而在做DMA mapping的時候,需要知道物理地址,有線性關係的虛擬地址很容易可以獲取其物理地址,但是對於vmalloc分配的虛擬地址,我們需要遍歷頁表纔可以找到其物理地址。

在驅動中定義的全局變量可以用於DMA嗎?如果編譯到內核,那麼全局變量位於內核的數據段或者bss段。在內核初始化的時候,會建立kernel image mapping,因此全局變量所佔據的內存都是連續的,並且VA和PA是有固定偏移的線性關係,因此可以用於DMA操作。不過,在定義這些全局變量的DMA buffer的時候,我們要小心的進行cacheline的對齊,並且要處理CPU和DMA controller之間的操作同步,以避免cache coherence問題。

如果驅動編譯成模塊會怎麼樣呢?這時候,驅動中的全局定義的DMA buffer不在內核的線性映射區域,其虛擬地址是在模塊加載的時候,通過vmalloc分配,因此這時候如果DMA buffer如果大於一個page frame,那麼實際上我們也是無法保證其底層物理地址的連續性,也無法保證VA和PA的線性關係,這一點和編譯到內核是不同的。

通過kmap接口返回的內存可以做DMA buffer嗎?也不行,其原理類似vmalloc,這裏就不贅述了。

塊設備使用的I/O buffer和網絡設備收發數據的buffer是如何確保其內存是可以進行DMA操作的呢?塊設備I/O子系統和

網絡子系統在分配buffer的時候會確保這一點的。

 

四、DMA尋址限制

你的設備有DMA尋址限制嗎?不同的硬件平臺有不同的配置方式,有的平臺沒有限制,外設可以訪問系統內存的每一個Byte,有些則不可以。例如:系統總線有32個bit,而你的設備通過DMA只能驅動低24位地址,在這種情況下,外設在發起DMA操作的時候,只能訪問16M以下的系統內存。如果設備有DMA尋址的限制,那麼驅動需要將這個限制通知到內核。如果驅動不通知內核,那麼內核缺省情況下認爲外設的DMA可以訪問所有的系統總線的32 bit地址線。對於64 bit平臺,情況類似,不再贅述。

是否有DMA尋址限制是和硬件設計相關,有時候標準總線協議也會規定這一點。例如:PCI-X規範規定,所有的PCI-X設備必須要支持64 bit的尋址。

如果有尋址限制,那麼在該外設驅動的probe函數中,你需要詢問內核,看看是否有DMA controller可以支持這個外設的尋址限制。雖然有缺省的尋址限制的設定,不過最好還是在probe函數中進行相關處理,至少這說明你已經爲你的外設考慮過尋址限制這事了。

一旦確定了設備DMA尋址限制之後,我們可以通過下面的接口進行設定:

int dma_set_mask_and_coherent(struct device *dev, u64 mask);

根據DMA buffer的特性,DMA操作有兩種:一種是streaming,DMA buffer是一次性的,用完就算。這種DMA buffer需要自己考慮cache一致性。另外一種是DMA buffer是cache coherent的,軟件實現上比較簡單,更重要的是這種DMA buffer往往是靜態的、長時間存在的。不同類型的DMA操作可能有有不同的尋址限制,也可能相同。如果相同,我們可以用上面這個接口設定streaming和coherent兩種DMA 操作的地址掩碼。如果不同,可以下面的接口進行設定:

int dma_set_mask(struct device *dev, u64 mask);

int dma_set_coherent_mask(struct device *dev, u64 mask);

前者是設定streaming類型的DMA地址掩碼,後者是設定coherent類型的DMA地址掩碼。爲了更好的理解這些接口,我們聊聊參數和返回值。dev指向該設備的struct device對象,一般來說,這個struct device對象應該是嵌入在bus-specific 的實例中,例如對於PCI設備,有一個struct pci_dev的實例與之對應,而在這裏需要傳入的dev參數則可以通過&pdev->dev得到(pdev指向struct pci_dev的實例)。mask表示你的設備支持的地址線信息。如果調用這些接口返回0,則說明一切OK,從該設備到指定mask的內存的DMA操作是可以被系統支持的(包括DMA controller、bus layer等)。如果返回值非0,那麼說明這樣的DMA尋址是不能正確完成的,如果強行這麼做將會產生不可預知的後果。驅動必須檢測返回值,如果不行,那麼建議修改mask或者不使用DMA。也就是說,對上面接口調用失敗後,你有三個選擇:

1、用另外的mask

2、不使用DMA模式,採用普通I/O模式

3、忽略這個設備的存在,不對其進行初始化

一個可以尋址32 bit的設備,其初始化的示例代碼如下:

if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32))) {
    dev_warn(dev, "mydev: No suitable DMA available\n");
    goto ignore_this_device;
}

另一個常見的場景是有64位尋址能力的設備。一般來說我們會首先嚐試設定64位的地址掩碼,但是這時候有可能會失敗,從而將掩碼降低爲32位。內核之所以會在設定64位掩碼的時候失敗,這並不是因爲平臺不能進行64位尋址,而僅僅是因爲32位尋址比64位尋址效率更高。例如,SPARC64 平臺上,PCI SAC尋址比DAC尋址性能更好。

下面的代碼描述瞭如何確定streaming類型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 {
    dev_warn(dev, "mydev: No suitable DMA available\n");
    goto ignore_this_device;
}

設定coherent 類型的DMA地址掩碼也是類似的,不再贅述。需要說明的是:coherent地址掩碼總是等於或者小於streaming地址掩碼,因此,一般來說,我們只要設定了streaming地址掩碼成功了,那麼使用同樣的掩碼或者小一些的掩碼來設定coherent地址掩碼總是會成功,因此這時候我們一般就不檢查dma_set_coherent_mask的返回值了,當然,有些設備很奇怪,只能使用coherent DMA,那麼這種情況下,驅動需要檢查dma_set_coherent_mask的返回值。

 

五、兩種類型的DMA mapping

1、一致性DMA映射(Consistent DMA mappings )

Consistent DMA mapping有下面兩種特點:

(1)持續使用該DMA buffer(不是一次性的),因此Consistent DMA總是在初始化的時候進行map,在shutdown的時候unmap。

(2)CPU和DMA controller在發起對DMA buffer的並行訪問的時候不需要考慮cache的影響,也就是說不需要軟件進行cache操作,CPU和DMA controller都可以看到對方對DMA buffer的更新。實際上一致性DMA映射中的那個Consistent實際上可以稱爲coherent,即cache coherent。

缺省情況下,coherent mask被設定爲低32 bit(0xFFFFFFFF),即便缺省值是OK了,我們也建議你通過接口在驅動中設定coherent mask。

一般使用Consistent DMA mapping的場景包括:

(1)網卡驅動和網卡DMA控制器往往是通過一些內存中的描述符(形成環或者鏈)進行交互,這些保存描述符的memory一般採用Consistent DMA mapping。

(2)SCSI硬件適配器上的DMA可以主存中的一些數據結構(mailbox command)進行交互,這些保存mailbox command的memory一般採用Consistent DMA mapping。

(3)有些外設有能力執行主存上的固件代碼(microcode),這些保存microcode的主存一般採用Consistent DMA mapping。

上面的這些例子有同樣的特性:CPU對memory的修改可以立刻被device感知到,反之亦然。一致性映射可以保證這一點。

需要注意的是:一致性的DMA映射並不意味着不需要memory barrier這樣的工具來保證memory order,CPU有可能爲了性能而重排對consistent memory上內存訪問指令。例如:如果在DMA consistent memory上有兩個word,分別是word0和word1,對於device一側,必須保證word0先更新,然後纔有對word1的更新,那麼你需要這樣寫代碼:

       desc->word0 = address;
        wmb();
        desc->word1 = DESC_VALID;

只有這樣才能保證在所有的平臺上,給設備驅動可以正常的工作。

此外,在有些平臺上,修改了DMA Consistent buffer後,你的驅動可能需要flush write buffer,以便讓device側感知到memory的變化。這個動作類似在PCI橋中的flush write buffer的動作。

2、流式DMA映射(streaming DMA mapping)

流式DMA映射是一次性的,一般是需要進行DMA傳輸的時候才進行mapping,一旦DMA傳輸完成,就立刻ummap(除非你使用dma_sync_*的接口,下面會描述)。並且硬件可以爲順序化訪問進行優化。

這裏的streaming可以被認爲是asynchronous,或者是不屬於coherent memory範圍的。

一般使用streaming DMA mapping的場景包括:

(1)網卡進行數據傳輸使用的DMA buffer

(2)文件系統中的各種數據buffer,這些buffer中的數據最終到讀寫到SCSI設備上去,一般而言,驅動會接受這些buffer,然後進行streaming DMA mapping,之後和SCSI設備上的DMA進行交互。

設計streaming DMA mapping這樣的接口是爲了充分優化硬件的性能,爲了打到這個目標,在使用這些接口的時候,你必須清清楚楚的知道調用接口會發生什麼。

無論哪種類型的DMA映射都有對齊的限制,這些限制來自底層的總線,當然也有可能是某些總線上的設備有這樣的限制。此外,如果系統中的cache並不是DMA coherent的,而且底層的DMA buffer不合其他數據共享cacheline,這樣的系統將工作的更好。

 

六、如何使用coherent DMA mapping的接口?

1、分配並映射dma buffer

爲了分配並映射一個較大(page大小或者類似)的coherent DMA memory,你需要調用下面的接口:

   dma_addr_t dma_handle;

    cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, gfp);

DMA操作總是會涉及具體設備上的DMA controller,而dev參數就是執行該設備的struct device對象的。size參數指明瞭你想要分配的DMA Buffer的大小,byte爲單位。dma_alloc_coherent這個接口也可以在中斷上下文調用,當然,gfp參數要傳遞GFP_ATOMIC標記,gfp是內存分配的flag,dma_alloc_coherent僅僅是透傳該flag到內存管理模塊。

需要注意的是dma_alloc_coherent分配的內存的起始地址和size都是對齊在page上(類似__get_free_pages的感覺,當然__get_free_pages接受的size參數是page order),如果你的驅動不需要那麼大的DMA buffer,那麼可以選擇dma_pool接口,下面會進一步描述。

如果傳入非空的dev參數,即使驅動調用了掩碼設置接口函數設定了DMA mask,說明該設備可以訪問大於32-bit地址空間的地址,一致性DMA映射的接口函數也一般會默認的返回一個32-bit可尋址的DMA buffer地址。要知道dma mask和coherent dma mask是不同的,除非驅動顯示的調用dma_set_coherent_mask()接口來修改coherent dma mask,例如大小大於32-bit地址,dma_alloc_coherent接口函數纔會返回大於32-bit地址空間的地址。dma pool接口也是如此。

dma_alloc_coherent函數返回兩個值,一個是從CPU角度訪問DMA buffer的虛擬地址,另外一個是從設備(DMA controller)角度看到的bus address:dma_handle,驅動可以將這個bus address傳遞給HW。

即便是請求的DMA buffer的大小小於PAGE SIZE,dma_alloc_coherent返回的cpu虛擬地址和DMA總線地址都保證對齊在最小的PAGE_SIZE上,這個特性確保了分配的DMA buffer有這樣的特性:如果page size是64K,即便是驅動分配一個小於或者等於64K的dma buffer,那麼DMA buffer不會越過64K的邊界。

2、umap並釋放dma buffer

當驅動需要umap並釋放dma buffer的時候,需要調用下面的接口:

dma_free_coherent(dev, size, cpu_addr, dma_handle);

這個接口函數的dev、size參數上面已經描述過了,而cpu_addr和dma_handle這兩個參數就是dma_alloc_coherent() 接口的那兩個地址返回值。需要強調的一點就是:和dma_alloc_coherent不同,dma_free_coherent不能在中斷上下文中調用。(因爲在有些平臺上,free DMA的操作會引發TLB維護的操作(從而引發cpu core之間的通信),如果關閉了IRQ會鎖死在SMP IPI 的代碼中)。

3、dma pool

如果你的驅動需非常多的小的dma buffer,那麼dma pool是最適合你的機制。這個概念類似kmem_cache,__get_free_pages往往獲取的是連續的page frame,而kmem_cache是批發了一大批page frame,然後自己“零售”。dma pool就是通過dma_alloc_coherent接口獲取大塊一致性的DMA內存,然後驅動可以調用dma_pool_alloc從那個大塊DMA內存中分一個小塊的dma buffer供自己使用。具體接口描述就不說了,大家可以自行閱讀。

 

七、DMA操作方向

由於下面的章節會用到DMA操作方向這個概念,因此我們先簡單的描述一下,DMA操作方向定義如下:

DMA_BIDIRECTIONAL
DMA_TO_DEVICE
DMA_FROM_DEVICE
DMA_NONE

如果你知道的話,你應該儘可能的提供準確的DMA操作方向。

DMA_TO_DEVICE表示“從內存(dma buffer)到設備”,而 DMA_FROM_DEVICE表示“從設備到內存(dma buffer)”,上面的這些字符定義了數據在DMA操作中的移動方向。

雖然我們強烈要求驅動在知道DMA傳輸方向的適合,精確的指明是DMA_TO_DEVICE或者DMA_FROM_DEVICE,然而,如果你確實是不知道具體的操作方向,那麼設定爲DMA_BIDIRECTIONAL也是可以的,表示DMA操作可以執行任何一個方向的的數據搬移。你的平臺需要保證這一點可以讓DMA正常工作,當然,這也有可能會引入一些性能上的額外開銷。

DMA_NONE主要是用於調試。在驅動知道精確的DMA方向之前,可以把它保存在DMA控制數據結構中,在dma方向設定有問題的適合,你可以跟蹤dma方向的設置情況,以便定位問題所在。

除了潛在的平臺相關的性能優化之外,精確地指定DMA操作方向還有另外一個優點就是方便調試。有些平臺實際上在創建DMA mapping的時候,頁表(指將bus地址映射到物理地址的頁表)中有一個寫權限布爾值,這個值非常類似於用戶程序地址空間中的頁保護。當DMA控制器硬件檢測到違反權限設置時(這時候dma buffer設定的是MA_TO_DEVICE類型,實際上DMA controller只能是讀dma buffer),這樣的平臺可以將錯誤寫入內核日誌,從而方便了debug。

只有streaming mappings纔會指明DMA操作方向,一致性DMA映射隱含的DMA操作方向是DMA_BIDIRECTIONAL。我們舉一個streaming mappings的例子:在網卡驅動中,如果要發送數據,那麼在map/umap的時候需要指明DMA_TO_DEVICE的操作方向,而在接受數據包的時候,map/umap需要指明DMA操作方向是DMA_FROM_DEVICE。

 

八、如何使用streaming DMA mapping的接口?

streaming DMA mapping的接口函數可以在中斷上下文中調用。streaming DMA mapping有兩個版本的接口函數,一個是用來map/umap單個的dma buffer,另外一個是用來map/umap形成scatterlist的多個dma buffer。

1、map/umap單個的dma buffer

map單個的dma buffer的示例如下:

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)) {
    goto map_error_handling;
}

umap單個的dma buffer可以使用下面的接口:

dma_unmap_single(dev, dma_handle, size, direction);

當調用dma_map_single()返回錯誤的時候,你應當調用dma_mapping_error()來處理錯誤。雖然並不是所有的DMA mapping實現都支持dma_mapping_error這個接口(調用dma_mapping_error函數實際上會調用底層dma_map_ops操作函數集中的mapping_error成員函數),但是調用它來進行出錯處理仍然是一個好的做法。這樣做的好處是可以確保DMA mapping代碼在所有DMA實現中都能正常工作,而不需要依賴底層實現的細節。沒有檢查錯誤就使用返回的地址可能會導致程序失敗,可能會產生kernel panic或者悄悄的損壞你有用的數據。下面列舉了一些不正確的方法來檢查DMA mapping錯誤,之所以是錯誤的方法是因爲這些代碼對底層的DMA實現進行了假設。順便說的是雖然這裏是使用dma_map_single作爲示例,實際上也是適用於dma_map_page()的。

錯誤示例一:

dma_addr_t dma_handle;

dma_handle = dma_map_single(dev, addr, size, direction);
if ((dma_handle & 0xffff != 0) || (dma_handle >= 0x1000000)) {
    goto map_error;
}

錯誤示例二:

dma_addr_t dma_handle;

dma_handle = dma_map_single(dev, addr, size, direction);
if (dma_handle == DMA_ERROR_CODE) {
    goto map_error;
}

當DMA傳輸完成的時候,程序應該調用dma_unmap_single()函數umap dma buffer。例如:在DMA完成傳輸後會通過中斷通知CPU,而在interrupt handler中可以調用dma_unmap_single()函數。dma_map_single函數在進行DMA mapping的時候使用的是CPU指針(虛擬地址),這樣就導致該函數有一個弊端:不能使用HIGHMEM memory進行mapping。鑑於此,map/unmap接口提供了另外一個類似的接口,這個接口不使用CPU指針,而是使用page和page offset來進行DMA mapping:

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)) {
    goto map_error_handling;
}

...

dma_unmap_page(dev, dma_handle, size, direction);

在上面的代碼中,offset表示一個指定page內的頁內偏移(以Byte爲單位)。和dma_map_single接口函數一樣,調用dma_map_page()返回錯誤後需要調用dma_mapping_error() 來進行錯誤處理,上面都已經描述了,這裏不再贅述。當DMA傳輸完成的時候,程序應該調用dma_unmap_page()函數umap dma buffer。例如:在DMA完成傳輸後會通過中斷通知CPU,而在interrupt handler中可以調用dma_unmap_page()函數。

2、map/umap多個形成scatterlist的dma buffer

在scatterlist的情況下,你要映射的對象是分散的若干段DMA buffer,示例代碼如下:

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中條目的數量(即map多少段dma buffer)。

具體DMA映射的實現是自由的,它可以把scatterlist 中的若干段連續的DMA buffer映射成一個大塊的,連續的bus address region。例如:如果DMA mapping是以PAGE_SIZE爲粒度進行映射,那麼那些分散的一塊塊的dma buffer可以被映射到一個對齊在PAGE_SIZE,然後各個dma buffer依次首尾相接的一個大的總線地址區域上。這樣做的好處就是對於那些不支持(或者支持有限)scatter-gather 的DMA controller,仍然可以通過mapping來實現。dma_map_sg調用識別的時候返回0,當調用成功的時候,返回成功mapping的數目。

一旦調用成功,你需要調用for_each_sg來遍歷所有成功映射的mappings(這個數目可能會小於nents)並且使用sg_dma_address() 和 sg_dma_len() 這兩個宏來得到mapping後的dma地址和長度。

umap多個形成scatterlist的dma buffer是通過下面的接口實現的:

dma_unmap_sg(dev, sglist, nents, direction);

再次強調,調用dma_unmap_sg的時候要確保DMA操作已經完成。另外,傳遞給dma_unmap_sg的nents參數需要等於傳遞給dma_map_sg的nents參數,而不是該函數返回的count。

由於DMA地址空間是共享資源,每一次dma_map_{single,sg}() 的調用都需要有其對應的dma_unmap_{single,sg}(),如果你總是分配dma地址資源而不回收,那麼系統將會由於DMA address被用盡而陷入不可用的狀態。

3、sync操作

如果你需要多次訪問同一個streaming DMA buffer,並且在DMA傳輸之間讀寫DMA Buffer上的數據,這時候你需要小心進行DMA buffer的sync操作,以便CPU和設備(DMA controller)可以看到最新的、正確的數據。

首先用dma_map_{single,sg}()進行映射,在完成DMA傳輸之後,用:

dma_sync_single_for_cpu(dev, dma_handle, size, direction);

或者:

dma_sync_sg_for_cpu(dev, sglist, nents, direction);

   來完成sync的操作,以便CPU可以看到最新的數據。

如果,CPU操作了DMA buffer的數據,然後你又想把控制權交給設備上的DMA 控制器,讓DMA controller訪問DMA buffer,這時候,在真正讓HW(指DMA控制器)去訪問DMA buffer之前,你需要調用:

dma_sync_single_for_device(dev, dma_handle, size, direction);

或者:

dma_sync_sg_for_device(dev, sglist, nents, direction);

以便device(也就是設備上的DMA控制器)可以看到cpu更新後的數據。此外,需要強調的是:傳遞給dma_sync_sg_for_cpu() 和 dma_sync_sg_for_device()的ents參數需要等於傳遞給dma_map_sg的nents參數,而不是該函數返回的count。

在完成最後依次DMA傳輸之後,你需要調用DMA unmap函數dma_unmap_{single,sg}()。如果在第一次dma_map_*() 調用和dma_unmap_*()之間,你從來都沒有碰過DMA buffer中的數據,那麼你根本不需要調用dma_sync_*() 這樣的sync操作。

下面的例子給出了一個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)) {
        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;

HW已經完成了傳輸,在cpu訪問buffer之前,cpu需要先sync一下,以便看到最新的數據。
        dma_sync_single_for_cpu(&cp->dev, cp->rx_dma,
                    cp->rx_len,
                    DMA_FROM_DEVICE);

sync之後就可以安全的讀dma 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 {
            give_rx_buf_to_card(cp);
        }
    }
}

當使用了這套DMA mapping接口後,驅動不應該再使用virt_to_bus() 這個接口了,當然bus_to_virt()也不行。不過,如果你的驅動使用了這些接口怎麼辦呢?其實這套新的DMA mapping接口沒有和virt_to_bus、bus_to_virt()一一對應的接口,因此,爲了讓你的程序能工作,你需要對驅動程序進行小小的修改:你必須要保存從dma_alloc_coherent()、dma_pool_alloc()以及dma_map_single()接口函數返回的dma address(對於dma_map_sg()這個接口,dma地址保存在scatterlist 中,當然這需要硬件支持dynamic DMA mapping ),並把這個dma address保存在驅動的數據結構中,並且同時/或者保存在硬件的寄存器中。

所有的驅動代碼都需要遷移到DMA mapping framework的接口函數上來。目前內核已經計劃完全移除virt_to_bus() 和bus_to_virt() 這兩個函數,因爲它們已經過時了。有些平臺由於不能正確的支持virt_to_bus() 和bus_to_virt(),因此根本就沒有提供這兩個接口。


九、錯誤處理

DMA地址空間在某些CPU架構上是有限的,因此分配並mapping可能會產生錯誤,我們可以通過下面的方法來判定是否發生了錯誤:

(1)檢查是否dma_alloc_coherent() 返回了NULL或者dma_map_sg 返回0

(2)檢查dma_map_single和dma_map_page返回了dma address(通過dma_mapping_error函數)

   dma_addr_t dma_handle;

    dma_handle = dma_map_single(dev, addr, size, direction);
    if (dma_mapping_error(dev, dma_handle)) {
        goto map_error_handling;
    }

(3)當在mapping多個page的時候,如果中間發生了mapping error,那麼需要對那些已經mapped的page進行unmap的操作。下面的示例代碼用dma_map_single函數,對於dma_map_page也一樣適用。

示例代碼一:

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)) {
    goto map_error_handling1;
}
dma_handle2 = dma_map_single(dev, addr, size, direction);
if (dma_mapping_error(dev, dma_handle2)) {
    goto map_error_handling2;
}

...

map_error_handling2:
    dma_unmap_single(dma_handle1);
map_error_handling1:

示例代碼二(如果我們在循環中mapping dma buffer,當在中間出錯的時候,一樣要unmap所有已經映射的dma buffer):

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)) {
        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);
}

如果在網卡驅動的tx回調函數(例如ndo_start_xmit)中出現了DMA mapping失敗,那麼驅動必須調用dev_kfree_skb() 來是否socket buffer並返回NETDEV_TX_OK 。這表示這個socket buffer由於錯誤而丟棄掉了。

如果在SCSI driver的queue command回調函數中出現了DMA mapping失敗,那麼驅動必須返回SCSI_MLQUEUE_HOST_BUSY 。這意味着SCSI子系統稍後會再次重傳該command給driver。

 

十、優化數據結構

在很多的平臺上,dma_unmap_{single,page}()其實什麼也沒有做,是空函數。因此,跟蹤映射的dma address及其長度基本上就是浪費內存空間。爲了方便驅動工程師編寫代碼方便,我們提供了幾個實用工具(宏定義),如果沒有它們,驅動程序中將充分ifdef或者類似的一些“work around”。下面我們並不是一個個的介紹這些宏定義,而是給出一些示例代碼,驅動工程師可以照葫蘆畫瓢。

1、DEFINE_DMA_UNMAP_{ADDR,LEN}。在DMA buffer數據結構中使用這個宏定義,具體例子如下:

before:

    struct ring_state {
        struct sk_buff *skb;
        dma_addr_t mapping;
        __u32 len;
    };

   after:

    struct ring_state {
        struct sk_buff *skb;
        DEFINE_DMA_UNMAP_ADDR(mapping);
        DEFINE_DMA_UNMAP_LEN(len);
    };

 

根據CONFIG_NEED_DMA_MAP_STATE的配置不同,DEFINE_DMA_UNMAP_{ADDR,LEN}可能是定義相關的dma address和長度的成員,也可能是空。

2、dma_unmap_{addr,len}_set()。使用該宏定義來賦值,具體例子如下:

before:

    ringp->mapping = FOO;
    ringp->len = BAR;

   after:

    dma_unmap_addr_set(ringp, mapping, FOO);
    dma_unmap_len_set(ringp, len, BAR);

3、dma_unmap_{addr,len}(),使用該宏來訪問變量。

before:

    dma_unmap_single(dev, ringp->mapping, ringp->len,
             DMA_FROM_DEVICE);

   after:

    dma_unmap_single(dev,
             dma_unmap_addr(ringp, mapping),
             dma_unmap_len(ringp, len),
             DMA_FROM_DEVICE);

上面的這些代碼基本是不需要解釋你就會明白的了。另外,我們對於dma address和len是分開處理的,因爲在有些實現中,unmaping的操作僅僅需要dma address信息就夠了。

 

十一、平臺移植需要注意的問題

如果你僅僅是驅動工程師,並不負責將linux遷移到某個cpu arch上去,那麼後面的內容其實你可以忽略掉了。

1、Struct scatterlist的需求

如果cpu arch支持IOMMU(包括軟件模擬的IOMMU),那麼你需要打開CONFIG_NEED_SG_DMA_LENGTH 這個內核選項。

2、ARCH_DMA_MINALIGN

CPU體系結構相關的代碼必須要要保證kmalloc分配的buffer是DMA-safe的(kmalloc分配的buffer也是有可能用於DMA buffer),驅動和內核子系統的正確運行都是依賴這個條件的。如果一個cpu arch不是全面支持DMA-coherent的(例如硬件並不保證cpu cache中的數據等於main memory中的數據),那麼必須定義ARCH_DMA_MINALIGN。而通過這個宏定義,kmalloc分配的buffer可以保證對齊在ARCH_DMA_MINALIGN上,從而保證了kmalloc分配的DMA Buffer不會和其他的buffer共享一個cacheline。想要了解具體的實例可以參考arch/arm/include/asm/cache.h。

另外,請注意:ARCH_DMA_MINALIGN 是DMA buffer的對齊約束,你不需要擔心CPU ARCH的數據對齊約束(例如,有些CPU arch要求有些數據對象需要64-bit對齊)。

 

十二、後記

如果沒有來自廣大人民羣衆的反饋和建議,這份文檔(包括DMA API本身)可能會顯得過時,陳舊。

此外,對這份文檔有幫助的人如下(沒有按照什麼特別的順序):

Russell King <[email protected]>
Leo Dagum <[email protected]>
Ralf Baechle <[email protected]>
Grant Grundler <[email protected]>
Jay Estabrook <[email protected]>
Thomas Sailer <[email protected]>
Andrea Arcangeli <[email protected]>
Jens Axboe <[email protected]>
David Mosberger-Tang [email protected]

 

備註:本文基本上是內核文檔DMA-API-HOWTO.txt的翻譯,如果有興趣可以參考原文。

發佈了13 篇原創文章 · 獲贊 7 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章