Linux內核中DMA分析



DMA---直接內存訪問
用來在設備內存與主存RAM之間直接進行數據交換,這個過程無需CPU干預,
對於系統中有大量數據交換的設備而言,如果能夠充分利用DMA特性,可以大大提高系統性能。

1.內核中DMA層

--內核爲設備驅動程序提供了統一的DMA接口,這些接口屏蔽了不同平臺之間的差異。

--一致性映射類型的dma_alloc_coherent/流式映射類型的dma_map_single

不同的平臺(X86/ARM)提供各自的struct dma_map_ops對象來實現對應的DMA映射。

2.物理地址和總線地址

物理地址是指cpu地址信號線上產生的地址。

總線地址可以認爲從設備的角度看到的地址,不同類型的總線具有不同類型的總線地址。

DMA地址--用來在設備和主存之間尋址,雖然是總線地址,但是從內核代碼的角度來看,被稱爲DMA地址,

與之相對應的數據結構是dma_addr_t(-->typedef u32 dma_addr_t;)。

3.DMA映射
3.1 基本原理
 DMA映射主要爲在設備與主存之間建立DMA數據傳輸通道時,在主存中爲該DMA通道分配內存空間的行爲,該內存空間
也稱爲DMA緩衝區。這個任務原本可以很簡單,但是由於現代處理器cache的存在,使得事情變得複雜。

3.2 RAM與cache內容的一致性問題

1.出現問題原因
現代處理器爲了提升系統性能,在CPU與RAM之間加入了高速緩存cache,
所以當在RAM中爲一個DMA通道建立一段緩衝區時,
必須仔細考慮RAM與cache內容的一致性問題。

/*具體的分析*/
如果RAM與Device之間的一次數據交換改變了RAM中DMA緩衝區的內容,
而cache中緩存了DMA緩衝區對應的RAM中一段內存塊。
如果沒有機制保護cache中的內容被新的DMA緩衝區數據更新(或者無效),
那麼cache和他對應的RAM中的一段內存塊在內容上出現了不一致,
此時如果CPU去讀取device傳到RAM的DMA緩衝區中的數據,
它將直接從cache獲得數據,這些數據顯然不是它所期望的,
因爲cache對應的RAM中的數據已經更新了。


2.解決問題--
就cache一致性問題,不同的體系架構有不同的策略,有些是在硬件層面予以保證(x86平臺)
有些沒有硬件支持而需要軟件的參與(ARM品臺)。
--Linux內核中的通用DMA盡力爲設備驅動程序提供統一的接口來處理cache緩存一致性的問題,
而將大量品臺相關的代碼對設備驅動程序隱藏起來。

3.3 DMA映射三種情況

1. 一致性DMA映射

linux內核DMA層爲一致性DMA映射提供的接口函數爲dma_alloc_coherent()
-->
/*
 * Allocate DMA-coherent memory space and return both the kernel remapped
 * virtual and bus address for that space.
 */
void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp)

函數分配的一致性DMA緩衝區的總線地址(也是DMA地址)由參數handle帶回,
函數返回的則是映射到DMA緩衝區的虛擬地址的起始地址


接着調用->__dma_alloc(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp,pgprot_t prot)
{
 struct page *page;
 void *addr;

 *handle = ~0;
 size = PAGE_ALIGN(size);

 page = __dma_alloc_buffer(dev, size, gfp); /*分配大小爲size的一段連續的物理內存頁,並且對應得虛擬地址範圍已經使cache失效*/
 if (!page)
  return NULL;

 if (!arch_is_coherent())/*arch_is_coherent確定體系結構是否通過硬件來保證cache一致性(arm不是,所以函數返回0)*/
  addr = __dma_alloc_remap(page, size, gfp, prot);/*在(?--0xffe0 0000)之間尋找一段虛擬地址段,將其重建新映射到page,
--------------------------------------------------------->由於關閉了cache功能所以保證了DMA操作時不會出現cache一致性問題*/-
 else
  addr = page_address(page);

 if (addr)
  *handle = pfn_to_dma(dev, page_to_pfn(page));

 return addr;
}

一致性所獲得的DMA緩衝區的大小都是頁面的整數倍,如果驅動程序需要更小的DMA一致性的DMA緩衝區,則應該使用內核提供的DMA池(pool)機制

/*釋放一致性DMA緩衝區*/
void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t handle)
/*cpu_addr表示要釋放的DMA緩衝區的起始虛擬地址,參數bus表示DMA緩衝區的總線地址*/

對於一致性DMA映射,在分配DMA緩衝區時各平臺相關代碼已經從根本上解決了cache一致性問題.

但是,一致性映射也會遇到無法克服的困難,主要是指驅動程序中使用的DMA緩衝區並非由驅動程序分配,

而是來自其他模塊(如網絡設備驅動程序中用於數據包傳輸的skb->data所指向的緩衝區),此時需要流式DMA映射。

2. 流式DMA映射

流式DMA映射的特點是DMA傳輸通道使用的緩衝區不是由當前驅動程序自身分配的,
而且往往對每次DMA傳輸都會重新建立一個流式映射的緩衝區,所以使用流式DMA映射時,
設備驅動程序必須小心負責處理可能出現的cache一致性。

