Dynamic DMA mapping Guide
一、前言
這是一篇指導(dǎo)驅(qū)動(dòng)工程師如何使用DMA API的文檔,為了方便理解,文檔中給出了偽代碼的例程。另外一篇文檔dma-api.txt給出了相關(guān)API的簡(jiǎn)明描述,有興趣也可以看看那一篇,這兩份文檔在DMA API的描述方面是一致的。
二、從CPU角度看到的地址和從DMA控制器看到的地址有什么不同?
在DMA API中涉及好幾個(gè)地址的概念(物理地址、虛擬地址和總線地址),正確的理解這些地址是非常重要的。
內(nèi)核通常使用的地址是虛擬地址。我們調(diào)用kmalloc()、vmalloc()或者類似的接口返回的地址都是虛擬地址,保存在"void *"的變量中。
虛擬內(nèi)存系統(tǒng)(TLB、頁表等)將虛擬地址(程序角度)翻譯成物理地址(CPU角度),物理地址保存在“phys_addr_t”或“resource_size_t”的變量中。對(duì)于一個(gè)硬件設(shè)備上的寄存器等設(shè)備資源,內(nèi)核是按照物理地址來管理的。通過/proc/iomem,你可以看到這些和設(shè)備IO 相關(guān)的物理地址。當(dāng)然,驅(qū)動(dòng)并不能直接使用這些物理地址,必須首先通過ioremap()接口將這些物理地址映射到內(nèi)核虛擬地址空間上去。
I/O設(shè)備使用第三種地址:“總線地址”。如果設(shè)備在MMIO地址空間中有若干的寄存器,或者該設(shè)備足夠的智能,它可以通過DMA執(zhí)行讀寫系統(tǒng)內(nèi)存的操作,這些情況下,設(shè)備使用的地址就是總線地址。在某些系統(tǒng)中,總線地址與CPU物理地址相同,但一般來說它們不是。iommus和host bridge可以在物理地址和總線地址之間進(jìn)行映射。
從設(shè)備的角度來看,DMA控制器使用總線地址空間,不過可能僅限于總線空間的一個(gè)子集。例如:即便是一個(gè)系統(tǒng)支持64位地址內(nèi)存和64 位地址的PCI bar,但是DMA可以不使用全部的64 bit地址,通過IOMMU的映射,PCI設(shè)備上的DMA可以只使用32位DMA地址。
我們用下面這樣的系統(tǒng)結(jié)構(gòu)來說明各種地址的概念:
在PCI設(shè)備枚舉(初始化)過程中,內(nèi)核了解了所有的IO device及其對(duì)應(yīng)的MMIO地址空間(MMIO是物理地址空間的子集),并且也了解了是PCI主橋設(shè)備將這些PCI device和系統(tǒng)連接在一起。PCI設(shè)備會(huì)有BAR(base address register),表示自己在PCI總線上的地址,CPU并不能通過總線地址A(位于BAR范圍內(nèi))直接訪問總線上的PCI設(shè)備,PCI host bridge會(huì)在MMIO(即物理地址)和總線地址之間進(jìn)行mapping。因此,對(duì)于CPU,它實(shí)際上是可以通過B地址(位于MMIO地址空間)訪問PCI設(shè)備(反正PCI host bridge會(huì)進(jìn)行翻譯)。地址B的信息保存在struct resource變量中,并可以通過/proc/iomem開放給用戶空間。對(duì)于驅(qū)動(dòng)程序,它往往是通過ioremap()把物理地址B映射成虛擬地址C,這時(shí)候,驅(qū)動(dòng)程序就可以通過ioread32(C)來訪問PCI總線上的地址A了。
如果PCI設(shè)備支持DMA,那么在驅(qū)動(dòng)中我們可以通過kmalloc或者其他類似接口分配一個(gè)DMA buffer,并且返回了虛擬地址X,MMU將X地址映射成了物理地址Y,從而定位了DMA buffer在系統(tǒng)內(nèi)存中的位置。因此,驅(qū)動(dòng)可以通過訪問地址X來操作DMA buffer,但是PCI 設(shè)備并不能通過X地址來訪問DMA buffer,因?yàn)镸MU對(duì)設(shè)備不可見,而且系統(tǒng)內(nèi)存所在的系統(tǒng)總線和PCI總線屬于不同的地址空間。
在一些簡(jiǎn)單的系統(tǒng)中,設(shè)備可以通過DMA直接訪問物理地址Y,但是在大多數(shù)的系統(tǒng)中,有一個(gè)IOMMU的硬件block用來將DMA可訪問的總線地址翻譯成物理地址,也就是把上圖中的地址Z翻譯成Y。理解了這些底層硬件,你也就知道類似dma_map_single這樣的DMA API是在做什么了。驅(qū)動(dòng)在調(diào)用dma_map_single這樣的接口函數(shù)的時(shí)候會(huì)傳遞一個(gè)虛擬地址X,在這個(gè)函數(shù)中會(huì)設(shè)定IOMMU的頁表,將地址X映射到Z,并且將返回z這個(gè)總線地址。驅(qū)動(dòng)可以把Z這個(gè)總線地址設(shè)定到設(shè)備上的DMA相關(guān)的寄存器中。這樣,當(dāng)設(shè)備發(fā)起對(duì)地址Z開始的DMA操作的時(shí)候,IOMMU可以進(jìn)行地址映射,并將DMA操作定位到Y(jié)地址開始的DMA buffer。
根據(jù)上面的描述我們可以得出這樣的結(jié)論:Linux可以使用動(dòng)態(tài)DMA 映射(dynamic DMA mapping)的方法,當(dāng)然,這需要一些來自驅(qū)動(dòng)的協(xié)助。所謂動(dòng)態(tài)DMA 映射是指只有在使用的時(shí)候,才建立DMA buffer虛擬地址到總線地址的映射,一旦DMA傳輸完畢,就將之前建立的映射關(guān)系銷毀。
雖然上面的例子使用IOMMU為例描述,不過本文隨后描述的API也可以在沒有IOMMU硬件的平臺(tái)上運(yùn)行。
順便說明一點(diǎn):DMA API適用于各種CPU arch,各種總線類型,DMA mapping framework已經(jīng)屏蔽了底層硬件的細(xì)節(jié)。對(duì)于驅(qū)動(dòng)工程師而言,你應(yīng)該使用通用的DMA API(例如dma_map_*() 接口函數(shù)),而不是和特定總線相關(guān)的API(例如pci_map_*() 接口函數(shù))。
驅(qū)動(dòng)想要使用DMA mapping framework的API,需要首先包含相關(guān)頭文件:
這個(gè)頭文件中定義了dma_addr_t這種數(shù)據(jù)類型,而這種類型的變量可以保存任何有效的DMA地址,不管是什么總線,什么樣的CPU arch。驅(qū)動(dòng)調(diào)用了DMA API之后,返回的DMA地址(總線地址)就是這種類型的。
三、什么樣的系統(tǒng)內(nèi)存可以被DMA控制器訪問到?
既然驅(qū)動(dòng)想要使用DMA mapping framework提供的接口,我們首先需要知道的就是是否所有的系統(tǒng)內(nèi)存都是可以調(diào)用DMA API進(jìn)行mapping?還是只有一部分?那么這些可以DMA控制器訪問系統(tǒng)內(nèi)存有什么特點(diǎn)?關(guān)于這一點(diǎn),一直以來有一些不成文的規(guī)則,在本文中我們看看是否能夠?qū)⑵淙坑涗浵聛怼?
如果驅(qū)動(dòng)是通過伙伴系統(tǒng)的接口(例如__get_free_page*())或者類似kmalloc() or kmem_cache_alloc()這樣的通用內(nèi)存分配的接口來分配DMA buffer,那么這些接口函數(shù)返回的虛擬地址可以直接用于DMA mapping接口API,并通過DMA操作在外設(shè)和dma buffer中交換數(shù)據(jù)。
使用vmalloc() 分配的DMA buffer可以直接使用嗎?最好不要這樣,雖然強(qiáng)行使用也沒有問題,但是終究是比較麻煩。首先,vmalloc分配的page frame是不連續(xù)的,如果底層硬件需要物理內(nèi)存連續(xù),那么vmalloc分配的內(nèi)存不能滿足硬件要求。即便是底層DMA硬件支持scatter-gather,vmalloc分配出來的內(nèi)存仍然存在其他問題。我們知道vmalloc分配的虛擬地址和對(duì)應(yīng)的物理地址沒有線性關(guān)系(kmalloc或者_(dá)_get_free_page*這樣的接口,其返回的虛擬地址和物理地址有一個(gè)固定偏移的關(guān)系),而在做DMA mapping的時(shí)候,需要知道物理地址,有線性關(guān)系的虛擬地址很容易可以獲取其物理地址,但是對(duì)于vmalloc分配的虛擬地址,我們需要遍歷頁表才可以找到其物理地址。
在驅(qū)動(dòng)中定義的全局變量可以用于DMA嗎?如果編譯到內(nèi)核,那么全局變量位于內(nèi)核的數(shù)據(jù)段或者bss段。在內(nèi)核初始化的時(shí)候,會(huì)建立kernel image mapping,因此全局變量所占據(jù)的內(nèi)存都是連續(xù)的,并且VA和PA是有固定偏移的線性關(guān)系,因此可以用于DMA操作。不過,在定義這些全局變量的DMA buffer的時(shí)候,我們要小心的進(jìn)行cacheline的對(duì)齊,并且要處理CPU和DMA controller之間的操作同步,以避免cache coherence問題。
如果驅(qū)動(dòng)編譯成模塊會(huì)怎么樣呢?這時(shí)候,驅(qū)動(dòng)中的全局定義的DMA buffer不在內(nèi)核的線性映射區(qū)域,其虛擬地址是在模塊加載的時(shí)候,通過vmalloc分配,因此這時(shí)候如果DMA buffer如果大于一個(gè)page frame,那么實(shí)際上我們也是無法保證其底層物理地址的連續(xù)性,也無法保證VA和PA的線性關(guān)系,這一點(diǎn)和編譯到內(nèi)核是不同的。
通過kmap接口返回的內(nèi)存可以做DMA buffer嗎?也不行,其原理類似vmalloc,這里就不贅述了。
塊設(shè)備使用的I/O buffer和網(wǎng)絡(luò)設(shè)備收發(fā)數(shù)據(jù)的buffer是如何確保其內(nèi)存是可以進(jìn)行DMA操作的呢?塊設(shè)備I/O子系統(tǒng)和
網(wǎng)絡(luò)子系統(tǒng)在分配buffer的時(shí)候會(huì)確保這一點(diǎn)的。
四、DMA尋址限制
你的設(shè)備有DMA尋址限制嗎?不同的硬件平臺(tái)有不同的配置方式,有的平臺(tái)沒有限制,外設(shè)可以訪問系統(tǒng)內(nèi)存的每一個(gè)Byte,有些則不可以。例如:系統(tǒng)總線有32個(gè)bit,而你的設(shè)備通過DMA只能驅(qū)動(dòng)低24位地址,在這種情況下,外設(shè)在發(fā)起DMA操作的時(shí)候,只能訪問16M以下的系統(tǒng)內(nèi)存。如果設(shè)備有DMA尋址的限制,那么驅(qū)動(dòng)需要將這個(gè)限制通知到內(nèi)核。如果驅(qū)動(dòng)不通知內(nèi)核,那么內(nèi)核缺省情況下認(rèn)為外設(shè)的DMA可以訪問所有的系統(tǒng)總線的32 bit地址線。對(duì)于64 bit平臺(tái),情況類似,不再贅述。
是否有DMA尋址限制是和硬件設(shè)計(jì)相關(guān),有時(shí)候標(biāo)準(zhǔn)總線協(xié)議也會(huì)規(guī)定這一點(diǎn)。例如:PCI-X規(guī)范規(guī)定,所有的PCI-X設(shè)備必須要支持64 bit的尋址。
如果有尋址限制,那么在該外設(shè)驅(qū)動(dòng)的probe函數(shù)中,你需要詢問內(nèi)核,看看是否有DMA controller可以支持這個(gè)外設(shè)的尋址限制。雖然有缺省的尋址限制的設(shè)定,不過最好還是在probe函數(shù)中進(jìn)行相關(guān)處理,至少這說明你已經(jīng)為你的外設(shè)考慮過尋址限制這事了。
一旦確定了設(shè)備DMA尋址限制之后,我們可以通過下面的接口進(jìn)行設(shè)定:
根據(jù)DMA buffer的特性,DMA操作有兩種:一種是streaming,DMA buffer是一次性的,用完就算。這種DMA buffer需要自己考慮cache一致性。另外一種是DMA buffer是cache coherent的,軟件實(shí)現(xiàn)上比較簡(jiǎn)單,更重要的是這種DMA buffer往往是靜態(tài)的、長(zhǎng)時(shí)間存在的。不同類型的DMA操作可能有有不同的尋址限制,也可能相同。如果相同,我們可以用上面這個(gè)接口設(shè)定streaming和coherent兩種DMA 操作的地址掩碼。如果不同,可以下面的接口進(jìn)行設(shè)定:
前者是設(shè)定streaming類型的DMA地址掩碼,后者是設(shè)定coherent類型的DMA地址掩碼。為了更好的理解這些接口,我們聊聊參數(shù)和返回值。dev指向該設(shè)備的struct device對(duì)象,一般來說,這個(gè)struct device對(duì)象應(yīng)該是嵌入在bus-specific 的實(shí)例中,例如對(duì)于PCI設(shè)備,有一個(gè)struct pci_dev的實(shí)例與之對(duì)應(yīng),而在這里需要傳入的dev參數(shù)則可以通過&pdev->dev得到(pdev指向struct pci_dev的實(shí)例)。mask表示你的設(shè)備支持的地址線信息。如果調(diào)用這些接口返回0,則說明一切OK,從該設(shè)備到指定mask的內(nèi)存的DMA操作是可以被系統(tǒng)支持的(包括DMA controller、bus layer等)。如果返回值非0,那么說明這樣的DMA尋址是不能正確完成的,如果強(qiáng)行這么做將會(huì)產(chǎn)生不可預(yù)知的后果。驅(qū)動(dòng)必須檢測(cè)返回值,如果不行,那么建議修改mask或者不使用DMA。也就是說,對(duì)上面接口調(diào)用失敗后,你有三個(gè)選擇:
1、用另外的mask
2、不使用DMA模式,采用普通I/O模式
3、忽略這個(gè)設(shè)備的存在,不對(duì)其進(jìn)行初始化
一個(gè)可以尋址32 bit的設(shè)備,其初始化的示例代碼如下:
另一個(gè)常見的場(chǎng)景是有64位尋址能力的設(shè)備。一般來說我們會(huì)首先嘗試設(shè)定64位的地址掩碼,但是這時(shí)候有可能會(huì)失敗,從而將掩碼降低為32位。內(nèi)核之所以會(huì)在設(shè)定64位掩碼的時(shí)候失敗,這并不是因?yàn)槠脚_(tái)不能進(jìn)行64位尋址,而僅僅是因?yàn)?2位尋址比64位尋址效率更高。例如,SPARC64 平臺(tái)上,PCI SAC尋址比DAC尋址性能更好。
下面的代碼描述了如何確定streaming類型DMA的地址掩碼:
設(shè)定coherent 類型的DMA地址掩碼也是類似的,不再贅述。需要說明的是:coherent地址掩碼總是等于或者小于streaming地址掩碼,因此,一般來說,我們只要設(shè)定了streaming地址掩碼成功了,那么使用同樣的掩碼或者小一些的掩碼來設(shè)定coherent地址掩碼總是會(huì)成功,因此這時(shí)候我們一般就不檢查dma_set_coherent_mask的返回值了,當(dāng)然,有些設(shè)備很奇怪,只能使用coherent DMA,那么這種情況下,驅(qū)動(dòng)需要檢查dma_set_coherent_mask的返回值。
五、兩種類型的DMA mapping
1、一致性DMA映射(Consistent DMA mappings )
Consistent DMA mapping有下面兩種特點(diǎn):
(1)持續(xù)使用該DMA buffer(不是一次性的),因此Consistent DMA總是在初始化的時(shí)候進(jìn)行map,在shutdown的時(shí)候unmap。
(2)CPU和DMA controller在發(fā)起對(duì)DMA buffer的并行訪問的時(shí)候不需要考慮cache的影響,也就是說不需要軟件進(jìn)行cache操作,CPU和DMA controller都可以看到對(duì)方對(duì)DMA buffer的更新。實(shí)際上一致性DMA映射中的那個(gè)Consistent實(shí)際上可以稱為coherent,即cache coherent。
缺省情況下,coherent mask被設(shè)定為低32 bit(0xFFFFFFFF),即便缺省值是OK了,我們也建議你通過接口在驅(qū)動(dòng)中設(shè)定coherent mask。
一般使用Consistent DMA mapping的場(chǎng)景包括:
(1)網(wǎng)卡驅(qū)動(dòng)和網(wǎng)卡DMA控制器往往是通過一些內(nèi)存中的描述符(形成環(huán)或者鏈)進(jìn)行交互,這些保存描述符的memory一般采用Consistent DMA mapping。
(2)SCSI硬件適配器上的DMA可以主存中的一些數(shù)據(jù)結(jié)構(gòu)(mailbox command)進(jìn)行交互,這些保存mailbox command的memory一般采用Consistent DMA mapping。
(3)有些外設(shè)有能力執(zhí)行主存上的固件代碼(microcode),這些保存microcode的主存一般采用Consistent DMA mapping。
上面的這些例子有同樣的特性:CPU對(duì)memory的修改可以立刻被device感知到,反之亦然。一致性映射可以保證這一點(diǎn)。
需要注意的是:一致性的DMA映射并不意味著不需要memory barrier這樣的工具來保證memory order,CPU有可能為了性能而重排對(duì)consistent memory上內(nèi)存訪問指令。例如:如果在DMA consistent memory上有兩個(gè)word,分別是word0和word1,對(duì)于device一側(cè),必須保證word0先更新,然后才有對(duì)word1的更新,那么你需要這樣寫代碼:
只有這樣才能保證在所有的平臺(tái)上,給設(shè)備驅(qū)動(dòng)可以正常的工作。
此外,在有些平臺(tái)上,修改了DMA Consistent buffer后,你的驅(qū)動(dòng)可能需要flush write buffer,以便讓device側(cè)感知到memory的變化。這個(gè)動(dòng)作類似在PCI橋中的flush write buffer的動(dòng)作。
2、流式DMA映射(streaming DMA mapping)
流式DMA映射是一次性的,一般是需要進(jìn)行DMA傳輸?shù)臅r(shí)候才進(jìn)行mapping,一旦DMA傳輸完成,就立刻ummap(除非你使用dma_sync_*的接口,下面會(huì)描述)。并且硬件可以為順序化訪問進(jìn)行優(yōu)化。
這里的streaming可以被認(rèn)為是asynchronous,或者是不屬于coherent memory范圍的。
一般使用streaming DMA mapping的場(chǎng)景包括:
(1)網(wǎng)卡進(jìn)行數(shù)據(jù)傳輸使用的DMA buffer
(2)文件系統(tǒng)中的各種數(shù)據(jù)buffer,這些buffer中的數(shù)據(jù)最終到讀寫到SCSI設(shè)備上去,一般而言,驅(qū)動(dòng)會(huì)接受這些buffer,然后進(jìn)行streaming DMA mapping,之后和SCSI設(shè)備上的DMA進(jìn)行交互。
設(shè)計(jì)streaming DMA mapping這樣的接口是為了充分優(yōu)化硬件的性能,為了打到這個(gè)目標(biāo),在使用這些接口的時(shí)候,你必須清清楚楚的知道調(diào)用接口會(huì)發(fā)生什么。
無論哪種類型的DMA映射都有對(duì)齊的限制,這些限制來自底層的總線,當(dāng)然也有可能是某些總線上的設(shè)備有這樣的限制。此外,如果系統(tǒng)中的cache并不是DMA coherent的,而且底層的DMA buffer不合其他數(shù)據(jù)共享cacheline,這樣的系統(tǒng)將工作的更好。
六、如何使用coherent DMA mapping的接口?
1、分配并映射dma buffer
為了分配并映射一個(gè)較大(page大小或者類似)的coherent DMA memory,你需要調(diào)用下面的接口:
DMA操作總是會(huì)涉及具體設(shè)備上的DMA controller,而dev參數(shù)就是執(zhí)行該設(shè)備的struct device對(duì)象的。size參數(shù)指明了你想要分配的DMA Buffer的大小,byte為單位。dma_alloc_coherent這個(gè)接口也可以在中斷上下文調(diào)用,當(dāng)然,gfp參數(shù)要傳遞GFP_ATOMIC標(biāo)記,gfp是內(nèi)存分配的flag,dma_alloc_coherent僅僅是透?jìng)髟揻lag到內(nèi)存管理模塊。
需要注意的是dma_alloc_coherent分配的內(nèi)存的起始地址和size都是對(duì)齊在page上(類似__get_free_pages的感覺,當(dāng)然__get_free_pages接受的size參數(shù)是page order),如果你的驅(qū)動(dòng)不需要那么大的DMA buffer,那么可以選擇dma_pool接口,下面會(huì)進(jìn)一步描述。
如果傳入非空的dev參數(shù),即使驅(qū)動(dòng)調(diào)用了掩碼設(shè)置接口函數(shù)設(shè)定了DMA mask,說明該設(shè)備可以訪問大于32-bit地址空間的地址,一致性DMA映射的接口函數(shù)也一般會(huì)默認(rèn)的返回一個(gè)32-bit可尋址的DMA buffer地址。要知道dma mask和coherent dma mask是不同的,除非驅(qū)動(dòng)顯示的調(diào)用dma_set_coherent_mask()接口來修改coherent dma mask,例如大小大于32-bit地址,dma_alloc_coherent接口函數(shù)才會(huì)返回大于32-bit地址空間的地址。dma pool接口也是如此。
dma_alloc_coherent函數(shù)返回兩個(gè)值,一個(gè)是從CPU角度訪問DMA buffer的虛擬地址,另外一個(gè)是從設(shè)備(DMA controller)角度看到的bus address:dma_handle,驅(qū)動(dòng)可以將這個(gè)bus address傳遞給HW。
即便是請(qǐng)求的DMA buffer的大小小于PAGE SIZE,dma_alloc_coherent返回的cpu虛擬地址和DMA總線地址都保證對(duì)齊在最小的PAGE_SIZE上,這個(gè)特性確保了分配的DMA buffer有這樣的特性:如果page size是64K,即便是驅(qū)動(dòng)分配一個(gè)小于或者等于64K的dma buffer,那么DMA buffer不會(huì)越過64K的邊界。
2、umap并釋放dma buffer
當(dāng)驅(qū)動(dòng)需要umap并釋放dma buffer的時(shí)候,需要調(diào)用下面的接口:
這個(gè)接口函數(shù)的dev、size參數(shù)上面已經(jīng)描述過了,而cpu_addr和dma_handle這兩個(gè)參數(shù)就是dma_alloc_coherent() 接口的那兩個(gè)地址返回值。需要強(qiáng)調(diào)的一點(diǎn)就是:和dma_alloc_coherent不同,dma_free_coherent不能在中斷上下文中調(diào)用。(因?yàn)樵谟行┢脚_(tái)上,free DMA的操作會(huì)引發(fā)TLB維護(hù)的操作(從而引發(fā)cpu core之間的通信),如果關(guān)閉了IRQ會(huì)鎖死在SMP IPI 的代碼中)。
3、dma pool
如果你的驅(qū)動(dòng)需非常多的小的dma buffer,那么dma pool是最適合你的機(jī)制。這個(gè)概念類似kmem_cache,__get_free_pages往往獲取的是連續(xù)的page frame,而kmem_cache是批發(fā)了一大批page frame,然后自己“零售”。dma pool就是通過dma_alloc_coherent接口獲取大塊一致性的DMA內(nèi)存,然后驅(qū)動(dòng)可以調(diào)用dma_pool_alloc從那個(gè)大塊DMA內(nèi)存中分一個(gè)小塊的dma buffer供自己使用。具體接口描述就不說了,大家可以自行閱讀。
七、DMA操作方向
由于下面的章節(jié)會(huì)用到DMA操作方向這個(gè)概念,因此我們先簡(jiǎn)單的描述一下,DMA操作方向定義如下:
如果你知道的話,你應(yīng)該盡可能的提供準(zhǔn)確的DMA操作方向。
DMA_TO_DEVICE表示“從內(nèi)存(dma buffer)到設(shè)備”,而 DMA_FROM_DEVICE表示“從設(shè)備到內(nèi)存(dma buffer)”,上面的這些字符定義了數(shù)據(jù)在DMA操作中的移動(dòng)方向。
雖然我們強(qiáng)烈要求驅(qū)動(dòng)在知道DMA傳輸方向的適合,精確的指明是DMA_TO_DEVICE或者DMA_FROM_DEVICE,然而,如果你確實(shí)是不知道具體的操作方向,那么設(shè)定為DMA_BIDIRECTIONAL也是可以的,表示DMA操作可以執(zhí)行任何一個(gè)方向的的數(shù)據(jù)搬移。你的平臺(tái)需要保證這一點(diǎn)可以讓DMA正常工作,當(dāng)然,這也有可能會(huì)引入一些性能上的額外開銷。
DMA_NONE主要是用于調(diào)試。在驅(qū)動(dòng)知道精確的DMA方向之前,可以把它保存在DMA控制數(shù)據(jù)結(jié)構(gòu)中,在dma方向設(shè)定有問題的適合,你可以跟蹤dma方向的設(shè)置情況,以便定位問題所在。
除了潛在的平臺(tái)相關(guān)的性能優(yōu)化之外,精確地指定DMA操作方向還有另外一個(gè)優(yōu)點(diǎn)就是方便調(diào)試。有些平臺(tái)實(shí)際上在創(chuàng)建DMA mapping的時(shí)候,頁表(指將bus地址映射到物理地址的頁表)中有一個(gè)寫權(quán)限布爾值,這個(gè)值非常類似于用戶程序地址空間中的頁保護(hù)。當(dāng)DMA控制器硬件檢測(cè)到違反權(quán)限設(shè)置時(shí)(這時(shí)候dma buffer設(shè)定的是MA_TO_DEVICE類型,實(shí)際上DMA controller只能是讀dma buffer),這樣的平臺(tái)可以將錯(cuò)誤寫入內(nèi)核日志,從而方便了debug。
只有streaming mappings才會(huì)指明DMA操作方向,一致性DMA映射隱含的DMA操作方向是DMA_BIDIRECTIONAL。我們舉一個(gè)streaming mappings的例子:在網(wǎng)卡驅(qū)動(dòng)中,如果要發(fā)送數(shù)據(jù),那么在map/umap的時(shí)候需要指明DMA_TO_DEVICE的操作方向,而在接受數(shù)據(jù)包的時(shí)候,map/umap需要指明DMA操作方向是DMA_FROM_DEVICE。
八、如何使用streaming DMA mapping的接口?
streaming DMA mapping的接口函數(shù)可以在中斷上下文中調(diào)用。streaming DMA mapping有兩個(gè)版本的接口函數(shù),一個(gè)是用來map/umap單個(gè)的dma buffer,另外一個(gè)是用來map/umap形成scatterlist的多個(gè)dma buffer。
1、map/umap單個(gè)的dma buffer
map單個(gè)的dma buffer的示例如下:
umap單個(gè)的dma buffer可以使用下面的接口:
當(dāng)調(diào)用dma_map_single()返回錯(cuò)誤的時(shí)候,你應(yīng)當(dāng)調(diào)用dma_mapping_error()來處理錯(cuò)誤。雖然并不是所有的DMA mapping實(shí)現(xiàn)都支持dma_mapping_error這個(gè)接口(調(diào)用dma_mapping_error函數(shù)實(shí)際上會(huì)調(diào)用底層dma_map_ops操作函數(shù)集中的mapping_error成員函數(shù)),但是調(diào)用它來進(jìn)行出錯(cuò)處理仍然是一個(gè)好的做法。這樣做的好處是可以確保DMA mapping代碼在所有DMA實(shí)現(xiàn)中都能正常工作,而不需要依賴底層實(shí)現(xiàn)的細(xì)節(jié)。沒有檢查錯(cuò)誤就使用返回的地址可能會(huì)導(dǎo)致程序失敗,可能會(huì)產(chǎn)生kernel panic或者悄悄的損壞你有用的數(shù)據(jù)。下面列舉了一些不正確的方法來檢查DMA mapping錯(cuò)誤,之所以是錯(cuò)誤的方法是因?yàn)檫@些代碼對(duì)底層的DMA實(shí)現(xiàn)進(jìn)行了假設(shè)。順便說的是雖然這里是使用dma_map_single作為示例,實(shí)際上也是適用于dma_map_page()的。
錯(cuò)誤示例一:
錯(cuò)誤示例二:
當(dāng)DMA傳輸完成的時(shí)候,程序應(yīng)該調(diào)用dma_unmap_single()函數(shù)umap dma buffer。例如:在DMA完成傳輸后會(huì)通過中斷通知CPU,而在interrupt handler中可以調(diào)用dma_unmap_single()函數(shù)。dma_map_single函數(shù)在進(jìn)行DMA mapping的時(shí)候使用的是CPU指針(虛擬地址),這樣就導(dǎo)致該函數(shù)有一個(gè)弊端:不能使用HIGHMEM memory進(jìn)行mapping。鑒于此,map/unmap接口提供了另外一個(gè)類似的接口,這個(gè)接口不使用CPU指針,而是使用page和page offset來進(jìn)行DMA mapping:
在上面的代碼中,offset表示一個(gè)指定page內(nèi)的頁內(nèi)偏移(以Byte為單位)。和dma_map_single接口函數(shù)一樣,調(diào)用dma_map_page()返回錯(cuò)誤后需要調(diào)用dma_mapping_error() 來進(jìn)行錯(cuò)誤處理,上面都已經(jīng)描述了,這里不再贅述。當(dāng)DMA傳輸完成的時(shí)候,程序應(yīng)該調(diào)用dma_unmap_page()函數(shù)umap dma buffer。例如:在DMA完成傳輸后會(huì)通過中斷通知CPU,而在interrupt handler中可以調(diào)用dma_unmap_page()函數(shù)。
2、map/umap多個(gè)形成scatterlist的dma buffer
在scatterlist的情況下,你要映射的對(duì)象是分散的若干段DMA buffer,示例代碼如下:
上面的代碼中nents說明了sglist中條目的數(shù)量(即map多少段dma buffer)。
具體DMA映射的實(shí)現(xiàn)是自由的,它可以把scatterlist 中的若干段連續(xù)的DMA buffer映射成一個(gè)大塊的,連續(xù)的bus address region。例如:如果DMA mapping是以PAGE_SIZE為粒度進(jìn)行映射,那么那些分散的一塊塊的dma buffer可以被映射到一個(gè)對(duì)齊在PAGE_SIZE,然后各個(gè)dma buffer依次首尾相接的一個(gè)大的總線地址區(qū)域上。這樣做的好處就是對(duì)于那些不支持(或者支持有限)scatter-gather 的DMA controller,仍然可以通過mapping來實(shí)現(xiàn)。dma_map_sg調(diào)用識(shí)別的時(shí)候返回0,當(dāng)調(diào)用成功的時(shí)候,返回成功mapping的數(shù)目。
一旦調(diào)用成功,你需要調(diào)用for_each_sg來遍歷所有成功映射的mappings(這個(gè)數(shù)目可能會(huì)小于nents)并且使用sg_dma_address() 和 sg_dma_len() 這兩個(gè)宏來得到mapping后的dma地址和長(zhǎng)度。
umap多個(gè)形成scatterlist的dma buffer是通過下面的接口實(shí)現(xiàn)的:
再次強(qiáng)調(diào),調(diào)用dma_unmap_sg的時(shí)候要確保DMA操作已經(jīng)完成。另外,傳遞給dma_unmap_sg的nents參數(shù)需要等于傳遞給dma_map_sg的nents參數(shù),而不是該函數(shù)返回的count。
由于DMA地址空間是共享資源,每一次dma_map_{single,sg}() 的調(diào)用都需要有其對(duì)應(yīng)的dma_unmap_{single,sg}(),如果你總是分配dma地址資源而不回收,那么系統(tǒng)將會(huì)由于DMA address被用盡而陷入不可用的狀態(tài)。
3、sync操作
如果你需要多次訪問同一個(gè)streaming DMA buffer,并且在DMA傳輸之間讀寫DMA Buffer上的數(shù)據(jù),這時(shí)候你需要小心進(jìn)行DMA buffer的sync操作,以便CPU和設(shè)備(DMA controller)可以看到最新的、正確的數(shù)據(jù)。
首先用dma_map_{single,sg}()進(jìn)行映射,在完成DMA傳輸之后,用:
或者:
來完成sync的操作,以便CPU可以看到最新的數(shù)據(jù)。
如果,CPU操作了DMA buffer的數(shù)據(jù),然后你又想把控制權(quán)交給設(shè)備上的DMA 控制器,讓DMA controller訪問DMA buffer,這時(shí)候,在真正讓HW(指DMA控制器)去訪問DMA buffer之前,你需要調(diào)用:
或者:
以便device(也就是設(shè)備上的DMA控制器)可以看到cpu更新后的數(shù)據(jù)。此外,需要強(qiáng)調(diào)的是:傳遞給dma_sync_sg_for_cpu() 和 dma_sync_sg_for_device()的ents參數(shù)需要等于傳遞給dma_map_sg的nents參數(shù),而不是該函數(shù)返回的count。
在完成最后依次DMA傳輸之后,你需要調(diào)用DMA unmap函數(shù)dma_unmap_{single,sg}()。如果在第一次dma_map_*() 調(diào)用和dma_unmap_*()之間,你從來都沒有碰過DMA buffer中的數(shù)據(jù),那么你根本不需要調(diào)用dma_sync_*() 這樣的sync操作。
下面的例子給出了一個(gè)sync操作的示例:
當(dāng)使用了這套DMA mapping接口后,驅(qū)動(dòng)不應(yīng)該再使用virt_to_bus() 這個(gè)接口了,當(dāng)然bus_to_virt()也不行。不過,如果你的驅(qū)動(dòng)使用了這些接口怎么辦呢?其實(shí)這套新的DMA mapping接口沒有和virt_to_bus、bus_to_virt()一一對(duì)應(yīng)的接口,因此,為了讓你的程序能工作,你需要對(duì)驅(qū)動(dòng)程序進(jìn)行小小的修改:你必須要保存從dma_alloc_coherent()、dma_pool_alloc()以及dma_map_single()接口函數(shù)返回的dma address(對(duì)于dma_map_sg()這個(gè)接口,dma地址保存在scatterlist 中,當(dāng)然這需要硬件支持dynamic DMA mapping ),并把這個(gè)dma address保存在驅(qū)動(dòng)的數(shù)據(jù)結(jié)構(gòu)中,并且同時(shí)/或者保存在硬件的寄存器中。
所有的驅(qū)動(dòng)代碼都需要遷移到DMA mapping framework的接口函數(shù)上來。目前內(nèi)核已經(jīng)計(jì)劃完全移除virt_to_bus() 和bus_to_virt() 這兩個(gè)函數(shù),因?yàn)樗鼈円呀?jīng)過時(shí)了。有些平臺(tái)由于不能正確的支持virt_to_bus() 和bus_to_virt(),因此根本就沒有提供這兩個(gè)接口。
九、錯(cuò)誤處理
DMA地址空間在某些CPU架構(gòu)上是有限的,因此分配并mapping可能會(huì)產(chǎn)生錯(cuò)誤,我們可以通過下面的方法來判定是否發(fā)生了錯(cuò)誤:
(1)檢查是否dma_alloc_coherent() 返回了NULL或者dma_map_sg 返回0
(2)檢查dma_map_single和dma_map_page返回了dma address(通過dma_mapping_error函數(shù))
(3)當(dāng)在mapping多個(gè)page的時(shí)候,如果中間發(fā)生了mapping error,那么需要對(duì)那些已經(jīng)mapped的page進(jìn)行unmap的操作。下面的示例代碼用dma_map_single函數(shù),對(duì)于dma_map_page也一樣適用。
示例代碼一:
示例代碼二(如果我們?cè)谘h(huán)中mapping dma buffer,當(dāng)在中間出錯(cuò)的時(shí)候,一樣要unmap所有已經(jīng)映射的dma buffer):
如果在網(wǎng)卡驅(qū)動(dòng)的tx回調(diào)函數(shù)(例如ndo_start_xmit)中出現(xiàn)了DMA mapping失敗,那么驅(qū)動(dòng)必須調(diào)用dev_kfree_skb() 來是否socket buffer并返回NETDEV_TX_OK 。這表示這個(gè)socket buffer由于錯(cuò)誤而丟棄掉了。
如果在SCSI driver的queue command回調(diào)函數(shù)中出現(xiàn)了DMA mapping失敗,那么驅(qū)動(dòng)必須返回SCSI_MLQUEUE_HOST_BUSY 。這意味著SCSI子系統(tǒng)稍后會(huì)再次重傳該command給driver。
十、優(yōu)化數(shù)據(jù)結(jié)構(gòu)
在很多的平臺(tái)上,dma_unmap_{single,page}()其實(shí)什么也沒有做,是空函數(shù)。因此,跟蹤映射的dma address及其長(zhǎng)度基本上就是浪費(fèi)內(nèi)存空間。為了方便驅(qū)動(dòng)工程師編寫代碼方便,我們提供了幾個(gè)實(shí)用工具(宏定義),如果沒有它們,驅(qū)動(dòng)程序中將充分ifdef或者類似的一些“work around”。下面我們并不是一個(gè)個(gè)的介紹這些宏定義,而是給出一些示例代碼,驅(qū)動(dòng)工程師可以照葫蘆畫瓢。
1、DEFINE_DMA_UNMAP_{ADDR,LEN}。在DMA buffer數(shù)據(jù)結(jié)構(gòu)中使用這個(gè)宏定義,具體例子如下:
根據(jù)CONFIG_NEED_DMA_MAP_STATE的配置不同,DEFINE_DMA_UNMAP_{ADDR,LEN}可能是定義相關(guān)的dma address和長(zhǎng)度的成員,也可能是空。
2、dma_unmap_{addr,len}_set()。使用該宏定義來賦值,具體例子如下:
3、dma_unmap_{addr,len}(),使用該宏來訪問變量。
上面的這些代碼基本是不需要解釋你就會(huì)明白的了。另外,我們對(duì)于dma address和len是分開處理的,因?yàn)樵谟行?shí)現(xiàn)中,unmaping的操作僅僅需要dma address信息就夠了。
十一、平臺(tái)移植需要注意的問題
如果你僅僅是驅(qū)動(dòng)工程師,并不負(fù)責(zé)將linux遷移到某個(gè)cpu arch上去,那么后面的內(nèi)容其實(shí)你可以忽略掉了。
1、Struct scatterlist的需求
如果cpu arch支持IOMMU(包括軟件模擬的IOMMU),那么你需要打開CONFIG_NEED_SG_DMA_LENGTH 這個(gè)內(nèi)核選項(xiàng)。
2、ARCH_DMA_MINALIGN
CPU體系結(jié)構(gòu)相關(guān)的代碼必須要要保證kmalloc分配的buffer是DMA-safe的(kmalloc分配的buffer也是有可能用于DMA buffer),驅(qū)動(dòng)和內(nèi)核子系統(tǒng)的正確運(yùn)行都是依賴這個(gè)條件的。如果一個(gè)cpu arch不是全面支持DMA-coherent的(例如硬件并不保證cpu cache中的數(shù)據(jù)等于main memory中的數(shù)據(jù)),那么必須定義ARCH_DMA_MINALIGN。而通過這個(gè)宏定義,kmalloc分配的buffer可以保證對(duì)齊在ARCH_DMA_MINALIGN上,從而保證了kmalloc分配的DMA Buffer不會(huì)和其他的buffer共享一個(gè)cacheline。想要了解具體的實(shí)例可以參考arch/arm/include/asm/cache.h。
另外,請(qǐng)注意:ARCH_DMA_MINALIGN 是DMA buffer的對(duì)齊約束,你不需要擔(dān)心CPU ARCH的數(shù)據(jù)對(duì)齊約束(例如,有些CPU arch要求有些數(shù)據(jù)對(duì)象需要64-bit對(duì)齊)。
十二、后記
如果沒有來自廣大人民群眾的反饋和建議,這份文檔(包括DMA API本身)可能會(huì)顯得過時(shí),陳舊。
此外,對(duì)這份文檔有幫助的人如下(沒有按照什么特別的順序):
Russell King
Leo Dagum
Ralf Baechle
Grant Grundler
Jay Estabrook
Thomas Sailer
Andrea Arcangeli
Jens Axboe
David Mosberger-Tang davidm@hpl.hp.com
備注:本文基本上是內(nèi)核文檔DMA-API-HOWTO.txt的翻譯,如果有興趣可以參考原文。
原創(chuàng)翻譯文章,轉(zhuǎn)發(fā)請(qǐng)注明出處。蝸窩科技
http://www.wowotech.net/memory_management/DMA-Mapping-api.html