1. 程式人生 > >裝置IO之二(DMA)

裝置IO之二(DMA)

DMA是硬體的一種能力,具備這種能力的硬體可以直接從主存中讀寫資料,也就是它可以直接使用主存進行I/O而不需要處理器的干預,這可以節省處理器資源並提高整個系統的IO吞吐量,因為IO操作相對來說是較慢的,如果每個IO都要使用處理器資源,則毫無以為會耗費大量CPU時間在單個IO上,最終導致系統IO效能下降。

一、DMA工作方式

對於I/O來說,在輸入端存在兩種工作模式:
  1. 軟體發起讀請求,然後硬體響應該請求(儲存器多用該方式)
  2. 硬體產生輸入事件,然後硬體處理(網絡卡多用該方式)
在輸出端,都是由軟體主動發出寫請求(這是顯然的,因為輸出的資料顯然要由軟體來準備)。
由於DMA是一種I/O的方式,因而這樣是它工作的場景。
DMA的工作方式是:
  1. 在輸入時,軟體準備一塊記憶體區(DMA緩衝區),然後告知硬體,硬體通過DMA的方式將資料寫入這部分割槽域
  2. 在輸出時,軟體準備好一塊包含輸出資料的記憶體區(DMA緩衝區),然後告知硬體,硬體通過DMA的方式獲得這部分資料並輸出出去。
本質上這就是DMA的工作方式。不過在輸入端,不同的I/O工作方式準備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層

不同架構上匯流排的連線工作方式,記憶體分配組織方式,處理快取一致性的方式都有可能有所不同,核心提供了獨立於匯流排和體系架構的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對映

DMA對映將要分配的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次方的邊界。
void dma_pool_destroy(struct dma_pool *pool);
在使用時,需要從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_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:資料流動的方向
在傳輸完畢後,要用dma_unmap_single來刪除對映,其原型如下:
void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size, enum dma_data_direction direction);
它刪除指定的對映,引數含義同建立對映的。
流式DMA的規則:
  • 緩衝區只能用於direction指定的資料傳輸
  • 一旦緩衝被對映,它就屬於裝置,而不屬於處理器。在該對映被刪除前,驅動不能以任何方式訪問該緩衝區。
  • 在DMA活動期間,即裝置還在使用該緩衝區時,不能刪除這個對映。
核心也提供了讓驅動在撤銷對映前就訪問流式DMA緩衝區的內容的方式,做法時,首先呼叫dma_sync_single_for_cpu,呼叫完該函式後,CPU就擁有了該緩衝區,因此也就可以訪問緩衝區了;在訪問完畢後,需要呼叫dma_sync_single_for_device將緩衝區的控制權歸還給裝置。
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:資料流動方向
該函式返回地址為該緩衝區的虛擬地址。從引數可以看出可以指定只對映一個頁的一部分,但是建議不這麼做,因為page是核心管理實體記憶體的單位,核心也基於它來提供一致性控制,只對映一頁可能會導致一致性問題。

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:資料流動方向
該函式的返回值是成功映射了多少個緩衝區。如果在分散/匯聚列表中一些緩衝的實體地址或虛擬地址相鄰的,且IOMMU可以將它們對映成單個記憶體塊,則返回值可能比輸入值nents小。
資料結構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