裝置IO之二(DMA)
阿新 • • 發佈:2018-11-05
DMA是硬體的一種能力,具備這種能力的硬體可以直接從主存中讀寫資料,也就是它可以直接使用主存進行I/O而不需要處理器的干預,這可以節省處理器資源並提高整個系統的IO吞吐量,因為IO操作相對來說是較慢的,如果每個IO都要使用處理器資源,則毫無以為會耗費大量CPU時間在單個IO上,最終導致系統IO效能下降。
一、DMA工作方式
對於I/O來說,在輸入端存在兩種工作模式:- 軟體發起讀請求,然後硬體響應該請求(儲存器多用該方式)
- 硬體產生輸入事件,然後硬體處理(網絡卡多用該方式)
由於DMA是一種I/O的方式,因而這樣是它工作的場景。
DMA的工作方式是:
- 在輸入時,軟體準備一塊記憶體區(DMA緩衝區),然後告知硬體,硬體通過DMA的方式將資料寫入這部分割槽域
- 在輸出時,軟體準備好一塊包含輸出資料的記憶體區(DMA緩衝區),然後告知硬體,硬體通過DMA的方式獲得這部分資料並輸出出去。
二、分配DMA快取
當使用DMA時,需要注意如果DMA緩衝區的大小大於一頁,則它們必須佔據連續的實體記憶體頁,因為裝置進行I/O時需要通過它所連線的匯流排(典型的匯流排就是PCI匯流排)進行,也就是說裝置需要通過匯流排來訪問這部分地址,在有些架構上匯流排需要使用實體地址,因而為了可移植性,總好總使用實體地址。分配緩衝區的機制可以是在系統啟動時,也可以是在系統執行時,驅動的實現者需要根據自己的情形做出選擇。驅動必須保證自己分配了正確的緩衝區(分配標記GFP_DMA可以幫助從DMA區域分配記憶體)。
編入核心的核心部件可以在系統啟動時為自己預留大塊的記憶體,但是如果一個編譯為核心模組的核心部件需要使用大塊記憶體時,該方式就不適用了,這個時候可以用另外一種方式:假設系統總共有4G記憶體,一個核心模組想要為自己預留100M大小的記憶體區域,則可以在系統啟動時,設定啟動引數mem=3.9G,這樣核心將不使用最後的100M,然後該核心部件可以使用ioremap來獲得最後的100M的記憶體。
由於匯流排使用實體地址,而程式使用的是虛擬地址,因而二者之間需要進行轉換,核心提供瞭如下兩個函式在二者之間進行轉換:
unsigned long virt_to_bus(volatile void *address);
void *bus_to_virt(unsigned long address);
這裡說的分配DMA緩衝區的方式以及匯流排地址和虛擬地址之間轉換的方式都是低level的一些介面,使用這些介面時,使用者需要對硬體以及該結構非常熟悉,也就說說驅動編寫者要確保自己知道所有的東西。實際上有更好的方案:為了方便使用DMA,核心提供了通用的DMA層,最好使用通用DMA層的介面,這樣可以簡化驅動的編寫工作。
三、通用DMA層
3.1 設定硬體DMA能力
通用DMA層假設裝置都能在32位地址上執行DMA,如果一個裝置不能再32位地址上執行DMA,則它應該呼叫int dma_set_mask(struct device *dev, u64 mask);
來設定自己可以進行DMA的地址能力,比如如果裝置只能在16位地址上進行DMA,則應該設定mask為0xffff。
該函式的返回值表明核心是否支援在指定的掩碼上進行DMA,如果返回非0,則表明核心支援這樣的DMA,如果返回0,則核心不支援在這樣的DMA,裝置就無法再進行DMA操作了。
如果裝置支援在32位地址上進行DMA,則不必執行該函式。
3.2 DMA對映
前邊提到用virt_to_bus可以將虛擬地址轉變成匯流排地址,但是它並不總是正確的,因為有的架構支援IOMMU,支援IOMMU的硬體為匯流排提供了一套對映暫存器。IOMMU在裝置可訪問的地址空間範圍內管理實體記憶體,該機制使得物理上分散的緩衝區對裝置來說可能是連續的了。在這種方式下virt_to_bus是無法工作的。而通用DMA層則包括了對IOMMU的使用支援。因而使用通用DMA層更簡單,更不易錯。
DMA對映必須解決快取一致性的問題,這是所有涉及到低階記憶體訪問的操作都需要考慮的問題,因為處理器會快取最近被使用的記憶體,如果該快取和它對應的主存的資料不一致就可能導致問題。通用DMA層會完成這個工作。
DMA對映使用資料結構dma_addr_t來代表匯流排地址。它由匯流排使用,驅動不應使用它。
根據DMA緩衝區的生命週期,存在兩種型別的DMA對映:
3.2.1 一致DMA對映
該型別的對映存在週期和驅動的生命週期一樣。這種對映的緩衝區必須同時可以被CPU和外設訪問。因此一致性對映必須建立在一致性快取中,該型別的對映的建立和使用開銷比較大。通過dma_alloc_coherent可以建立一致性對映,其原型如下:
void * dmam_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t gfp);
它完成緩衝區的分配和對映。各引數的含義:
- dev:裝置device結構
- size:以位元組為單位的緩衝區大小
- dma_handle:與該緩衝區相關的匯流排地址
- gfp:分配標記
當使用完後,需要使用dmam_free_coherent來釋放DMA緩衝區,其原型如下:
void dmam_free_coherent(struct device *dev, size_t size, void *vaddr,dma_addr_t dma_handle);
各引數含義和分配時的相同。
除了以上兩個API外,核心還提供了一個生成小型、一致性DMA對映的機制—DMA池。它可以生成較小的一致性DMA緩衝區。
使用DMA池中的緩衝區時,需要首先建立DMA池,DMA池用dma_pool_create來建立,用dma_pool_destory來釋放。其原型分別如下:
struct dma_pool *dma_pool_create(const char *name, struct device *dev,size_t size, size_t align, size_t boundary);
各引數含義如下:
- name:DMA池的名字
- dev:裝置資料結構指標
- size:從該DMA池中分配的緩衝區的大小
- align:從該池分配時所遵循的對其原則
- boundary:如果它不為0,則從該DMA池返回的記憶體不能越過2的boundary次方的邊界。
在使用時,需要從DMA池中分配DMA快取,從DMA池分配DMA快取使用函式dma_pool_alloc,其原型如下:
void *dma_pool_alloc(struct dma_pool *pool, gfp_t mem_flags, dma_addr_t *handle);
各引數含義如下:
- pool:從其中進行分配的DMA池
- mem_flags:分配標記
- handle:該緩衝區對應的匯流排地址
當使用完從DMA池分配的DMA緩衝區時,需要使用dma_pool_free來釋放。其原型如下:
void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t dma);其引數含義同dma_pool_alloc
3.2.2 流式DMA對映
- 通常為單獨的DMA操作建立流式DMA 對映。在一些架構上,流式DMA對映被優化了,當然這需要遵循嚴格的訪問規則。在使用DMA對映時,應該優先選擇流式DMA,原因在於:
- 在支援對映暫存器的系統上,每個DMA 對映需要在總線上使用一個或多個的對映暫存器。一致對映具有很長的宣告週期,因而會長期佔用這些寶貴的資源,這有時候是一種浪費。
- 在某些硬體上,流式對映可以使用一致對映中無法使用的方式進行優化。
3.2.2.1 建立流式DMA 對映
相對於一致性對映,流式對映的介面比較複雜,這是因為:- 流式對映應該能與已經由驅動分配的緩衝區一起工作,因而不得不處理那些不是它們所選擇的地址(但是已經被驅動分配的)。
- 某些架構上,流式對映能夠擁有多個不連續的頁和多個“分散/聚集”緩衝區。
- DMA_TO_DEVICE
- DMA_FROM_DEVICE
- DMA_BIDIRECTIONAL
- DMA_NONE
驅動不應該總是使用DMA_BIDIRECTIONAL,因為在某些架構,這可能導致效能急劇下降。
當只有一個緩衝區要傳輸時,使用函式dma_map_single來對映它,其原型如下:
dma_addr_t dma_map_single(struct device *dev, void *ptr, size_t size, enum dma_data_direction direction);
它將建立一個流式對映,並將核心虛擬地址和匯流排地址關聯起來。在這一步完成後,核心會保證緩衝區所包含的所有資料都已經進入主存而不是在CPU快取(即cache)中。各個引數含義如下:
- dev:裝置資料結構指標
- ptr:指向DMA緩衝區的指標
- size:大小
- direction:資料流動的方向
void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size, enum dma_data_direction direction);
它刪除指定的對映,引數含義同建立對映的。
流式DMA的規則:
- 緩衝區只能用於direction指定的資料傳輸
- 一旦緩衝被對映,它就屬於裝置,而不屬於處理器。在該對映被刪除前,驅動不能以任何方式訪問該緩衝區。
- 在DMA活動期間,即裝置還在使用該緩衝區時,不能刪除這個對映。
void dma_sync_single_for_cpu(struct device *dev, dma_addr_t dma_handle, size_t size, enum dma_data_direction direction);
void dma_sync_single_for_device(struct device *dev, dma_addr_t addr, size_t size, enum dma_data_direction dir);
3.2.2.2 單頁流對映
通用DMA框架也提供了對單頁進行DMA對映以及取消對映的API,相關的API如下:
dma_addr_t dma_map_page(struct device *dev, struct page *page, size_t offset, size_t size, enum dma_data_direction dir);
引數含義如下:
- dev:裝置資料結構指標
- page:指向作為DAM緩衝區的page指標
- offset:對映從page的何處開始
- size:對映區域的大小
- dir:資料流動方向
void dma_unmap_page(struct device *dev, dma_addr_t addr, size_t size, enum dma_data_direction dir);
該函式用於取消對映
3.2.2.3 發散/匯聚對映
通用DMA框架還提供了一種特殊型別的流DMA對映機制--發散/匯聚對映。該機制允許一次為多個緩衝區建立DMA對映。其原型如下:int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);
各引數含義如下:
- dev:裝置資料結構指標
- sg:緩衝區列表的第一個緩衝區的指標
- nets:sg中有多少個緩衝區
- direction:資料流動方向
資料結構scatterlist包含了每個緩衝區的資訊,其定義如下:
struct scatterlist {
#ifdef CONFIG_DEBUG_SG
unsigned long sg_magic;
#endif
unsigned long page_link;
unsigned int offset;
unsigned int length;
dma_addr_t dma_address;
#ifdef CONFIG_NEED_SG_DMA_LENGTH
unsigned int dma_length;
#endif
};
注意如果sg已經對映過了,則不能再對其進行對映,再次對映會損壞sg中的資訊。對於sg中的每個緩衝,該函式會正確的為其產生裝置匯流排地址,驅動應該使用該匯流排地址,核心提供了兩個相關的巨集:
dma_addr_t sg_dma_address(struct scatterlist *sg);
用於從scatterlist返回匯流排( DMA )地址.
unsigned int sg_dma_len(struct scatterlist *sg);
用於返回這個緩衝的長度.
void dma_unmap_sg(struct device *dev, struct scatterlist *list, int nents, enum dma_data_direction direction);
該函式用於取消發散/匯聚對映。netns必須等於傳給dma_map_sg的值,而不是dma_map_sg返回的值。
類似於單一對映,如果CPU必須訪問已經映射了的緩衝區,則必須先讓CPU獲取這些緩衝區,對應的API如下:
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控制器(DMAC)
DMA控制器擁有關於DMA傳送的資訊,比如傳送的方向,記憶體地址,傳送資料的大小。它還包含了一個計數器來跟蹤進行中的傳送的狀態。當控制器收到一個DMA請求訊號時,它會獲得匯流排的控制權,並驅動訊號線以便裝置可以讀寫資料。當外設想要傳送資料時,它必須首先啟用DMA請求線,實際的傳輸由DMAC管理。當DMA控制器選中裝置時,即裝置可以訪問匯流排時,它就在總線上進行讀寫,當讀寫完成時,裝置常常通過中斷來進行通知。外設的驅動負責向DMAC提供傳輸的方向,匯流排地址以及傳送資料的大小。同時外設的驅動還要負責準備傳送的資料並且在DMA結束時響應中斷。
DMA控制器包括了多個(4個)DMA通道,每個通道都與一組DMA暫存器相關聯,這些暫存器用於儲存進行DMA操作所需要的資訊,因此DAM通道數目決定了可以同時由DMA控制器管理的DMA的數目。每次DMA傳輸的大小儲存在DMA控制器中,表示每次傳輸需要多少個匯流排週期,匯流排週期*匯流排寬頻即可得到每次所傳輸的資料大小。DMA控制器是一個系統範圍的資源,並且DMA資源以通道的形式存在。核心提供了一套API來管理這個資源。
4.1 註冊 DMA
類似於中斷線,核心提供了一個API用於申請試用DMA通道。相應的API如下:int request_dma(unsigned int chan, const char *dev_id);
各引數含義:
chan:請求的通道號。是一個小於MAX_DMA_CHANNELS的值
dev_id:用於標識誰在請求DMA通道資源。
函式成功時返回0
void free_dma(unsigned int channel);
該函式用於釋放DMA通道資源。
一般情況下,如果DMA也需要用到中斷,則建議先申請中斷資源,後申請DMA資源;先釋放DMA資源,後釋放中斷資源。
4.2 設定DMA控制器
在申請了DMA資源後,如果要使用DMA(比如要進行DMA讀或者DMA寫時),裝置驅動就需要正確的設定DMA控制器以使得它可以工作。DMA 控制器是一個共享的資源,並且它不支援併發的設定,因而DMA控制器由一個自旋鎖dma_spin_lock來進行保護。裝置驅動可以使用如下兩個函式來使用該自旋鎖:
unsigned long claim_dma_lock( );
它用於獲取DMA自旋鎖,其返回值必須在釋放DMA自旋鎖時被傳遞給釋放DMA自旋鎖的函式。
void release_dma_lock(unsigned long flags);
它用於釋放DMA自旋鎖。
自旋鎖用於保護DMA控制器,因而當一個驅動對DMA控制器進行設定時,必須持有自旋鎖。對DMA控制器進行設定的API包括:
void set_dma_mode(unsigned int channel, char mode);
設定DMA通道channel的傳輸模式。
void set_dma_addr(unsigned int channel, unsigned int addr);
該函式用於設定DMA通道channel的匯流排地址
void set_dma_count(unsigned int channel, unsigned int count);
該函式用於設定DMA通道channel所要傳輸的位元組數。
void enable_dma(unsigned int channel);
該函式用於使能指定的DMA通道
void disable_dma(unsigned int channel);
該函式用於關閉指定的DMA通道
更多的API詳見相關BSP的dma.h