linux內核DMA層爲設備驅動提供的建立流式DMA映射的函數---dma_map_single

//dma_map_single(d, a, s, r) dma_map_single_attrs(d, a, s, r, NULL)

static inline dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
  size_t size, enum dma_data_direction dir)
          
/*dev-->設備對象指針,cpu_addr-->cpu的虛擬地址,
size-->流式空間的範圍,dir-->表明當前流式映射中DMA傳輸通道中的數據方向*/
函數返回數據類型-->dma_addr_t即表示DMA操作中的源地址和目的地址。

/*下面分析ARM的平臺*/
/*將cpu_addr表示的段虛擬地址映射到DMA緩衝區中,返回該緩衝區的起始地址*/
2.1
static inline dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
  size_t size, enum dma_data_direction dir)
{
 dma_addr_t addr;

 addr = __dma_map_single(dev, cpu_addr, size, dir);////
 
 return addr;
}

2.2
static inline dma_addr_t __dma_map_single(struct device *dev, void *cpu_addr,
  size_t size, enum dma_data_direction dir)
{
 __dma_single_cpu_to_dev(cpu_addr, size, dir);
 return virt_to_dma(dev, cpu_addr);
}

2.3
void ___dma_page_cpu_to_dev(struct page *page, unsigned long off,
 size_t size, enum dma_data_direction dir)
{
 unsigned long paddr;

 dma_cache_maint_page(page, off, size, dir, dmac_map_area);

 paddr = page_to_phys(page) + off;
 
 if (dir == DMA_FROM_DEVICE) {
  outer_inv_range(paddr, paddr + size); /*保證讀數據使得時候,使得(paddr, paddr + size)對應的cache失效*/
 } else {
  outer_clean_range(paddr, paddr + size); 
 }
}

/*
sync_single_for_cpu方法用於數據從設備傳到主存的情況:
爲了避免cache的介入導致CPU讀到的只是cache中舊的數據,
驅動程序需要在CPU讀取之前調用該函數---->使得cache無效,這樣處理器將直接從主存中獲得數據。

sync_single_for_device方法用於數據從主存傳到設備,
在啓動DMA操作之前,CPU需要將數據放在位於主存的DMA緩衝區中,
爲了防止write buffer的介入,導致數據只是臨時寫到write buffer中,
驅動程序需要在CPU往主存寫數據之後啓動DMA操作之前調用該函數。
*/


3. 分散/聚集DMA映射

分散/聚集DMA映射通過將虛擬地址上分散的DMA緩衝區通過一個struct scatterlist的數組或鏈表組織起來,
然後通過一次的DMA傳輸操作在主存RAM與設備之間傳輸數據。

分散/聚集DMA映射本質上是通過一次DMA操作把內存中分散的數據塊在主存與設備之間進行傳輸,對於其中的每個數據塊
內核都會建立對應的一個流式DMA映射。---》需要設備的支持。


3.4 回彈緩衝區

如果CPU側虛擬地址對應的物理地址不適合設備的DMA操作,那麼需要建立回彈緩衝區,相當於一個 中轉站,把數據往設備傳輸時,
驅動程序需要把CPU給的數據拷貝到回彈緩衝區,然後再啓動DMA操作。


3.5 DMA池

由於DMA映射所建立的緩衝區是單個頁面的整數倍,
如果驅動程序需要更小的一致性映射的DMA緩衝區,
可以使用內核提供的DMA池機制(非常類似於Linux內存管理中的slab機制).

struct dma_pool就是內核用來完成該任務的數據結構
struct dma_pool {  /* the pool */
 struct list_head page_list; /*用來將一致性DMA映射建立的頁面組織成鏈表*/
 spinlock_t lock;   /*自旋鎖*/
 size_t size;    /*該DMA池用來分配一致性DMA映射的緩衝區的大小,也稱爲塊大小*/
 struct device *dev;   /*進行DMA操作的 設備對象指針*/
 size_t allocation; 
 size_t boundary;
 char name[32];    /*dma池的名稱*/
 wait_queue_head_t waitq; /*等待隊列*/
 struct list_head pools;  /*用來將當前DMA池對象加入到dev->dma_pools鏈表中*/
};

/*相關操作*/
1--創建dma_pool,並初始化
 /* dma_pool_create - Creates a pool of consistent memory blocks, for dma.
 * @name: name of pool, for diagnostics
 * @dev: device that will be doing the DMA
 * @size: size of the blocks in this pool.
 * @align: alignment requirement for blocks; must be a power of two
 * @boundary: returned blocks won't cross this power of two boundary
 * Context: !in_interrupt()
 */
struct dma_pool *dma_pool_create(const char *name, struct device *dev,
     size_t size, size_t align, size_t boundary)

2--釋放DMA池中的DMA緩衝塊
/**
 * dma_pool_free - put block back into dma pool
 * @pool: the dma pool holding the block
 * @vaddr: virtual address of block
 * @dma: dma address of block
**/
void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t dma)

3--銷燬dma_pool
/* dma_pool_destroy - destroys a pool of dma memory blocks.
 * @pool: dma pool that will be destroyed
 */
void dma_pool_destroy(struct dma_pool *pool)


//參考文獻:陳雪松-深入Linux設備驅動程序內核機制

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章