直接內(nèi)存存取, 或者 DMA, 是結(jié)束我們的內(nèi)存問題概覽的高級主題. DMA 是硬件機制允許外設組件來直接傳輸它們的 I/O 數(shù)據(jù)到和從主內(nèi)存, 而不需要包含系統(tǒng)處理器. 這種機制的使用能夠很大提高吞吐量到和從一個設備, 因為大量的計算開銷被削減了.
在介紹程序細節(jié)之前, 讓我們回顧一個 DMA 傳輸如何發(fā)生的, 只考慮輸入傳輸來簡化討論.
數(shù)據(jù)傳輸可由 2 種方法觸發(fā):或者軟件請求數(shù)據(jù)(通過一個函數(shù)例如 read)或者硬件異步推數(shù)據(jù)到系統(tǒng).
在第一種情況, 包含的步驟總結(jié)如下:
第 2 種情況到來是當 DMA 被異步使用. 例如, 這發(fā)生在數(shù)據(jù)獲取設備, 它在沒有人讀它們的時候也持續(xù)推入數(shù)據(jù). 在這個情況下, 驅(qū)動應當維護一個緩沖以至于后續(xù)的讀調(diào)用能返回所有的累積的數(shù)據(jù)給用戶空間. 這類傳輸包含的步驟有點不同:
異步方法的變體常常在網(wǎng)卡中見到. 這些卡常常期望見到一個在內(nèi)存中和處理器共享的環(huán)形緩沖(常常被稱為一個 DMA 的緩沖); 每個到來的報文被放置在環(huán)中下一個可用的緩沖, 并且發(fā)出一個中斷. 驅(qū)動接著傳遞網(wǎng)絡本文到內(nèi)核其他部分并且在環(huán)中放置一個新 DMA 緩沖.
在所有這些情況中的處理的步驟都強調(diào), 有效的 DMA 處理依賴中斷報告. 雖然可能實現(xiàn) DMA 使用一個輪詢驅(qū)動, 它不可能有意義, 因為一個輪詢驅(qū)動可能浪費 DMA 提供的性能益處超過更容易的處理器驅(qū)動的I/O.[49]
在這里介紹的另一個相關(guān)項是 DMA 緩沖. DMA 要求設備驅(qū)動來分配一個或多個特殊的適合 DMA 的緩沖. 注意許多驅(qū)動分配它們的緩沖在初始化時并且使用它們直到關(guān)閉 -- 在之前列表中的分配一詞, 意思是"獲得一個之前分配的緩沖".
本節(jié)涵蓋 DMA 緩沖在底層的分配; 我們稍后介紹一個高級接口, 但是來理解這里展示的內(nèi)容仍是一個好主意.
隨 DMA 緩沖帶來的主要問題是, 當它們大于一頁, 它們必須占據(jù)物理內(nèi)存的連續(xù)頁因為設備使用 ISA 或者 PCI 系統(tǒng)總線傳輸數(shù)據(jù), 它們都使用物理地址. 注意有趣的是這個限制不適用 SBus ( 見 12 章的"SBus"一節(jié) ), 它在外設總線上使用虛擬地址. 一些體系結(jié)構(gòu)還可以在 PCI 總線上使用虛擬地址, 但是一個可移植的驅(qū)動不能依賴這個功能.
盡管 DMA 緩沖可被分配或者在系統(tǒng)啟動時或者在運行時, 模塊只可在運行時分配它們的緩沖. (第 8 章介紹這些技術(shù); "獲取大緩沖"一節(jié)涵蓋在系統(tǒng)啟動時分配, 而"kmalloc 的真實"和"get_free_page 和其友"描述在運行時分配). 驅(qū)動編寫者必須關(guān)心分配正確的內(nèi)存,當它被用做 DMA 操作時; 不是所有內(nèi)存區(qū)是合適的. 特別的, 在一些系統(tǒng)中的一些設備上高端內(nèi)存可能不為 DMA 工作 - 外設完全無法使用高端地址.
在現(xiàn)代總線上的大部分設備可以處理 32-位 地址, 意思是正常的內(nèi)存分配對它們是剛剛好的. 一些 PCI 設備, 但是, 不能實現(xiàn)完整的 PCI 標準并且不能使用 32-位 地址. 并且 ISA 設備, 當然, 限制只在 24-位 地址.
對于有這種限制的設備, 內(nèi)存應當從 DMA 區(qū)進行分配, 通過添加 GFP_DMA 標志到 kmalloc 或者 get_free_pages 調(diào)用. 當這個標志存在, 只有可用 24-位 尋址的內(nèi)存被分配. 另一種選擇, 你可以使用通用的 DMA 層( 我們馬上討論這個 )來分配緩沖以解決你的設備的限制.
我們已見到 get_free_pages 如何分配直到幾個 MByte (由于 order 可以直到 MAX_ORDER, 當前是 11), 但是高級數(shù)的請求容易失敗當請求的緩沖遠遠小于 128 KB, 因為系統(tǒng)內(nèi)存時間長了變得碎裂.[50]
當內(nèi)核無法返回請求數(shù)量的內(nèi)存或者當你需要多于 128 KB(例如, 一個通常的 PCI 幀抓取的請求), 一個替代返回 -ENOMEM 的做法是在啟動時分配內(nèi)存或者保留物理 RAM 的頂部給你的緩沖. 我們在第 8 章的 "獲得大量緩沖" 一節(jié)描述在啟動時間分配, 但是它對模塊是不可用的. 保留 RAM 的頂部是通過在啟動時傳遞一個 mem= 參數(shù)給內(nèi)核實現(xiàn)的. 例如, 如果你有 256 MB, 參數(shù) mem=255M 使內(nèi)核不使用頂部的 MByte. 你的模塊可能后來使用下列代碼來獲得對這個內(nèi)存的存取:
dmabuf = ioremap (0xFF00000 /* 255M */, 0x100000 /* 1M */);
分配器, 配合本書的例子代碼的一部分, 提供了一個簡單的 API 來探測和管理這樣的保留 RAM 并且已在幾個體系上被成功使用. 但是, 這個技巧當你有一個高內(nèi)存系統(tǒng)時無效(即, 一個有比適合 CPU 地址空間更多的物理內(nèi)存的系統(tǒng) ).
當然, 另一個選項, 是使用 GFP_NOFAIL 來分配你的緩沖. 這個方法, 但是, 確實嚴重地對內(nèi)存管理子系統(tǒng)有壓力, 并且它冒鎖住系統(tǒng)的風險; 最好是避免除非確實沒有其他方法.
如果你分配一個大 DMA 緩沖到這樣的長度, 但是, 值得想一下替代的方法. 如果你的設備可以做發(fā)散/匯聚 I/O, 你可以分配你的緩沖以更小的片段并且讓設備做其他的. 發(fā)散/匯聚 I/O 也可以用當進行直接 I/O 到用戶空間時, 它可能是最好地解決方法當需要一個真正大緩沖時.
一個使用 DMA 的設備驅(qū)動必須和連接到接口總線的硬件通訊, 總線使用物理地址, 而程序代碼使用虛擬地址.
事實上, 情況比這個稍微有些復雜. 基于DMA 的硬件使用總線地址, 而不是物理地址. 盡管 ISA 和 PCI 總線地址在 PC 上完全是物理地址, 這對每個平臺卻不總是真的. 有時接口總線被通過橋接電路連接, 它映射 I/O 地址到不同的物理地址. 一些系統(tǒng)甚至有一個頁映射機制, 使任意的頁連續(xù)出現(xiàn)在外設總線.
在最低級別(再次, 我們將馬上查看一個高級解決方法), Linux 內(nèi)核提供一個可移植的方法, 通過輸出下列函數(shù), 在 <asm/io.h> 定義. 這些函數(shù)的使用不被推薦, 因為它們只在有非常簡單的 I/O 體系的系統(tǒng)上正常工作; 但是, 你可能遇到它們當使用內(nèi)核代碼時.
unsigned long virt_to_bus(volatile void *address);
void *bus_to_virt(unsigned long address);
這些函數(shù)進行一個簡單的轉(zhuǎn)換在內(nèi)核邏輯地址和總線地址之間. 它們在許多情況下不工作, 一個 I/O 內(nèi)存管理單元必須被編程的地方或者必須使用反彈緩沖的地方. 做這個轉(zhuǎn)換的正確方法是使用通用的 DMA 層, 因此我們現(xiàn)在轉(zhuǎn)移到這個主題.
DMA 操作, 最后, 下到分配一個緩沖并且傳遞總線地址到你的設備. 但是, 編寫在所有體系上安全并正確進行 DMA 的可移植啟動的任務比想象的要難. 不同的系統(tǒng)有不同的概念, 關(guān)于緩存一致性應當如何工作的概念; 如果你不正確處理這個問題, 你的驅(qū)動可能破壞內(nèi)存. 一些系統(tǒng)有復雜的總線硬件, 它使 DMA 任務更容易 - 或者更難. 并且不是所有的系統(tǒng)可以在內(nèi)存所有部分進行 DMA. 幸運的是, 內(nèi)核提供了一個總線和體系獨立的 DMA 層來對驅(qū)動作者隱藏大部分這些問題. 我們非常鼓勵你來使用這個層來 DMA 操作, 在任何你編寫的驅(qū)動中.
下面的許多函數(shù)需要一個指向 struct device 的指針. 這個結(jié)構(gòu)是 Linux 設備模型中設備的低級表示. 它不是驅(qū)動常常必須直接使用的東西, 但是你確實需要它當使用通用 DMA 層時. 常常地, 你可發(fā)現(xiàn)這個結(jié)構(gòu), 深埋在描述你的設備的總線. 例如, 它可在 struct pci_device 或者 struct usb_device 中發(fā)現(xiàn)它作為 dev 成員. 設備結(jié)構(gòu)在 14 章中詳細描述.
使用下面函數(shù)的驅(qū)動應當包含 <linux/dma-mapping.h>.
在嘗試 DMA 之前必須回答的第一個問題是給定設備是否能夠在當前主機上做這樣的操作. 許多設備受限于它們能夠?qū)ぶ返膬?nèi)存范圍, 因為許多理由. 缺省地, 內(nèi)核假定你的設備能夠?qū)θ魏?32-位 地址進行 DMA. 如果不是這樣, 你應當通知內(nèi)核這個事實, 使用一個調(diào)用:
int dma_set_mask(struct device *dev, u64 mask);
mask 應當顯示你的設備能夠?qū)ぶ返奈? 如果它被限制到 24 位, 例如, 你要傳遞 mask 作為 0x0FFFFFF. 返回值是非零如果使用給定的 mask 可以 DMA; 如果 dma_set_mask 返回 0, 你不能對這個設備使用 DMA 操作. 因此, 設備的驅(qū)動中的初始化代碼限制到 24-位 DMA 操作可能看來如:
if (dma_set_mask (dev, 0xffffff))
card->use_dma = 1;
else
{
card->use_dma = 0; /* We'll have to live without DMA */
printk (KERN_WARN, "mydev: DMA not supported\n");
}
再次, 如果你的設備支持正常的, 32-位 DMA 操作, 沒有必要調(diào)用 dma_set_mask.
一個 DMA 映射是分配一個 DMA 緩沖和產(chǎn)生一個設備可以存取的地址的結(jié)合. 它試圖使用一個簡單的對 virt_to_bus 的調(diào)用來獲得這個地址, 但是有充分的理由來避免那個方法. 它們中的第一個是合理的硬件帶有一個 IOMMU 來為總線提供一套映射寄存器. IOMMU 可為任何物理內(nèi)存安排來出現(xiàn)在設備可存取的地址范圍內(nèi), 并且它可使物理上散布的緩沖對設備看來是連續(xù)的. 使用 IOMMU 需要使用通用的 DMA 層; virt_to_bus 不負責這個任務.
注意不是所有的體系都有一個 IOMMU; 特別的, 流行的 x86 平臺沒有 IOMMU 支持. 一個正確編寫的驅(qū)動不需要知道它在之上運行的 I/O 支持硬件, 但是.
為設備設置一個有用的地址可能也, 在某些情況下, 要求一個反彈緩沖的建立. 反彈緩沖是當一個驅(qū)動試圖在一個外設不能達到的地址上進行 DMA 時創(chuàng)建的, 比如一個高內(nèi)存地址. 數(shù)據(jù)接著根據(jù)需要被拷貝到和從反彈緩沖. 無需說, 反彈緩沖的使用能拖慢事情, 但是有時沒有其他選擇.
DMA 映射也必須解決緩存一致性問題. 記住現(xiàn)代處理器保持最近存取的內(nèi)存區(qū)的拷貝在一個快速的本地緩沖中; 如果沒有這個緩存, 合理的性能是不可能的. 如果你的設備改變主存一個區(qū), 會強制使任何包含那個區(qū)的處理器緩存被失效; 負責處理器可能使用不正確的主存映象, 并且導致數(shù)據(jù)破壞. 類似地, 當你的設備使用 DMA 來從主存中讀取數(shù)據(jù), 任何對那個駐留在處理器緩存的內(nèi)存的改變必須首先被刷新. 這些緩存一致性問題可以產(chǎn)生無頭的模糊和難尋的錯誤, 如果編程者不小心. 一個體系在硬件中管理緩存一致性, 但是其他的要求軟件支持. 通用的 DMA 層深入很多來保證在所有體系上事情都正確工作, 但是, 如同我們將見到的, 正確的行為要求符合一些規(guī)則.
DMA 映射設置一個新類型, dma_addr_t, 來代表總線地址. 類型 dma_addr_t 的變量應當被驅(qū)動當作不透明的; 唯一可允許的操作是傳遞它們到 DMA 支持過程和設備自身. 作為一個總線地址, dma_addr_t 可導致不期望的問題如果被 CPU 直接使用.
PCI 代碼在 2 類 DMA 映射中明顯不同, 依賴 DMA 緩沖被期望停留多長時間:
Coherent DMA mappings
連貫的 DMA 映射. 這些映射常常在驅(qū)動的生命期內(nèi)存在. 一個連貫的緩沖必須是同時對 CPU 和外設可用(其他的映射類型, 如同我們之后將看到的, 在任何給定時間只對一個或另一個可用). 結(jié)果, 一致的映射必須在緩沖一致的內(nèi)存. 一致的映射建立和使用可能是昂貴的.
Streaming DMA mappings
流 DMA 映射. 流映射常常為一個單個操作建立. 一些體系當使用流映射時允許大的優(yōu)化, 如我們所見, 但是這些映射也服從一個更嚴格的關(guān)于如何存取它們的規(guī)則. 內(nèi)核開發(fā)者建議使用一致映射而不是流映射在任何可能的時候. 這個建議有 2 個原因. 第一個, 在支持映射寄存器的系統(tǒng)上, 每個 DMA 映射在總線上使用它們一個或多個. 一致映射, 有長的生命周期, 可以長時間獨占這些寄存器, 甚至當它們不在使用時. 另外一個原因是, 在某些硬件上, 流映射可以用無法在一致映射中使用的方法來優(yōu)化.
這 2 種映射類型必須以不同的方式操作; 是時候看看細節(jié)了.
一個驅(qū)動可以建立一個一致映射, 使用對 dma_alloc_coherent 的調(diào)用:
void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, int flag);
這個函數(shù)處理緩沖的分配和映射. 前 2 個參數(shù)是設備結(jié)果和需要的緩沖大小. 這個函數(shù)返回 DMA 映射的結(jié)果在 2 個地方. 來自這個函數(shù)的返回值是緩沖的一個內(nèi)核虛擬地址, 它可被驅(qū)動使用; 其間相關(guān)的總線地址在 dma_handle 中返回. 分配在這個函數(shù)中被處理以至緩沖被放置在一個可以使用 DMA 的位置; 常常地內(nèi)存只是使用 get_freepages 來分配(但是注意大小是以字節(jié)計的, 而不是一個 order 值). flag 參數(shù)是通常的 GFP 值來描述內(nèi)存如何被分配; 常常應當是 GFP_KERNEL (常常) 或者 GFP_ATOMIC (當在原子上下文中運行時).
當不再需要緩沖(常常在模塊卸載時), 它應當被返回給系統(tǒng), 使用 dma_free_coherent:
void dma_free_coherent(struct device *dev, size_t size,
void *vaddr, dma_addr_t dma_handle);
注意, 這個函數(shù)象許多通常的 DMA 函數(shù), 需要提供所有的大小, CPU 地址, 和 總線地址參數(shù).
一個 DMA池 是分配小的, 一致DMA映射的分配機制. 從 dma_alloc_coherent 獲得的映射可能有一頁的最小大小. 如果你的驅(qū)動需要比那個更小的 DMA 區(qū)域, 你應當可能使用一個 DMA 池. DMA 池也在這種情況下有用, 當你可能試圖對嵌在一個大結(jié)構(gòu)中的小區(qū)域進行 DMA 操作. 一些非常模糊的驅(qū)動錯誤已被追蹤到緩存一致性問題, 在靠近小 DMA 區(qū)域的結(jié)構(gòu)成員. 為避免這個問題, 你應當一直明確分配進行 DMA 操作的區(qū)域, 和其他的非 DMA 數(shù)據(jù)結(jié)構(gòu)分開.
DMA 池函數(shù)定義在 <linux/dmapool.h>.
一個 DMA 池必須在使用前創(chuàng)建, 使用一個調(diào)用:
struct dma_pool *dma_pool_create(const char *name, struct device *dev,
size_t size, size_t align,
size_t allocation);
這里, name 是池的名子, dev 是你的設備結(jié)構(gòu), size 是要從這個池分配的緩沖區(qū)大小, align 是來自池的分配要求的硬件對齊(以字節(jié)表達的), 以及 allocation是, 如果非零, 一個分配不應當越過的內(nèi)存邊界. 如果 allocation 以 4096 傳遞, 例如, 從池分配的緩沖不越過 4-KB 邊界.
當你用完一個池, 可被釋放, 用:
void dma_pool_destroy(struct dma_pool *pool);
你應當返回所有的分配給池, 在銷毀它之前. 分配被用 dma_pool_alloc 處理:
void *dma_pool_alloc(struct dma_pool *pool, int mem_flags, dma_addr_t *handle);
對這個調(diào)用, memflags 是常用的 GFP 分配標志的設置. 如果所有都進行順利, 一個內(nèi)存區(qū)(大小是當池創(chuàng)建時指定的)被分配和返回. 至于 dam_alloc_coherent, 結(jié)果 DMA 緩沖地址被返回作為一個內(nèi)核虛擬地址, 并作為一個總線地址被存于 handle.
不需要的緩沖應當返回池, 使用:
void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t addr);
流映射比一致映射有更復雜的接口, 有幾個原因. 這些映射行為使用一個由驅(qū)動已經(jīng)分配的緩沖, 因此, 必須處理它們沒有選擇的地址. 在一些體系上, 流映射也可以有多個不連續(xù)的頁和多部分的"發(fā)散/匯聚"緩沖. 所有這些原因, 流映射有它們自己的一套映射函數(shù).
當建立一個流映射時, 你必須告知內(nèi)核數(shù)據(jù)移向哪個方向. 一些符號(enum dam_data_direction 類型)已為此定義:
DMA_TO_DEVICEDMA_FROM_DEVICE
這 2 個符號應當是自解釋的. 如果數(shù)據(jù)被發(fā)向這個設備(相應地, 也許, 到一個 write 系統(tǒng)調(diào)用), DMA_IO_DEVICE 應當被使用; 去向 CPU 的數(shù)據(jù), 相反, 用 DMA_FROM_DEVICE 標志.
DMA_BIDIRECTIONAL
如果數(shù)據(jù)被在任一方向移動, 使用 DMA_BIDIRECTIONAL.
DMA_NONE
這個符號只作為一個調(diào)試輔助而提供. 試圖使用帶這個方向的緩沖導致內(nèi)核崩潰.
可能在所有時間里試圖只使用 DMA_BIDIRECTIONAL, 但是驅(qū)動作者應當?shù)謸踝∵@個誘惑. 在一些體系上, 這個選擇會有性能損失.
當你有單個緩沖要發(fā)送, 使用 dma_map_single 來映射它:
dma_addr_t dma_map_single(struct device *dev, void *buffer, size_t size, enum dma_data_direction direction);
返回值是總線地址, 你可以傳遞到設備, 或者是 NULL 如果有錯誤.
一旦傳輸完成, 映射應當用 dma_unmap_single 來刪除:
void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size, enum dma_data_direction direction);
這里, size 和 direction 參數(shù)必須匹配那些用來映射緩沖的.
一些重要的規(guī)則適用于流 DMA 映射:
緩沖必須用在只匹配它被映射時給定的方向的傳輸.
一旦一個緩沖已被映射, 它屬于這個設備, 不是處理器. 直到這個緩沖已被去映射, 驅(qū)動不應當以任何方式觸動它的內(nèi)容. 只在調(diào)用 dma_unmap_single 后驅(qū)動才可安全存取緩沖的內(nèi)容(有一個例外, 我們馬上見到). 其他的事情, 這個規(guī)則隱含一個在被寫入設備的緩沖不能被映射, 直到它包含所有的要寫的數(shù)據(jù).
這個緩沖必須不被映射, 當 DMA 仍然激活, 否則肯定會有嚴重的系統(tǒng)不穩(wěn)定.
你可能奇怪為什么一旦一個緩沖已被映射驅(qū)動就不能再使用它. 為什么這個規(guī)則有意義實際上有 2 個原因. 第一, 當一個緩沖為 DMA 而被映射, 內(nèi)核必須確保緩沖中的所有的數(shù)據(jù)實際上已被寫入內(nèi)存. 有可能一些數(shù)據(jù)在處理器的緩存當 dma_unmap_single 被調(diào)用時, 并且必須被明確刷新. 被處理器在刷新后寫入緩沖的數(shù)據(jù)可能對設備不可見.
第二, 考慮一下會發(fā)生什么, 當被映射的緩沖在一個對設備不可存取的內(nèi)存區(qū). 一些體系在這種情況下完全失敗, 但是其他的創(chuàng)建一個反彈緩沖. 反彈緩沖只是一個分開的內(nèi)存區(qū), 它對設備可存取. 如果一個緩沖被映射使用 DMA_TO_DEVICE 方向, 并且要求一個反彈緩沖, 原始緩沖的內(nèi)容作為映射操作的一部分被拷貝. 明顯地, 在拷貝后的對原始緩沖的改變設備見不到. 類似地, DMA_FROM_DEVICE 反彈緩沖被 dma_unmap_single 拷回到原始緩沖; 來自設備的數(shù)據(jù)直到拷貝完成才出現(xiàn).
偶然地, 為什么獲得正確方向是重要的, 反彈緩沖是一個原因. DMA_BIDIRECTIONAL 反彈緩沖在操作前后被拷貝, 這常常是一個 CPU 周期的不必要浪費.
偶爾一個驅(qū)動需要存取一個流 DMA 緩沖的內(nèi)容而不映射它. 已提供了一個調(diào)用來做這個:
void dma_sync_single_for_cpu(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_direction direction);
這個函數(shù)應當在處理器存取一個流 DMA 緩沖前調(diào)用. 一旦已做了這個調(diào)用, CPU "擁有" DMA 緩沖并且可以按需使用它. 在設備存取這個緩沖前, 但是, 擁有權(quán)應當傳遞回給它, 使用:
void dma_sync_single_for_device(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_direction direction);
處理器, 再一次, 在調(diào)用這個之后不應當存取 DMA 緩沖.
偶然地, 你可能想建立一個緩沖的映射, 這個緩沖你有一個 struct page 指針; 例如, 這可能發(fā)生在使用 get_user_pages 映射用戶緩沖. 為建立和取消流映射使用 struct page 指針, 使用下面:
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);
offset 和 size 參數(shù)可被用來映射頁的部分. 但是, 建議部分頁映射應當避免, 除非你真正確信你在做什么. 映射一頁的部分可能導致緩存一致性問題, 如果這個分配只覆蓋一個緩存線的一部分; 這, 隨之, 會導致內(nèi)存破壞和嚴重的難以調(diào)試的錯誤.
發(fā)散/匯聚映射是一個特殊類型的流 DMA 映射. 假設你有幾個緩沖, 都需要傳送數(shù)據(jù)到或者從設備. 這個情況可來自幾個方式, 包括從一個 readv 或者 writev 系統(tǒng)調(diào)用, 一個成簇的磁盤 I/O 請求, 或者一個頁鏈表在一個被映射的內(nèi)核 I/O 緩沖. 你可簡單地映射每個緩沖, 輪流的, 并且進行要求的操作, 但是有幾個優(yōu)點來一次映射整個鏈表.
許多設備可以接收一個散布表數(shù)組指針和長度, 并且傳送它們?nèi)吭谝粋€ DMA 操作中; 例如, "零拷貝"網(wǎng)絡是更輕松如果報文在多個片中建立. 另一個映射發(fā)散列表為一個整體的理由是利用在總線硬件上有映射寄存器的系統(tǒng). 在這樣的系統(tǒng)上, 物理上不連續(xù)的頁從設備的觀點看可被匯集為一個單個的, 連續(xù)的數(shù)組. 這個技術(shù)只當散布表中的項在長度上等于頁大小(除了第一個和最后一個), 但是當它做這個工作時, 它可轉(zhuǎn)換多個操作到一個單個的 DMA, 和有針對性的加速事情.
最后, 如果一個反彈緩沖必須被使用, 應該連接整個列表為一個單個緩沖(因為它在被以任何方式拷貝).
因此現(xiàn)在你確信散布表的映射在某些情況下是值得的. 映射一個散布表的第一步是創(chuàng)建和填充一個 struct scatterlist 數(shù)組, 它描述被傳輸?shù)木彌_. 這個結(jié)構(gòu)是體系依賴的, 并且在 <asm/scatterlist.h> 中描述. 但是, 它常常包含 3 個成員:
struct page *page;
struct page 指針, 對應在發(fā)散/匯聚操作中使用的緩沖.
unsigned int length;unsigned int offset;
緩沖的長度和它的頁內(nèi)偏移.
為映射一個發(fā)散/匯聚 DMA 操作, 你的驅(qū)動應當設置 page, offset, 和 length 成員在一個 struct scatterlist 項給每個要被發(fā)送的緩沖. 接著調(diào)用:
int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction)
這里 nents 是傳入的散布表項的數(shù)目. 返回值是要發(fā)送的 DMA 緩沖的數(shù)目. 它可能小于 nents.
對于輸入散布表中的每個緩沖, dma_map_sg 決定了正確的給設備的總線地址. 作為任務的一部分, 它也連接在內(nèi)存中相近的緩沖. 如果你的驅(qū)動運行的系統(tǒng)有一個 I/O 內(nèi)存管理單元, dma_map_sg 也編程這個單元的映射寄存器, 可能的結(jié)果是, 從你的驅(qū)動的觀點, 你能夠傳輸一個單個的, 連續(xù)的緩沖. 你將不會知道傳送的結(jié)果將看來如何, 但是, 直到在調(diào)用之后.
你的驅(qū)動應當傳送由 pci_map_sg 返回的每個緩沖. 總線地址和每個緩沖的長度存儲于 struct scatterlist 項, 但是它們在結(jié)構(gòu)中的位置每個體系不同. 2 個宏定義已被定義來使得可能編寫可移植的代碼:
dma_addr_t sg_dma_address(struct scatterlist *sg);
從這個散布表入口返回總線( DMA )地址.
unsigned int sg_dma_len(struct scatterlist *sg);
返回這個緩沖的長度.
再次, 記住要傳送的緩沖的地址和長度可能和傳遞給 dma_map_sg 的不同.
一旦傳送完成, 一個 發(fā)散/匯聚 映射被使用 dma_unmap_sg 去映射:
void dma_unmap_sg(struct device *dev, struct scatterlist *list, int nents, enum dma_data_direction direction);
注意 nents 必須是你起初傳遞給 dma_map_sg 的入口項的數(shù)目, 并且不是這個函數(shù)返回給你的 DMA 緩沖的數(shù)目.
發(fā)散/匯聚映射是流 DMA 映射, 并且同樣的存取規(guī)則如同單一映射一樣適用. 如果你必須存取一個被映射的發(fā)散/匯聚列表, 你必須首先同步它:
void dma_sync_sg_for_cpu(struct device *dev, struct scatterlist *sg,
int nents, enum dma_data_direction direction);
void dma_sync_sg_for_device(struct device *dev, struct scatterlist *sg,
int nents, enum dma_data_direction direction);
正常地, DMA 支持層使用 32-位 總線地址, 可能受限于一個特定設備的 DMA 掩碼. PCI 總線, 但是, 也支持一個 64-位地址模式, 雙地址周期(DAC). 通常的 DMA 層不支持這個模式, 因為幾個理由, 第一個是它是一個 PCI-特定 的特性. 還有, 許多 DAC 的實現(xiàn)滿是錯誤, 并且, 因為 DAC 慢于一個常規(guī)的, 32-位 DMA, 可能有一個性能開銷. 即便如此, 有的應用程序使用 DAC 是正確的事情; 如果你有一個設備可能使用非常大的位于高內(nèi)存的緩沖, 你可能要考慮實現(xiàn) DAC 支持. 這個支持只對 PCI 總線適用, 因此 PCI-特定的函數(shù)必須被使用.
為使用 DAC, 你的驅(qū)動必須包含 <linux/pci.h>. 你必須設置一個單獨的 DMA 掩碼:
int pci_dac_set_dma_mask(struct pci_dev *pdev, u64 mask);
你可使用 DAC 尋址只在這個調(diào)用返回 0 時. 一個特殊的類型 (dma64_addr_t) 被用作 DAC 映射. 為建立一個這些映射, 調(diào)用 pci_dac_page_to_dma:
dma64_addr_t pci_dac_page_to_dma(struct pci_dev *pdev, struct page *page, unsigned long offset, int direction);
DAC 映射, 你將注意到, 可能被完成只從 struct page 指針(它們應當位于高內(nèi)存, 畢竟, 否則使用它們沒有意義了); 它們必須一次一頁地被創(chuàng)建. direction 參數(shù)是在通用 DMA 層中使用的 enum dma_data_direction 的 PCI 對等體; 它應當是 PCI_DMA_TODEVICE, PCI_DMA_FROMDEVICE, 或者 PCI_DMA_BIRDIRECTIONAL.
DAC 映射不要求外部資源, 因此在使用后沒有必要明確釋放它們. 但是, 有必要象對待其他流映射一樣對待 DAC 映射, 并且遵守關(guān)于緩沖所有權(quán)的規(guī)則. 有一套函數(shù)來同步 DMA 緩沖, 和通常的變體相似:
void pci_dac_dma_sync_single_for_cpu(struct pci_dev *pdev,
dma64_addr_t dma_addr,
size_t len,
int direction);
void pci_dac_dma_sync_single_for_device(struct pci_dev *pdev,
dma64_addr_t dma_addr,
size_t len,
int direction);
作為一個 DMA 映射如何被使用的例子, 我們展示了一個簡單的給一個 PCI 設備的 DMA 編碼的例子. 在 PCI 總線上的數(shù)據(jù)的 DMA 操作的形式非常依賴被驅(qū)動的設備. 因此, 這個例子不適用于任何真實的設備; 相反, 它是一個稱為 dad ( DMA Acquisiton Device) 的假想驅(qū)動的一部分. 一個給這個設備的驅(qū)動可能定義一個傳送函數(shù)象這樣:
int dad_transfer(struct dad_dev *dev, int write, void *buffer,
size_t count)
{
dma_addr_t bus_addr;
/* Map the buffer for DMA */
dev->dma_dir = (write ? DMA_TO_DEVICE : DMA_FROM_DEVICE);
dev->dma_size = count;
bus_addr = dma_map_single(&dev->pci_dev->dev, buffer, count,
dev->dma_dir);
dev->dma_addr = bus_addr;
/* Set up the device */
writeb(dev->registers.command, DAD_CMD_DISABLEDMA);
writeb(dev->registers.command, write ? DAD_CMD_WR : DAD_CMD_RD);
writel(dev->registers.addr, cpu_to_le32(bus_addr));
writel(dev->registers.len, cpu_to_le32(count));
/* Start the operation */
writeb(dev->registers.command, DAD_CMD_ENABLEDMA);
return 0;
}
這個函數(shù)映射要被傳送的緩沖并且啟動設備操作. 這個工作的另一半必須在中斷服務過程中完成, 這個看來如此:
void dad_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
struct dad_dev *dev = (struct dad_dev *) dev_id;
/* Make sure it's really our device interrupting */
/* Unmap the DMA buffer */
dma_unmap_single(dev->pci_dev->dev, dev->dma_addr,
dev->dma_size, dev->dma_dir);
/* Only now is it safe to access the buffer, copy to user, etc. */
...
}
顯然, 這個例子缺乏大量的細節(jié), 包括可能需要的任何步驟來阻止啟動多個同時的 DMA 操作.
ISA 總線允許 2 類 DMA 傳送: 本地 DMA 和 ISA 總線主 DMA. 本地 DMA 使用在主板上的標準 DMA-控制器電路來驅(qū)動 ISA 總線上的信號線. ISA 總線主 DMA, 另一方面, 完全由外設處理, 至少從驅(qū)動的觀點看. 一個 ISA 總線主的例子是 1542 SCSI 控制器, 在內(nèi)核源碼中是在 drivers/scsi/aha1542.c.
至于本地 DMA, 有 3 個實體包含在 ISA 總線上的 DMA 數(shù)據(jù)傳送.
The 8237 DMA controller (DMAC)
控制器持有關(guān)于 DMA 傳送的信息, 諸如方向, 內(nèi)存地址, 以及傳送的大小. 它還包含一個計數(shù)器來跟蹤進行中的傳送的狀態(tài). 當這個控制器收到一個 DMA 請求信號, 它獲得總線的控制權(quán)并且驅(qū)動信號線以便設備可讀或些它的數(shù)據(jù).
The peripheral device
這個設備必須激活 DMA 請求線當它準備傳送數(shù)據(jù)時. 實際的傳送由 DMAC 管理; 硬件設備順序讀或?qū)憯?shù)據(jù)到總線當控制器探測設備時. 設備常常觸發(fā)中斷當傳送結(jié)束時.
The device driver
這個驅(qū)動什么不做; 它提供給 DMA 控制器方向, 總線地址,和傳送的大小. 它還和它的外設通訊來準備傳送數(shù)據(jù)和響應中斷當 DMA 結(jié)束時.
開始的在 PC 上使用的 DMA 控制器管理 4 個"通道", 每個有一套 DMA 寄存器. 4 個設備可同時存儲它們的 DMA 信息在控制器中. 更新的 PC 包含相同的 2 個 DMAC 設備[51]: 第 2 個控制器(主)被連接到系統(tǒng)的處理器, 并且第 1 個(從)被連接到第 2 個控制器的通道 0.
最初的 PC 只有一個控制器; 第 2 個是在基于 286 的平臺上增加的. 但是, 第 2 個控制器如同主控制器一樣被連接, 因為它處理 16-位的傳送; 第 1 個只傳送 8 位每次并且它為向后兼容而存在.
通道的編號從 0 到 7: 通道 4 對 ISA 外設不可用, 因為它在內(nèi)部用來層疊從控制器到主控制器. 因此, 可用的通道是 0 到 3 在從控制器上( 8-位 通道) 和 5 到 7 到主控制器上( 16-位通道). 任何 DMA 傳送的大小, 當被存儲于控制器中, 是一個代表總線周期的數(shù)目的 16-位數(shù). 最大的傳送大小是, 因此, 64KB 對于從控制器(因為它傳送 8 位在一個周期)和 128KB 對于主控制器( 它進行 16-位 傳送).
因為 DMA 控制器是一個系統(tǒng)范圍的資源, 內(nèi)核幫助處理這個. 它使用一個 DMA 注冊來提供一個請求并釋放機制給 DMA 通道, 和一套函數(shù)來在 DMA 控制器中配置通道信息.
你應當熟悉內(nèi)核注冊 -- 我們已經(jīng)見到它們在 I/O 端口和中斷線. DMA 通道注冊和其他的類似. 在 <asm/dma.h> 中已經(jīng)包含, 下面的函數(shù)可用來獲得和釋放一個 DMA 通道的擁有權(quán):
int request_dma(unsigned int channel, const char *name);
void free_dma(unsigned int channel);
通道參數(shù)是一個在 0 到 7 之間的數(shù), 更精確些, 一個小于 MAX_DMA_CHANNELS 的正值. 在 PC 上, MAX_DMA_CHANNELS 定義為 8 來匹配硬件. name 參數(shù)是一個字符串來標識設備. 特定的 name 出現(xiàn)在文件 /proc/dma, 它可被用戶程序讀.
從 request_dma 的返回值是 0 對于成功, 是 -EINVAL 或者 -EBUSY 如果有錯誤. 前者意思是請求的通道超范圍, 后者意思是另一個設備持有這個通道.
我們推薦你象對待 I/O 端口和中斷線一樣小心對待 DMA 通道; 在打開時請求通道好于從模塊初始化函數(shù)里請求它. 延后請求允許在驅(qū)動之間的一些共享; 例如, 你的聲卡和模擬 I/O 接口可以共享 DMA 通道只要它們不同時使用.
我們還建議你請求 DMA 通道在你已請求中斷線之后并且你在中斷前釋放它. 這是慣用的順序來請求這 2 個資源; 遵循這個慣例避免了死鎖的可能. 注意每個使用 DMA 的設備需要一個 IRQ 線; 否則, 它不能指示數(shù)據(jù)傳送的完成.
在一個典型的情況, open 代碼看來如下, 引用了我們的假想的 dad 模塊. dad 設備使用了一個快速中斷處理, 不帶共享 IRQ 線支持.
int dad_open (struct inode *inode, struct file *filp)
{
struct dad_device *my_device;
/* ... */
if ( (error = request_irq(my_device.irq, dad_interrupt,
SA_INTERRUPT, "dad", NULL)) )
return error; /* or implement blocking open */
if ( (error = request_dma(my_device.dma, "dad")) ) {
free_irq(my_device.irq, NULL);
return error; /* or implement blocking open */
}
/* ... */
return 0;
}
和 open 匹配的 close 實現(xiàn)看來如此:
void dad_close (struct inode *inode, struct file *filp)
{
struct dad_device *my_device;
/* ... */
free_dma(my_device.dma);
free_irq(my_device.irq, NULL);
/* ... */
}
這是 /proc/dma 文件 在一個安裝有聲卡的系統(tǒng)中的樣子:
merlino% cat /proc/dma
1: Sound Blaster8
4: cascade
注意, 缺省的聲音驅(qū)動獲得 DMA 通道在系統(tǒng)啟動時并且從不釋放它. 層疊的入口是一個占位者, 指出通道 4 對驅(qū)動不可用, 如同前面解釋的.
在注冊后, 驅(qū)動工作的主要部分包括配置 DMA 控制器正確操作. 這個任務并非微不足道的, 但是幸運的是, 內(nèi)核輸出了典型驅(qū)動需要的所有的函數(shù).
驅(qū)動需要配置 DMA 控制器或者讀或?qū)懕徽{(diào)用時, 或者當準備異步傳送時. 后面這個任務或者在打開時進行或者響應一個 ioctl 命令, 根據(jù)驅(qū)動和它實現(xiàn)的策略. 這里展示的代碼是典型地被讀或?qū)懺O備方法調(diào)用的.
這一小節(jié)提供一個對于 DMA 控制器內(nèi)部的快速概覽, 這樣你可理解這里介紹的代碼. 如果你想知道更多, 我們勸你讀 <asm/dma.h> 和一些描述 PC 體系的硬件手冊. 特別地, 我們不處理 8-位 和 16-位 傳送的問題. 如果你在編寫設備驅(qū)動給 ISA 設備板, 你應當在設備的硬件手冊中找到相關(guān)的信息.
DMA 控制器是一個共享的資源, 并且如果多個處理器試圖同時對它編程會引起混亂. 為此, 控制器被一個自旋鎖保護, 稱為 dma_spin_lock. 驅(qū)動不應當直接操作這個鎖; 但是, 2 個函數(shù)已提供給你來做這個:
unsigned long claim_dma_lock( );
獲取 DMA 自旋鎖. 這個函數(shù)還在本地處理器上阻塞中斷; 因此, 返回值是一些描述之前中斷狀態(tài)的標志; 它必須被傳遞給隨后的函數(shù)來恢復中斷狀態(tài), 當你用完這個鎖.
void release_dma_lock(unsigned long flags);
返回 DMA 自旋鎖并且恢復前面的中斷狀態(tài).
自旋鎖應當被持有, 當使用下面描述的函數(shù)時. 但是, 它不應當被持有, 在實際的 I/O 當中. 一個驅(qū)動應當從不睡眠當持有一個自旋鎖時.
必須被加載到控制器中的信息包括 3 項: RAM 地址, 必須被傳送的原子項的數(shù)目(以字節(jié)或字計), 以及傳送的方向. 為此, 下列函數(shù)由 <asm/dma.h> 輸出:
void set_dma_mode(unsigned int channel, char mode);
指示是否這個通道必須從設備讀( DMA_MODE_READ)或者寫到設備(DMA_MODE_WRITE). 存在第 3 個模式, DMA_MODE_CASCADE, 它被用來釋放對總線的控制. 層疊是第 1 個控制器連接到第 2 個控制器頂部的方式, 但是它也可以被真正的 ISA 總線主設備使用. 我們這里不討論總線控制.
void set_dma_addr(unsigned int channel, unsigned int addr);
分配 DMA 緩沖的地址. 這個函數(shù)存儲 addr 的低 24 有效位在控制器中. addr 參數(shù)必須是一個總線地址(見"總線地址"一節(jié), 在本章前面).
void set_dma_count(unsigned int channel, unsigned int count);
分配傳送的字節(jié)數(shù). count 參數(shù)也表示給 16-位 通道的字節(jié); 在這個情況下, 這個數(shù)必須是偶數(shù).
除了這些函數(shù), 有一些維護工具必須用, 當處理 DMA 設備時:
void disable_dma(unsigned int channel);
一個 DMA 通道可在控制器內(nèi)部被關(guān)閉. 這個通道應當在控制器被配置為阻止進一步不正確的操作前被關(guān)閉. (否則, 會因為控制器被通過 8-位數(shù)據(jù)傳送被編程而發(fā)生破壞, 并且, 因此, 之前的功能都不自動執(zhí)行.
void enable_dma(unsigned int channel);
這個函數(shù)告知控制器 DMA 通道包含有效數(shù)據(jù).
int get_dma_residue(unsigned int channel);
這個驅(qū)動有時需要知道是否一個 DMA 傳輸已經(jīng)完成. 這個函數(shù)返回仍要被傳送的字節(jié)數(shù). 在一次成功的傳送后的返回值是 0 并且在控制器在工作時是不可預測的 (但不是 0). 這種不可預測性來自需要通過 2 個8-位輸入操作來獲得 16-位 的余數(shù).
void clear_dma_ff(unsigned int channel) ;
這個函數(shù)清理 DMA flip-flop. 這個 flip-flop 用來控制對 16-位 寄存器的存取. 這些寄存器被 2 個連續(xù)的 8-位操作來存取, 并且這個 flip-flop 被用來選擇低有效字節(jié)(當它被清零)或者是最高有效字節(jié)(當它被置位). flip-flop 自動翻轉(zhuǎn)當已經(jīng)傳送了 8 位; 程序員必須清除 flip-flop( 來設置它為已知的狀態(tài) )在存取 DMA 寄存器之前.
使用這些, 一個驅(qū)動可如下實現(xiàn)一個函數(shù)來準備一次 DMA 傳送:
int dad_dma_prepare(int channel, int mode, unsigned int buf, unsigned int count)
{
unsigned long flags;
flags = claim_dma_lock();
disable_dma(channel);
clear_dma_ff(channel);
set_dma_mode(channel, mode);
set_dma_addr(channel, virt_to_bus(buf));
set_dma_count(channel, count);
enable_dma(channel);
release_dma_lock(flags);
return 0;
}
接著, 一個象下一個的函數(shù)被用來檢查 DMA 的成功完成:
int dad_dma_isdone(int channel)
{
int residue;
unsigned long flags = claim_dma_lock ();
residue = get_dma_residue(channel);
release_dma_lock(flags);
return (residue == 0);
}
未完成的唯一一個事情是配置設備板. 這個設備特定的任務常常包含讀或?qū)憥讉€ I/O 端口. 設備在幾個大的方面不同. 例如, 一些設備期望程序員告訴硬件 DMA 緩沖有多大, 并且有時驅(qū)動不得不讀一個被硬連到設備中的值. 為配置板, 硬件手冊是你唯一的朋友.
[49] 當然, 什么事情都有例外; 見"接收中斷緩解"一節(jié)在 17 章, 演示了高性能網(wǎng)絡驅(qū)動如何被使用輪詢最好地實現(xiàn).
[50] 碎片一詞常常用于磁盤來表達文件沒有連續(xù)存儲在磁介質(zhì)上. 相同的概念適用于內(nèi)存, 這里每個虛擬地址空間在整個物理 RAM 散布, 并且難于獲取連續(xù)的空閑頁當請求一個 DMA 緩沖.
[51] 這些電路現(xiàn)在是主板芯片組的一部分, 但是幾年前它們是 2 個單獨的 8237 芯片.
更多建議: