## 15.4.?直接內存存取
直接內存存取, 或者 DMA, 是結束我們的內存問題概覽的高級主題. DMA 是硬件機制允許外設組件來直接傳輸它們的 I/O 數據到和從主內存, 而不需要包含系統處理器. 這種機制的使用能夠很大提高吞吐量到和從一個設備, 因為大量的計算開銷被削減了.
### 15.4.1.?一個 DMA 數據傳輸的概況
在介紹程序細節之前, 讓我們回顧一個 DMA 傳輸如何發生的, 只考慮輸入傳輸來簡化討論.
數據傳輸可由 2 種方法觸發:或者軟件請求數據(通過一個函數例如 read)或者硬件異步推數據到系統.
在第一種情況, 包含的步驟總結如下:
- 1. 當一個進程調用 read, 驅動方法分配一個 DMA 緩沖并引導硬件來傳輸它的數據到那個緩沖. 這個進程被置為睡眠.
- 2. 硬件寫數據到這個 DMA 緩沖并且在它完成時引發一個中斷.
- 3. 中斷處理獲得輸入數據, 確認中斷, 并且喚醒進程, 它現在可以讀數據了.
第 2 種情況到來是當 DMA 被異步使用. 例如, 這發生在數據獲取設備, 它在沒有人讀它們的時候也持續推入數據. 在這個情況下, 驅動應當維護一個緩沖以至于后續的讀調用能返回所有的累積的數據給用戶空間. 這類傳輸包含的步驟有點不同:
- 1. 硬件引發一個中斷來宣告新數據已經到達.
- 2. 中斷處理分配一個緩沖并且告知硬件在哪里傳輸數據.
- 3. 外設寫數據到緩沖并且引發另一個中斷當完成時.
- 處理者分派新數據, 喚醒任何相關的進程, 并且負責雜務.
異步方法的變體常常在網卡中見到. 這些卡常常期望見到一個在內存中和處理器共享的環形緩沖(常常被稱為一個 DMA 的緩沖); 每個到來的報文被放置在環中下一個可用的緩沖, 并且發出一個中斷. 驅動接著傳遞網絡本文到內核其他部分并且在環中放置一個新 DMA 緩沖.
在所有這些情況中的處理的步驟都強調, 有效的 DMA 處理依賴中斷報告. 雖然可能實現 DMA 使用一個輪詢驅動, 它不可能有意義, 因為一個輪詢驅動可能浪費 DMA 提供的性能益處超過更容易的處理器驅動的I/O.[[49](#)]
在這里介紹的另一個相關項是 DMA 緩沖. DMA 要求設備驅動來分配一個或多個特殊的適合 DMA 的緩沖. 注意許多驅動分配它們的緩沖在初始化時并且使用它們直到關閉 -- 在之前列表中的分配一詞, 意思是"獲得一個之前分配的緩沖".
### 15.4.2.?分配 DMA 緩沖
本節涵蓋 DMA 緩沖在底層的分配; 我們稍后介紹一個高級接口, 但是來理解這里展示的內容仍是一個好主意.
隨 DMA 緩沖帶來的主要問題是, 當它們大于一頁, 它們必須占據物理內存的連續頁因為設備使用 ISA 或者 PCI 系統總線傳輸數據, 它們都使用物理地址. 注意有趣的是這個限制不適用 SBus ( 見 12 章的"SBus"一節 ), 它在外設總線上使用虛擬地址. 一些體系結構還可以在 PCI 總線上使用虛擬地址, 但是一個可移植的驅動不能依賴這個功能.
盡管 DMA 緩沖可被分配或者在系統啟動時或者在運行時, 模塊只可在運行時分配它們的緩沖. (第 8 章介紹這些技術; "獲取大緩沖"一節涵蓋在系統啟動時分配, 而"kmalloc 的真實"和"get_free_page 和其友"描述在運行時分配). 驅動編寫者必須關心分配正確的內存,當它被用做 DMA 操作時; 不是所有內存區是合適的. 特別的, 在一些系統中的一些設備上高端內存可能不為 DMA 工作 - 外設完全無法使用高端地址.
在現代總線上的大部分設備可以處理 32-位 地址, 意思是正常的內存分配對它們是剛剛好的. 一些 PCI 設備, 但是, 不能實現完整的 PCI 標準并且不能使用 32-位 地址. 并且 ISA 設備, 當然, 限制只在 24-位 地址.
對于有這種限制的設備, 內存應當從 DMA 區進行分配, 通過添加 GFP_DMA 標志到 kmalloc 或者 get_free_pages 調用. 當這個標志存在, 只有可用 24-位 尋址的內存被分配. 另一種選擇, 你可以使用通用的 DMA 層( 我們馬上討論這個 )來分配緩沖以解決你的設備的限制.
#### 15.4.2.1.?自己做分配
我們已見到 get_free_pages 如何分配直到幾個 MByte (由于 order 可以直到 MAX_ORDER, 當前是 11), 但是高級數的請求容易失敗當請求的緩沖遠遠小于 128 KB, 因為系統內存時間長了變得碎裂.[[50](#)]
當內核無法返回請求數量的內存或者當你需要多于 128 KB(例如, 一個通常的 PCI 幀抓取的請求), 一個替代返回 -ENOMEM 的做法是在啟動時分配內存或者保留物理 RAM 的頂部給你的緩沖. 我們在第 8 章的 "獲得大量緩沖" 一節描述在啟動時間分配, 但是它對模塊是不可用的. 保留 RAM 的頂部是通過在啟動時傳遞一個 mem= 參數給內核實現的. 例如, 如果你有 256 MB, 參數 mem=255M 使內核不使用頂部的 MByte. 你的模塊可能后來使用下列代碼來獲得對這個內存的存取:
~~~
dmabuf = ioremap (0xFF00000 /* 255M */, 0x100000 /* 1M */);
~~~
分配器, 配合本書的例子代碼的一部分, 提供了一個簡單的 API 來探測和管理這樣的保留 RAM 并且已在幾個體系上被成功使用. 但是, 這個技巧當你有一個高內存系統時無效(即, 一個有比適合 CPU 地址空間更多的物理內存的系統 ).
當然, 另一個選項, 是使用 GFP_NOFAIL 來分配你的緩沖. 這個方法, 但是, 確實嚴重地對內存管理子系統有壓力, 并且它冒鎖住系統的風險; 最好是避免除非確實沒有其他方法.
如果你分配一個大 DMA 緩沖到這樣的長度, 但是, 值得想一下替代的方法. 如果你的設備可以做發散/匯聚 I/O, 你可以分配你的緩沖以更小的片段并且讓設備做其他的. 發散/匯聚 I/O 也可以用當進行直接 I/O 到用戶空間時, 它可能是最好地解決方法當需要一個真正大緩沖時.
### 15.4.3.?總線地址
一個使用 DMA 的設備驅動必須和連接到接口總線的硬件通訊, 總線使用物理地址, 而程序代碼使用虛擬地址.
事實上, 情況比這個稍微有些復雜. 基于DMA 的硬件使用總線地址, 而不是物理地址. 盡管 ISA 和 PCI 總線地址在 PC 上完全是物理地址, 這對每個平臺卻不總是真的. 有時接口總線被通過橋接電路連接, 它映射 I/O 地址到不同的物理地址. 一些系統甚至有一個頁映射機制, 使任意的頁連續出現在外設總線.
在最低級別(再次, 我們將馬上查看一個高級解決方法), Linux 內核提供一個可移植的方法, 通過輸出下列函數, 在 <asm/io.h> 定義. 這些函數的使用不被推薦, 因為它們只在有非常簡單的 I/O 體系的系統上正常工作; 但是, 你可能遇到它們當使用內核代碼時.
~~~
unsigned long virt_to_bus(volatile void *address);
void *bus_to_virt(unsigned long address);
~~~
這些函數進行一個簡單的轉換在內核邏輯地址和總線地址之間. 它們在許多情況下不工作, 一個 I/O 內存管理單元必須被編程的地方或者必須使用反彈緩沖的地方. 做這個轉換的正確方法是使用通用的 DMA 層, 因此我們現在轉移到這個主題.
### 15.4.4.?通用 DMA 層
DMA 操作, 最后, 下到分配一個緩沖并且傳遞總線地址到你的設備. 但是, 編寫在所有體系上安全并正確進行 DMA 的可移植啟動的任務比想象的要難. 不同的系統有不同的概念, 關于緩存一致性應當如何工作的概念; 如果你不正確處理這個問題, 你的驅動可能破壞內存. 一些系統有復雜的總線硬件, 它使 DMA 任務更容易 - 或者更難. 并且不是所有的系統可以在內存所有部分進行 DMA. 幸運的是, 內核提供了一個總線和體系獨立的 DMA 層來對驅動作者隱藏大部分這些問題. 我們非常鼓勵你來使用這個層來 DMA 操作, 在任何你編寫的驅動中.
下面的許多函數需要一個指向 struct device 的指針. 這個結構是 Linux 設備模型中設備的低級表示. 它不是驅動常常必須直接使用的東西, 但是你確實需要它當使用通用 DMA 層時. 常常地, 你可發現這個結構, 深埋在描述你的設備的總線. 例如, 它可在 struct pci_device 或者 struct usb_device 中發現它作為 dev 成員. 設備結構在 14 章中詳細描述.
使用下面函數的驅動應當包含 <linux/dma-mapping.h>.
#### 15.4.4.1.?處理困難硬件
在嘗試 DMA 之前必須回答的第一個問題是給定設備是否能夠在當前主機上做這樣的操作. 許多設備受限于它們能夠尋址的內存范圍, 因為許多理由. 缺省地, 內核假定你的設備能夠對任何 32-位 地址進行 DMA. 如果不是這樣, 你應當通知內核這個事實, 使用一個調用:
~~~
int dma_set_mask(struct device *dev, u64 mask);
~~~
mask 應當顯示你的設備能夠尋址的位; 如果它被限制到 24 位, 例如, 你要傳遞 mask 作為 0x0FFFFFF. 返回值是非零如果使用給定的 mask 可以 DMA; 如果 dma_set_mask 返回 0, 你不能對這個設備使用 DMA 操作. 因此, 設備的驅動中的初始化代碼限制到 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 操作, 沒有必要調用 dma_set_mask.
#### 15.4.4.2.?DMA 映射
一個 DMA 映射是分配一個 DMA 緩沖和產生一個設備可以存取的地址的結合. 它試圖使用一個簡單的對 virt_to_bus 的調用來獲得這個地址, 但是有充分的理由來避免那個方法. 它們中的第一個是合理的硬件帶有一個 IOMMU 來為總線提供一套映射寄存器. IOMMU 可為任何物理內存安排來出現在設備可存取的地址范圍內, 并且它可使物理上散布的緩沖對設備看來是連續的. 使用 IOMMU 需要使用通用的 DMA 層; virt_to_bus 不負責這個任務.
注意不是所有的體系都有一個 IOMMU; 特別的, 流行的 x86 平臺沒有 IOMMU 支持. 一個正確編寫的驅動不需要知道它在之上運行的 I/O 支持硬件, 但是.
為設備設置一個有用的地址可能也, 在某些情況下, 要求一個反彈緩沖的建立. 反彈緩沖是當一個驅動試圖在一個外設不能達到的地址上進行 DMA 時創建的, 比如一個高內存地址. 數據接著根據需要被拷貝到和從反彈緩沖. 無需說, 反彈緩沖的使用能拖慢事情, 但是有時沒有其他選擇.
DMA 映射也必須解決緩存一致性問題. 記住現代處理器保持最近存取的內存區的拷貝在一個快速的本地緩沖中; 如果沒有這個緩存, 合理的性能是不可能的. 如果你的設備改變主存一個區, 會強制使任何包含那個區的處理器緩存被失效; 負責處理器可能使用不正確的主存映象, 并且導致數據破壞. 類似地, 當你的設備使用 DMA 來從主存中讀取數據, 任何對那個駐留在處理器緩存的內存的改變必須首先被刷新. 這些緩存一致性問題可以產生無頭的模糊和難尋的錯誤, 如果編程者不小心. 一個體系在硬件中管理緩存一致性, 但是其他的要求軟件支持. 通用的 DMA 層深入很多來保證在所有體系上事情都正確工作, 但是, 如同我們將見到的, 正確的行為要求符合一些規則.
DMA 映射設置一個新類型, dma_addr_t, 來代表總線地址. 類型 dma_addr_t 的變量應當被驅動當作不透明的; 唯一可允許的操作是傳遞它們到 DMA 支持過程和設備自身. 作為一個總線地址, dma_addr_t 可導致不期望的問題如果被 CPU 直接使用.
PCI 代碼在 2 類 DMA 映射中明顯不同, 依賴 DMA 緩沖被期望停留多長時間:
Coherent DMA mappings
連貫的 DMA 映射. 這些映射常常在驅動的生命期內存在. 一個連貫的緩沖必須是同時對 CPU 和外設可用(其他的映射類型, 如同我們之后將看到的, 在任何給定時間只對一個或另一個可用). 結果, 一致的映射必須在緩沖一致的內存. 一致的映射建立和使用可能是昂貴的.
Streaming DMA mappings
流 DMA 映射. 流映射常常為一個單個操作建立. 一些體系當使用流映射時允許大的優化, 如我們所見, 但是這些映射也服從一個更嚴格的關于如何存取它們的規則. 內核開發者建議使用一致映射而不是流映射在任何可能的時候. 這個建議有 2 個原因. 第一個, 在支持映射寄存器的系統上, 每個 DMA 映射在總線上使用它們一個或多個. 一致映射, 有長的生命周期, 可以長時間獨占這些寄存器, 甚至當它們不在使用時. 另外一個原因是, 在某些硬件上, 流映射可以用無法在一致映射中使用的方法來優化.
這 2 種映射類型必須以不同的方式操作; 是時候看看細節了.
#### 15.4.4.3.?建立一致 DMA 映射
一個驅動可以建立一個一致映射, 使用對 dma_alloc_coherent 的調用:
~~~
void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, int flag);
~~~
這個函數處理緩沖的分配和映射. 前 2 個參數是設備結果和需要的緩沖大小. 這個函數返回 DMA 映射的結果在 2 個地方. 來自這個函數的返回值是緩沖的一個內核虛擬地址, 它可被驅動使用; 其間相關的總線地址在 dma_handle 中返回. 分配在這個函數中被處理以至緩沖被放置在一個可以使用 DMA 的位置; 常常地內存只是使用 get_free_pages 來分配(但是注意大小是以字節計的, 而不是一個 order 值). flag 參數是通常的 GFP_ 值來描述內存如何被分配; 常常應當是 GFP_KERNEL (常常) 或者 GFP_ATOMIC (當在原子上下文中運行時).
當不再需要緩沖(常常在模塊卸載時), 它應當被返回給系統, 使用 dma_free_coherent:
~~~
void dma_free_coherent(struct device *dev, size_t size,
void *vaddr, dma_addr_t dma_handle);
~~~
注意, 這個函數象許多通常的 DMA 函數, 需要提供所有的大小, CPU 地址, 和 總線地址參數.
#### 15.4.4.4.?DMA 池
一個 DMA池 是分配小的, 一致DMA映射的分配機制. 從 dma_alloc_coherent 獲得的映射可能有一頁的最小大小. 如果你的驅動需要比那個更小的 DMA 區域, 你應當可能使用一個 DMA 池. DMA 池也在這種情況下有用, 當你可能試圖對嵌在一個大結構中的小區域進行 DMA 操作. 一些非常模糊的驅動錯誤已被追蹤到緩存一致性問題, 在靠近小 DMA 區域的結構成員. 為避免這個問題, 你應當一直明確分配進行 DMA 操作的區域, 和其他的非 DMA 數據結構分開.
DMA 池函數定義在 <linux/dmapool.h>.
一個 DMA 池必須在使用前創建, 使用一個調用:
~~~
struct dma_pool *dma_pool_create(const char *name, struct device *dev,
size_t size, size_t align,
size_t allocation);
~~~
這里, name 是池的名子, dev 是你的設備結構, size 是要從這個池分配的緩沖區大小, align 是來自池的分配要求的硬件對齊(以字節表達的), 以及 allocation是, 如果非零, 一個分配不應當越過的內存邊界. 如果 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);
~~~
對這個調用, mem_flags 是常用的 GFP_ 分配標志的設置. 如果所有都進行順利, 一個內存區(大小是當池創建時指定的)被分配和返回. 至于 dam_alloc_coherent, 結果 DMA 緩沖地址被返回作為一個內核虛擬地址, 并作為一個總線地址被存于 handle.
不需要的緩沖應當返回池, 使用:
~~~
void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t addr);
~~~
#### 15.4.4.5.?建立流 DMA 映射
流映射比一致映射有更復雜的接口, 有幾個原因. 這些映射行為使用一個由驅動已經分配的緩沖, 因此, 必須處理它們沒有選擇的地址. 在一些體系上, 流映射也可以有多個不連續的頁和多部分的"發散/匯聚"緩沖. 所有這些原因, 流映射有它們自己的一套映射函數.
當建立一個流映射時, 你必須告知內核數據移向哪個方向. 一些符號(enum dam_data_direction 類型)已為此定義:
DMA_TO_DEVICEDMA_FROM_DEVICE
這 2 個符號應當是自解釋的. 如果數據被發向這個設備(相應地, 也許, 到一個 write 系統調用), DMA_IO_DEVICE 應當被使用; 去向 CPU 的數據, 相反, 用 DMA_FROM_DEVICE 標志.
DMA_BIDIRECTIONAL
如果數據被在任一方向移動, 使用 DMA_BIDIRECTIONAL.
DMA_NONE
這個符號只作為一個調試輔助而提供. 試圖使用帶這個方向的緩沖導致內核崩潰.
可能在所有時間里試圖只使用 DMA_BIDIRECTIONAL, 但是驅動作者應當抵擋住這個誘惑. 在一些體系上, 這個選擇會有性能損失.
當你有單個緩沖要發送, 使用 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 參數必須匹配那些用來映射緩沖的.
一些重要的規則適用于流 DMA 映射:
-
緩沖必須用在只匹配它被映射時給定的方向的傳輸.
-
一旦一個緩沖已被映射, 它屬于這個設備, 不是處理器. 直到這個緩沖已被去映射, 驅動不應當以任何方式觸動它的內容. 只在調用 dma_unmap_single 后驅動才可安全存取緩沖的內容(有一個例外, 我們馬上見到). 其他的事情, 這個規則隱含一個在被寫入設備的緩沖不能被映射, 直到它包含所有的要寫的數據.
-
這個緩沖必須不被映射, 當 DMA 仍然激活, 否則肯定會有嚴重的系統不穩定.
你可能奇怪為什么一旦一個緩沖已被映射驅動就不能再使用它. 為什么這個規則有意義實際上有 2 個原因. 第一, 當一個緩沖為 DMA 而被映射, 內核必須確保緩沖中的所有的數據實際上已被寫入內存. 有可能一些數據在處理器的緩存當 dma_unmap_single 被調用時, 并且必須被明確刷新. 被處理器在刷新后寫入緩沖的數據可能對設備不可見.
第二, 考慮一下會發生什么, 當被映射的緩沖在一個對設備不可存取的內存區. 一些體系在這種情況下完全失敗, 但是其他的創建一個反彈緩沖. 反彈緩沖只是一個分開的內存區, 它對設備可存取. 如果一個緩沖被映射使用 DMA_TO_DEVICE 方向, 并且要求一個反彈緩沖, 原始緩沖的內容作為映射操作的一部分被拷貝. 明顯地, 在拷貝后的對原始緩沖的改變設備見不到. 類似地, DMA_FROM_DEVICE 反彈緩沖被 dma_unmap_single 拷回到原始緩沖; 來自設備的數據直到拷貝完成才出現.
偶然地, 為什么獲得正確方向是重要的, 反彈緩沖是一個原因. DMA_BIDIRECTIONAL 反彈緩沖在操作前后被拷貝, 這常常是一個 CPU 周期的不必要浪費.
偶爾一個驅動需要存取一個流 DMA 緩沖的內容而不映射它. 已提供了一個調用來做這個:
~~~
void dma_sync_single_for_cpu(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_direction direction);
~~~
這個函數應當在處理器存取一個流 DMA 緩沖前調用. 一旦已做了這個調用, CPU "擁有" DMA 緩沖并且可以按需使用它. 在設備存取這個緩沖前, 但是, 擁有權應當傳遞回給它, 使用:
~~~
void dma_sync_single_for_device(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_direction direction);
~~~
處理器, 再一次, 在調用這個之后不應當存取 DMA 緩沖.
#### 15.4.4.6.?單頁流映射
偶然地, 你可能想建立一個緩沖的映射, 這個緩沖你有一個 struct page 指針; 例如, 這可能發生在使用 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 參數可被用來映射頁的部分. 但是, 建議部分頁映射應當避免, 除非你真正確信你在做什么. 映射一頁的部分可能導致緩存一致性問題, 如果這個分配只覆蓋一個緩存線的一部分; 這, 隨之, 會導致內存破壞和嚴重的難以調試的錯誤.
#### 15.4.4.7.?發散/匯聚映射
發散/匯聚映射是一個特殊類型的流 DMA 映射. 假設你有幾個緩沖, 都需要傳送數據到或者從設備. 這個情況可來自幾個方式, 包括從一個 readv 或者 writev 系統調用, 一個成簇的磁盤 I/O 請求, 或者一個頁鏈表在一個被映射的內核 I/O 緩沖. 你可簡單地映射每個緩沖, 輪流的, 并且進行要求的操作, 但是有幾個優點來一次映射整個鏈表.
許多設備可以接收一個散布表數組指針和長度, 并且傳送它們全部在一個 DMA 操作中; 例如, "零拷貝"網絡是更輕松如果報文在多個片中建立. 另一個映射發散列表為一個整體的理由是利用在總線硬件上有映射寄存器的系統. 在這樣的系統上, 物理上不連續的頁從設備的觀點看可被匯集為一個單個的, 連續的數組. 這個技術只當散布表中的項在長度上等于頁大小(除了第一個和最后一個), 但是當它做這個工作時, 它可轉換多個操作到一個單個的 DMA, 和有針對性的加速事情.
最后, 如果一個反彈緩沖必須被使用, 應該連接整個列表為一個單個緩沖(因為它在被以任何方式拷貝).
因此現在你確信散布表的映射在某些情況下是值得的. 映射一個散布表的第一步是創建和填充一個 struct scatterlist 數組, 它描述被傳輸的緩沖. 這個結構是體系依賴的, 并且在 <asm/scatterlist.h> 中描述. 但是, 它常常包含 3 個成員:
struct page *page;
struct page 指針, 對應在發散/匯聚操作中使用的緩沖.
unsigned int length;unsigned int offset;
緩沖的長度和它的頁內偏移.
為映射一個發散/匯聚 DMA 操作, 你的驅動應當設置 page, offset, 和 length 成員在一個 struct scatterlist 項給每個要被發送的緩沖. 接著調用:
~~~
int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction)
~~~
這里 nents 是傳入的散布表項的數目. 返回值是要發送的 DMA 緩沖的數目. 它可能小于 nents.
對于輸入散布表中的每個緩沖, dma_map_sg 決定了正確的給設備的總線地址. 作為任務的一部分, 它也連接在內存中相近的緩沖. 如果你的驅動運行的系統有一個 I/O 內存管理單元, dma_map_sg 也編程這個單元的映射寄存器, 可能的結果是, 從你的驅動的觀點, 你能夠傳輸一個單個的, 連續的緩沖. 你將不會知道傳送的結果將看來如何, 但是, 直到在調用之后.
你的驅動應當傳送由 pci_map_sg 返回的每個緩沖. 總線地址和每個緩沖的長度存儲于 struct scatterlist 項, 但是它們在結構中的位置每個體系不同. 2 個宏定義已被定義來使得可能編寫可移植的代碼:
~~~
dma_addr_t sg_dma_address(struct scatterlist *sg);
~~~
從這個散布表入口返回總線( DMA )地址.
~~~
unsigned int sg_dma_len(struct scatterlist *sg);
~~~
返回這個緩沖的長度.
再次, 記住要傳送的緩沖的地址和長度可能和傳遞給 dma_map_sg 的不同.
一旦傳送完成, 一個 發散/匯聚 映射被使用 dma_unmap_sg 去映射:
~~~
void dma_unmap_sg(struct device *dev, struct scatterlist *list, int nents, enum dma_data_direction direction);
~~~
注意 nents 必須是你起初傳遞給 dma_map_sg 的入口項的數目, 并且不是這個函數返回給你的 DMA 緩沖的數目.
發散/匯聚映射是流 DMA 映射, 并且同樣的存取規則如同單一映射一樣適用. 如果你必須存取一個被映射的發散/匯聚列表, 你必須首先同步它:
~~~
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);
~~~
#### 15.4.4.8.?PCI 雙地址周期映射
正常地, DMA 支持層使用 32-位 總線地址, 可能受限于一個特定設備的 DMA 掩碼. PCI 總線, 但是, 也支持一個 64-位地址模式, 雙地址周期(DAC). 通常的 DMA 層不支持這個模式, 因為幾個理由, 第一個是它是一個 PCI-特定 的特性. 還有, 許多 DAC 的實現滿是錯誤, 并且, 因為 DAC 慢于一個常規的, 32-位 DMA, 可能有一個性能開銷. 即便如此, 有的應用程序使用 DAC 是正確的事情; 如果你有一個設備可能使用非常大的位于高內存的緩沖, 你可能要考慮實現 DAC 支持. 這個支持只對 PCI 總線適用, 因此 PCI-特定的函數必須被使用.
為使用 DAC, 你的驅動必須包含 <linux/pci.h>. 你必須設置一個單獨的 DMA 掩碼:
~~~
int pci_dac_set_dma_mask(struct pci_dev *pdev, u64 mask);
~~~
你可使用 DAC 尋址只在這個調用返回 0 時. 一個特殊的類型 (dma64_addr_t) 被用作 DAC 映射. 為建立一個這些映射, 調用 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 指針(它們應當位于高內存, 畢竟, 否則使用它們沒有意義了); 它們必須一次一頁地被創建. direction 參數是在通用 DMA 層中使用的 enum dma_data_direction 的 PCI 對等體; 它應當是 PCI_DMA_TODEVICE, PCI_DMA_FROMDEVICE, 或者 PCI_DMA_BIRDIRECTIONAL.
DAC 映射不要求外部資源, 因此在使用后沒有必要明確釋放它們. 但是, 有必要象對待其他流映射一樣對待 DAC 映射, 并且遵守關于緩沖所有權的規則. 有一套函數來同步 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);
~~~
#### 15.4.4.9.?一個簡單的 PCI DMA 例子
作為一個 DMA 映射如何被使用的例子, 我們展示了一個簡單的給一個 PCI 設備的 DMA 編碼的例子. 在 PCI 總線上的數據的 DMA 操作的形式非常依賴被驅動的設備. 因此, 這個例子不適用于任何真實的設備; 相反, 它是一個稱為 dad ( DMA Acquisiton Device) 的假想驅動的一部分. 一個給這個設備的驅動可能定義一個傳送函數象這樣:
~~~
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;
}
~~~
這個函數映射要被傳送的緩沖并且啟動設備操作. 這個工作的另一半必須在中斷服務過程中完成, 這個看來如此:
~~~
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. */
...
}
~~~
顯然, 這個例子缺乏大量的細節, 包括可能需要的任何步驟來阻止啟動多個同時的 DMA 操作.
### 15.4.5.?ISA 設備的 DMA
ISA 總線允許 2 類 DMA 傳送: 本地 DMA 和 ISA 總線主 DMA. 本地 DMA 使用在主板上的標準 DMA-控制器電路來驅動 ISA 總線上的信號線. ISA 總線主 DMA, 另一方面, 完全由外設處理, 至少從驅動的觀點看. 一個 ISA 總線主的例子是 1542 SCSI 控制器, 在內核源碼中是在 drivers/scsi/aha1542.c.
至于本地 DMA, 有 3 個實體包含在 ISA 總線上的 DMA 數據傳送.
The 8237 DMA controller (DMAC)
控制器持有關于 DMA 傳送的信息, 諸如方向, 內存地址, 以及傳送的大小. 它還包含一個計數器來跟蹤進行中的傳送的狀態. 當這個控制器收到一個 DMA 請求信號, 它獲得總線的控制權并且驅動信號線以便設備可讀或些它的數據.
The peripheral device
這個設備必須激活 DMA 請求線當它準備傳送數據時. 實際的傳送由 DMAC 管理; 硬件設備順序讀或寫數據到總線當控制器探測設備時. 設備常常觸發中斷當傳送結束時.
The device driver
這個驅動什么不做; 它提供給 DMA 控制器方向, 總線地址,和傳送的大小. 它還和它的外設通訊來準備傳送數據和響應中斷當 DMA 結束時.
開始的在 PC 上使用的 DMA 控制器管理 4 個"通道", 每個有一套 DMA 寄存器. 4 個設備可同時存儲它們的 DMA 信息在控制器中. 更新的 PC 包含相同的 2 個 DMAC 設備[[51](#)]: 第 2 個控制器(主)被連接到系統的處理器, 并且第 1 個(從)被連接到第 2 個控制器的通道 0.
最初的 PC 只有一個控制器; 第 2 個是在基于 286 的平臺上增加的. 但是, 第 2 個控制器如同主控制器一樣被連接, 因為它處理 16-位的傳送; 第 1 個只傳送 8 位每次并且它為向后兼容而存在.
通道的編號從 0 到 7: 通道 4 對 ISA 外設不可用, 因為它在內部用來層疊從控制器到主控制器. 因此, 可用的通道是 0 到 3 在從控制器上( 8-位 通道) 和 5 到 7 到主控制器上( 16-位通道). 任何 DMA 傳送的大小, 當被存儲于控制器中, 是一個代表總線周期的數目的 16-位數. 最大的傳送大小是, 因此, 64KB 對于從控制器(因為它傳送 8 位在一個周期)和 128KB 對于主控制器( 它進行 16-位 傳送).
因為 DMA 控制器是一個系統范圍的資源, 內核幫助處理這個. 它使用一個 DMA 注冊來提供一個請求并釋放機制給 DMA 通道, 和一套函數來在 DMA 控制器中配置通道信息.
#### 15.4.5.1.?注冊 DMA 使用
你應當熟悉內核注冊 -- 我們已經見到它們在 I/O 端口和中斷線. DMA 通道注冊和其他的類似. 在 <asm/dma.h> 中已經包含, 下面的函數可用來獲得和釋放一個 DMA 通道的擁有權:
~~~
int request_dma(unsigned int channel, const char *name);
void free_dma(unsigned int channel);
~~~
通道參數是一個在 0 到 7 之間的數, 更精確些, 一個小于 MAX_DMA_CHANNELS 的正值. 在 PC 上, MAX_DMA_CHANNELS 定義為 8 來匹配硬件. name 參數是一個字符串來標識設備. 特定的 name 出現在文件 /proc/dma, 它可被用戶程序讀.
從 request_dma 的返回值是 0 對于成功, 是 -EINVAL 或者 -EBUSY 如果有錯誤. 前者意思是請求的通道超范圍, 后者意思是另一個設備持有這個通道.
我們推薦你象對待 I/O 端口和中斷線一樣小心對待 DMA 通道; 在打開時請求通道好于從模塊初始化函數里請求它. 延后請求允許在驅動之間的一些共享; 例如, 你的聲卡和模擬 I/O 接口可以共享 DMA 通道只要它們不同時使用.
我們還建議你請求 DMA 通道在你已請求中斷線之后并且你在中斷前釋放它. 這是慣用的順序來請求這 2 個資源; 遵循這個慣例避免了死鎖的可能. 注意每個使用 DMA 的設備需要一個 IRQ 線; 否則, 它不能指示數據傳送的完成.
在一個典型的情況, 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 實現看來如此:
~~~
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 文件 在一個安裝有聲卡的系統中的樣子:
~~~
merlino% cat /proc/dma
1: Sound Blaster8
4: cascade
~~~
注意, 缺省的聲音驅動獲得 DMA 通道在系統啟動時并且從不釋放它. 層疊的入口是一個占位者, 指出通道 4 對驅動不可用, 如同前面解釋的.
#### 15.4.5.2.?和 DMA 控制器通訊
在注冊后, 驅動工作的主要部分包括配置 DMA 控制器正確操作. 這個任務并非微不足道的, 但是幸運的是, 內核輸出了典型驅動需要的所有的函數.
驅動需要配置 DMA 控制器或者讀或寫被調用時, 或者當準備異步傳送時. 后面這個任務或者在打開時進行或者響應一個 ioctl 命令, 根據驅動和它實現的策略. 這里展示的代碼是典型地被讀或寫設備方法調用的.
這一小節提供一個對于 DMA 控制器內部的快速概覽, 這樣你可理解這里介紹的代碼. 如果你想知道更多, 我們勸你讀 <asm/dma.h> 和一些描述 PC 體系的硬件手冊. 特別地, 我們不處理 8-位 和 16-位 傳送的問題. 如果你在編寫設備驅動給 ISA 設備板, 你應當在設備的硬件手冊中找到相關的信息.
DMA 控制器是一個共享的資源, 并且如果多個處理器試圖同時對它編程會引起混亂. 為此, 控制器被一個自旋鎖保護, 稱為 dma_spin_lock. 驅動不應當直接操作這個鎖; 但是, 2 個函數已提供給你來做這個:
unsigned long claim_dma_lock( );
獲取 DMA 自旋鎖. 這個函數還在本地處理器上阻塞中斷; 因此, 返回值是一些描述之前中斷狀態的標志; 它必須被傳遞給隨后的函數來恢復中斷狀態, 當你用完這個鎖.
void release_dma_lock(unsigned long flags);
返回 DMA 自旋鎖并且恢復前面的中斷狀態.
自旋鎖應當被持有, 當使用下面描述的函數時. 但是, 它不應當被持有, 在實際的 I/O 當中. 一個驅動應當從不睡眠當持有一個自旋鎖時.
必須被加載到控制器中的信息包括 3 項: RAM 地址, 必須被傳送的原子項的數目(以字節或字計), 以及傳送的方向. 為此, 下列函數由 <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 緩沖的地址. 這個函數存儲 addr 的低 24 有效位在控制器中. addr 參數必須是一個總線地址(見"總線地址"一節, 在本章前面).
void set_dma_count(unsigned int channel, unsigned int count);
分配傳送的字節數. count 參數也表示給 16-位 通道的字節; 在這個情況下, 這個數必須是偶數.
除了這些函數, 有一些維護工具必須用, 當處理 DMA 設備時:
void disable_dma(unsigned int channel);
一個 DMA 通道可在控制器內部被關閉. 這個通道應當在控制器被配置為阻止進一步不正確的操作前被關閉. (否則, 會因為控制器被通過 8-位數據傳送被編程而發生破壞, 并且, 因此, 之前的功能都不自動執行.
void enable_dma(unsigned int channel);
這個函數告知控制器 DMA 通道包含有效數據.
int get_dma_residue(unsigned int channel);
這個驅動有時需要知道是否一個 DMA 傳輸已經完成. 這個函數返回仍要被傳送的字節數. 在一次成功的傳送后的返回值是 0 并且在控制器在工作時是不可預測的 (但不是 0). 這種不可預測性來自需要通過 2 個8-位輸入操作來獲得 16-位 的余數.
void clear_dma_ff(unsigned int channel) ;
這個函數清理 DMA flip-flop. 這個 flip-flop 用來控制對 16-位 寄存器的存取. 這些寄存器被 2 個連續的 8-位操作來存取, 并且這個 flip-flop 被用來選擇低有效字節(當它被清零)或者是最高有效字節(當它被置位). flip-flop 自動翻轉當已經傳送了 8 位; 程序員必須清除 flip-flop( 來設置它為已知的狀態 )在存取 DMA 寄存器之前.
使用這些, 一個驅動可如下實現一個函數來準備一次 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;
}
~~~
接著, 一個象下一個的函數被用來檢查 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);
}
~~~
未完成的唯一一個事情是配置設備板. 這個設備特定的任務常常包含讀或寫幾個 I/O 端口. 設備在幾個大的方面不同. 例如, 一些設備期望程序員告訴硬件 DMA 緩沖有多大, 并且有時驅動不得不讀一個被硬連到設備中的值. 為配置板, 硬件手冊是你唯一的朋友.
[[49](#)] 當然, 什么事情都有例外; 見"接收中斷緩解"一節在 17 章, 演示了高性能網絡驅動如何被使用輪詢最好地實現.
[[50](#)] 碎片一詞常常用于磁盤來表達文件沒有連續存儲在磁介質上. 相同的概念適用于內存, 這里每個虛擬地址空間在整個物理 RAM 散布, 并且難于獲取連續的空閑頁當請求一個 DMA 緩沖.
[[51](#)] 這些電路現在是主板芯片組的一部分, 但是幾年前它們是 2 個單獨的 8237 芯片.
- Linux設備驅動第三版
- 第 1 章 設備驅動簡介
- 1.1. 驅動程序的角色
- 1.2. 劃分內核
- 1.3. 設備和模塊的分類
- 1.4. 安全問題
- 1.5. 版本編號
- 1.6. 版權條款
- 1.7. 加入內核開發社團
- 1.8. 本書的內容
- 第 2 章 建立和運行模塊
- 2.1. 設置你的測試系統
- 2.2. Hello World 模塊
- 2.3. 內核模塊相比于應用程序
- 2.4. 編譯和加載
- 2.5. 內核符號表
- 2.6. 預備知識
- 2.7. 初始化和關停
- 2.8. 模塊參數
- 2.9. 在用戶空間做
- 2.10. 快速參考
- 第 3 章 字符驅動
- 3.1. scull 的設計
- 3.2. 主次編號
- 3.3. 一些重要數據結構
- 3.4. 字符設備注冊
- 3.5. open 和 release
- 3.6. scull 的內存使用
- 3.7. 讀和寫
- 3.8. 使用新設備
- 3.9. 快速參考
- 第 4 章 調試技術
- 4.1. 內核中的調試支持
- 4.2. 用打印調試
- 4.3. 用查詢來調試
- 4.4. 使用觀察來調試
- 4.5. 調試系統故障
- 4.6. 調試器和相關工具
- 第 5 章 并發和競爭情況
- 5.1. scull 中的缺陷
- 5.2. 并發和它的管理
- 5.3. 旗標和互斥體
- 5.4. Completions 機制
- 5.5. 自旋鎖
- 5.6. 鎖陷阱
- 5.7. 加鎖的各種選擇
- 5.8. 快速參考
- 第 6 章 高級字符驅動操作
- 6.1. ioctl 接口
- 6.2. 阻塞 I/O
- 6.3. poll 和 select
- 6.4. 異步通知
- 6.5. 移位一個設備
- 6.6. 在一個設備文件上的存取控制
- 6.7. 快速參考
- 第 7 章 時間, 延時, 和延后工作
- 7.1. 測量時間流失
- 7.2. 獲知當前時間
- 7.3. 延后執行
- 7.4. 內核定時器
- 7.5. Tasklets 機制
- 7.6. 工作隊列
- 7.7. 快速參考
- 第 8 章 分配內存
- 8.1. kmalloc 的真實故事
- 8.2. 后備緩存
- 8.3. get_free_page 和其友
- 8.4. 每-CPU 的變量
- 8.5. 獲得大量緩沖
- 8.6. 快速參考
- 第 9 章 與硬件通訊
- 9.1. I/O 端口和 I/O 內存
- 9.2. 使用 I/O 端口
- 9.3. 一個 I/O 端口例子
- 9.4. 使用 I/O 內存
- 9.5. 快速參考
- 第 10 章 中斷處理
- 10.1. 準備并口
- 10.2. 安裝一個中斷處理
- 10.3. 前和后半部
- 10.4. 中斷共享
- 10.5. 中斷驅動 I/O
- 10.6. 快速參考
- 第 11 章 內核中的數據類型
- 11.1. 標準 C 類型的使用
- 11.2. 安排一個明確大小給數據項
- 11.3. 接口特定的類型
- 11.4. 其他移植性問題
- 11.5. 鏈表
- 11.6. 快速參考
- 第 12 章 PCI 驅動
- 12.1. PCI 接口
- 12.2. 回顧: ISA
- 12.3. PC/104 和 PC/104+
- 12.4. 其他的 PC 總線
- 12.5. SBus
- 12.6. NuBus 總線
- 12.7. 外部總線
- 12.8. 快速參考
- 第 13 章 USB 驅動
- 13.1. USB 設備基礎知識
- 13.2. USB 和 sysfs
- 13.3. USB 的 Urbs
- 13.4. 編寫一個 USB 驅動
- 13.5. 無 urb 的 USB 傳送
- 13.6. 快速參考
- 第 14 章 Linux 設備模型
- 14.1. Kobjects, Ksets 和 Subsystems
- 14.2. 低級 sysfs 操作
- 14.3. 熱插拔事件產生
- 14.4. 總線, 設備, 和驅動
- 14.5. 類
- 14.6. 集成起來
- 14.7. 熱插拔
- 14.8. 處理固件
- 14.9. 快速參考
- 第 15 章 內存映射和 DMA
- 15.1. Linux 中的內存管理
- 15.2. mmap 設備操作
- 15.3. 進行直接 I/O
- 15.4. 直接內存存取
- 15.5. 快速參考
- 第 16 章 塊驅動
- 16.1. 注冊
- 16.2. 塊設備操作
- 16.3. 請求處理
- 16.4. 一些其他的細節
- 16.5. 快速參考
- 第 17 章 網絡驅動
- 17.1. snull 是如何設計的
- 17.2. 連接到內核
- 17.3. net_device 結構的詳情
- 17.4. 打開與關閉
- 17.5. 報文傳送
- 17.6. 報文接收
- 17.7. 中斷處理
- 17.8. 接收中斷緩解
- 17.9. 連接狀態的改變
- 17.10. Socket 緩存
- 17.11. MAC 地址解析
- 17.12. 定制 ioctl 命令
- 17.13. 統計信息
- 17.14. 多播
- 17.15. 幾個其他細節
- 17.16. 快速參考
- 第 18 章 TTY 驅動
- 18.1. 一個小 TTY 驅動
- 18.2. tty_driver 函數指針
- 18.3. TTY 線路設置
- 18.4. ioctls 函數
- 18.5. TTY 設備的 proc 和 sysfs 處理
- 18.6. tty_driver 結構的細節
- 18.7. tty_operaions 結構的細節
- 18.8. tty_struct 結構的細節
- 18.9. 快速參考