申請和釋放DMA緩衝區

 


1、申請和釋放DMA緩衝區

內存中用於與外設交互數據的一塊區域被稱作DMA緩衝區,在設備不支持scatter/gather(SG,分散/聚集)操作的情況下,DMA 緩衝區必須是物理上連續的。

對於ISA設備而言,其DMA操作只能在16MB以下的內存中進行,因此,在使用kmalloc()和__get_free_pages()及其類似函數申請DMA緩衝區時應使用GFP_DMA標誌,這樣能保證獲得的內存是具備DMA能力的(DMA-capable)。內核中定義了__get_free_pages()針對DMA的“快捷方式”__get_dma_pages(),它在申請標誌中添加了GFP_DMA:

#define __get_dma_pages(gfp_mask, order) \

__get_free_pages((gfp_mask) | GFP_DMA,(order))

如果不想使用log2size即order為參數申請DMA內存,則可以使用另一個函數dma_mem_alloc(),其源代碼如代碼清單11.17。

代碼清單11.17 dma_mem_alloc()函數

1 static unsigned long dma_mem_alloc(int size)

2 {

3 int order = get_order(size);//大小->指數

4 return __get_dma_pages(GFP_KERNEL, order);

5 }

基於DMA的硬件使用總線地址而非物理地址,雖然在PC上,對於ISA和PCI而言,總線地址即為物理地址,但並非每個平台都是如此。因為有時候接口總線被通過橋接電路連接,橋接電路會將I/O地址映射為不同的物理地址。還有一些系統提供了頁面映射機制,它能將任意的頁面映射為連續的外設總線地址。內核提供瞭如下函數用於進行簡單的虛擬地址/總線地址轉換:

unsigned long virt_to_bus(volatile void *address);

void *bus_to_virt(unsigned long address);

在必須使用IOMMU或反彈緩衝區的情況下,上述函數一般不會正常工作。而且,這2個函數並不建議被使用。如圖11.13所示,IOMMU的工作原理與CPU內的MMU非常類似,不過它針對的是外設總線地址和內存地址之間的轉化。由於IOMMU可以使得外設看到“虛擬地址”,因此在使用IOMMU的情況下,在修改映射寄存器後,可以使得SG中分段的緩衝區地址對外設變得連續。

圖11.13 MMU與IOMMU

設備並不一定能在所有的內存地址上執行DMA操作,在這種情況下應該通過下列函數執行DMA地址掩碼:

int dma_set_mask(struct device *dev, u64 mask);

譬如,對於只能在24位地址上執行DMA操作的設備而言,就應該調用dma_set_mask (dev, 0xffffff)。

DMA映射包括2個方面的工作:分配一片DMA緩衝區;為這片緩衝區產生設備可訪問的地址。同時,DMA映射也必須考慮cache一致性問題。內核中提供了以下函數用於分配一個DMA一致性的內存區域:

void * dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp);

上述函數的返回值為申請到的DMA緩衝區的虛擬地址,此外,該函數還通過參數handle返回DMA緩衝區的總線地址。handle的類型為dma_addr_t,代表的是總線地址。

dma_alloc_coherent()申請一片DMA緩衝區,進行地址映射並保證該緩衝區的cache一致性。與dma_alloc_coherent()對應的釋放函數為:

void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t handle);

以下函數用於分配一個寫合併(writecombining)的DMA緩衝區:

void * dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp);

與dma_alloc_writecombine()對應的釋放“函數”dma_free_writecombine()實際上就是dma_free_coherent(),因為它定義為:

#define dma_free_writecombine(dev,size,cpu_addr,handle) \

dma_free_coherent(dev,size,cpu_addr,handle)

此外,Linux內核還提供了PCI 設備申請DMA緩衝區的函數pci_alloc_consistent(),其原型為:

void * pci_alloc_consistent(struct pci_dev *pdev, size_t size, dma_addr_t *dma_addrp);

對應的釋放函數為pci_free_consistent(),其原型為:

void pci_free_consistent(struct pci_dev *pdev, size_t size, void *cpu_addr, dma_addr_t dma_addr);

相對於一致性DMA映射而言,流式DMA映射的接口較為複雜。對於單個已經分配的緩衝區而言,使用dma_map_single()可實現流式DMA映射,該函數原型為:

dma_addr_t dma_map_single(struct device *dev, void *buffer, size_t size,

enum dma_data_direction direction);

如果映射成功,返回的是總線地址,否則,返回NULL。第4個參數為DMA的方向,可能的值包括DMA_TO_DEVICE、DMA_FROM_DEVICE、DMA_BIDIRECTIONAL和DMA_NONE。

dma_map_single()的“反函數”為dma_unmap_single(),原型是:

void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size,

enum dma_data_direction direction);

通常情況下,設備驅動不應該訪問unmap的流式DMA緩衝區,如果一定要這麼做,可先使用如下函數獲得DMA緩衝區的擁有權:

void dma_sync_single_for_cpu(struct device *dev, dma_handle_t bus_addr,

size_t size, enum dma_data_direction direction);

在驅動訪問完DMA緩衝區後,應該將其所有權返還給設備,通過如下函數完成:

void dma_sync_single_for_device(struct device *dev, dma_handle_t bus_addr,

size_t size, enum dma_data_direction direction);

如果設備要求較大的DMA緩衝區,在其支持SG模式的情況下,申請多個不連續的、相對較小的DMA緩衝區通常是防止申請太大的連續物理空間的方法。在Linux內核中,使用如下函數映射SG:

int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents,

enum dma_data_direction direction);

nents是散列表(scatterlist)入口的數量,該函數的返回值是DMA緩衝區的數量,可能小於nents。對於scatterlist中的每個項目,dma_map_sg()為設備產生恰當的總線地址,它會合並物理上臨近的內存區域。

scatterlist結構體的定義如代碼清單11.18所示,它包含了scatterlist對應的page結構體指針、緩衝區在page中的偏移(offset)、緩衝區長度(length)以及總線地址(dma_address)。

代碼清單11.18 scatterlist結構體

1 struct scatterlist

2 {

3 struct page *page;

4 unsigned int offset;

5 dma_addr_t dma_address;

6 unsigned int length;

7 };

執行dma_map_sg()後,通過sg_dma_address()可返回scatterlist對應緩衝區的總線地址,sg_dma_len()可返回scatterlist對應緩衝區的長度,這2個函數的原型為:

dma_addr_t sg_dma_address(struct scatterlist *sg);

unsigned int sg_dma_len(struct scatterlist *sg);

在DMA傳輸結束後,可通過dma_map_sg()的反函數dma_unmap_sg()去除DMA映射:

void dma_unmap_sg(struct device *dev, struct scatterlist *list,

int nents, enum dma_data_direction direction);

SG映射屬於流式DMA映射,與單一緩衝區情況下的流式DMA映射類似,如果設備驅動一定要訪問映射情況下的SG緩衝區,應該先調用如下函數:

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);

Linux系統中可以有一個相對簡單的方法預先分配緩衝區,那就是通過“mem=”參數預留內存。譬如對於內存為64MB的系統,通過給其傳遞mem=62MB命令行參數可以使得頂部的2MB內存被預留出來作為IO內存使用,這2MB內存可以被靜態映射(11.5節),也可以被執行ioremap()。