3項技術:
1,mmap系統調用可以實現將設備記憶體映射到用戶進程的地址空間。
2,使用get_user_pages,可以把用戶空間記憶體映射到核心中。
3,DMA的I/O操作,使得外設具有直接訪問系統記憶體的能力。

--------
地址型態

Linux是一個虛擬記憶體系統,即用戶程序使用的地址與硬件使用的物理地址是不等同的。

虛擬記憶體引入了一個間接層,使得許多操作成為可能:
*有了虛擬記憶體,系統中運行的程序可以分配比物理記憶體更多的記憶體。
*虛擬地址還能讓程序在進程的地址空間內使用更多的技巧,包括將程序的記憶體映射到設備記憶體上。

地址內型列表
----------
用戶虛擬地址
每個進程都有自己的虛擬地址空間。
物理地址
處理器訪問系統記憶體時使用的地址。
總線地址
在外圍總線和記憶體之間使用。MMU可以實現總線和主記憶體之間的重新映射。
當設置DMA操作時,編寫MMU相關的代碼是一個必需的步驟。
核心邏輯地址
核心邏輯地址組成了核心的常規地址空間,該地址映射了部分(或全部)記憶體,並經常被視為物理地址。
在大多數體系架構中,邏輯地址與其相關聯的物理地址的不同,僅僅在於它們之間存在一個固定的偏移量。
kmalloc返回的記憶體就是核心邏輯地址。
核心虛擬地址
核心虛擬地址與邏輯地址相同之處在於,都將核心空間的地址映射到物理地址上。
不同之處在於,核心虛擬地址與物理地址的映射不是線性的和一對一的。
vmalloc返回一個虛擬地址,kmap函數也返回一個虛擬地址。

物理地址和頁
----------
物理地址被分為離散的單元,稱之為頁。
系統內部許多對記憶體的操作都是基於單個頁的。
大多數系統都使用每頁4096個字節,PAGE_SIZE 給出指定體系架構下的頁大小。
觀察記憶體地址,無論是虛擬的還是物理的,它們都被分為頁號和一個頁內的偏移量。
如果每頁4096個字節,那麼最後的12位就是偏移量,剩餘的高位則指定頁號。
將除去偏移量的剩余位移到右端,稱該結果為頁幀數。

高端與低端記憶體
----------
核心(在x86架構中)將4GB的虛擬地址空間分割為用戶空間和核心空間。
一個典型的分割是將3GB分配給用戶空間,1GB分配給核心空間。
占用核心地址空間最大的部分是物理記憶體的虛擬映射,核心無法直接操作沒有映射到核心地址空間的記憶體。
低端記憶體
只有記憶體的低端部分擁有邏輯地址。核心的數據結構必須放置在低端記憶體中。
高端記憶體
除去低端記憶體的剩餘部分沒有邏輯地址。它們處於核心虛擬地址之上。

記憶體映射和頁結構
----------
核心使用邏輯地址來引用物理記憶體中的頁。
為解決在高端記憶體中無法使用邏輯地址的問題,核心中處理記憶體的函數趨向於使用指向page結構的指針 。
page結構用來保存核心需要知道的所有物理記憶體信息,對系統中的每個物理頁,都有一個page結構相對應。

page結構的幾個成員
----------
atomic_t count; 對該頁的訪問計數。
void *virtual; 如果頁面被映射,則指向頁的核心虛擬地址;如果未被映射,則為NULL。
低端記憶體頁總是被映射,而高端記憶體頁通常不被映射。
unsigned long flags; 描述頁狀態的一系列標誌。
PG_locked 表示記憶體中的頁已經被鎖住,PG_reserved 表示禁止記憶體管理系統訪問該頁。

有一些函數和宏用來在page結構指針與虛擬地址之間進行轉換:
----------
struct page *virt_to_page(void *kaddr); 將核心邏輯地址轉換為響應的page結構指針。
struct page *pfn_to_page(int pfn); 針對給定的頁幀號,返回page結構指針。
void *page_address(struct page *page); 如果地址存在的話,則返回頁的核心虛擬地址。
void *kmap(struct page *page); 為系統中的頁返回核心虛擬地址。
對於低端記憶體頁,它只返回頁的邏輯地址;
對於高端記憶體頁,kmap在專用的核心地址空間創建特殊的映射。
void kunmap(struct page *page); 釋放由kmap創建的映射。
void *kmap_atomic(struct page *page,enum km_type type);
void kunmap_atomic(void *addr,enum km_type type); 是kmap的高性能版本。

在任何現代的系統中,處理器必須使用某種機制,將虛擬地址轉化為響應的物理地址,這種機制成為頁表。

虛擬記憶體區
----------
虛擬記憶體區(VMA)用於管理進程地址空間中不同區域的核心數據結構。
可以將其描述為"擁有自身屬性的記憶體對象"。

進程的記憶體映射包含下面這些區域
----------
可執行代碼區域
多個數據區(初始化數據,非初始化數據(BSS),程序堆棧)
與每個活動的記憶體映射對應的區域


vm_area_struct結構
----------
當用戶空間進程調用mmap,將設備記憶體映射到它的地址空間時,系統將創建一個表示該映射的VMA。

支持mmap的驅動程序需要幫助進城完成VMA的初始化。

VMA的主要成員如下:
unsigned long vm_start; VMA所覆蓋的虛擬地址起點。
unsigned long vm_end; VMA所覆蓋的虛擬地址終點。
struct file *vm_file; 指向與該區域相關聯的file結構指針。
unsigned long vm_pgoff; 以頁為單位,文件中該區域的偏移量。
unsigned long vm_flags; 描述該區域的一套標誌。
struct vm_operations_struct *vm_ops; 核心能調用的一套函數,用來對該記憶體區進行操作。
它的存在表示記憶體區域是一個核心對象。
void *vm_private_data; 驅動程序用來保存自身信息的成員。

vm_operations_struct 結構的幾個成員
----------
void (*open)(struct vm_area_struct *vma);
void (*close)(struct vm_area_struct *vma);
struct page *(*nopage)(struct vm_area_struct *vma,
unsigned long address, int *type);
int (*populate)(struct vm_area_struct *vm,
unsigned long address,
unsigned long len,
pgprot_t prot,
unsigned long pgoff,
int nonblock);

記憶體映射處理
----------
映射一個設備意味著將用戶空間的一段記憶體與設備記憶體關聯起來。
無論何時當程序在分配的地址範圍內讀寫時,實際上訪問的就是設備。

在系統中的每個進程都擁有一個struct mm_struct結構,
其中包含了虛擬記憶體區域鏈表、頁表以及其他大量記憶體管理信息,
還包含一個信號燈(mmap_sem)和一個自旋鎖(page_table_lock)。

mmap操作
對於驅動程序來說,記憶體映射可以提供給用戶程序直接訪問設備記憶體的能力。
像串口和其它流設備不能進行mmap抽象。必須以PAGE_SIZE為單位進行映射。

rmpap_pfn_range 和 io_remap_page_range 為一段物理地址建立新的頁表。

int remap_pfn_range(struct vm_area_struct *vm,
unsigned long virt_addr,
unsigned long pfn,
unsigned long size,
pgprot_t prot);
int io_remap_page_range(struct vm_area_struct *vma,
unsigned long virt_addr,
unsigned long phys_addr,
unsigned long size,
pgprot_t prot);

vma 虛擬記憶體區域,在一定範圍內的頁將被映射到該區域內。
virt_addr 重新映射時的起始用戶虛擬地址。
該函數為處於 virt_addr 和 virt_addr + size 之間的虛擬地址建立頁表。
pfn 與物理記憶體對應的頁幀號,虛擬記憶體將要被映射到該物理記憶體上。
size 以字節為單位,被重新映射的區域大小。
prot 新VMA要求的"保護"屬性。

----------
一個簡單的實現
static int simple_remap_mmap(struct file *filp,struct vm_area_struct *vma)
{
if (remap_pfn_range(vma,vma->vm_start,vma->vm_pgoff,vma->vm_end-vma->vm->vm_start,vma->vm_page_prot)){
return -EAGAIN;
}
vma->vm_ops = &simple_remap_vm_ops;
simple_vma_open(vma);
return 0;
}
可見,重新映射記憶體就是調用remap_pfn_range函數創建所需的頁表。

----------
為VMA添加操作
vm_area_struct結構包含了一系列針對VMA的操作。

void simple_vma_open(struct vm_area_struct *vma)
{
printk(KERN_NOTICE "Simple VMA open,virt %lx,phys %lx\n", vma->vm_start,vma->vm_pgoff << PAGE_SHIFT);
}

void simple_vma_close(struct vm_area_struct *vma)
{
printk(KERN_NOTICE "Simple VMA close.\n");
}

static struct vm_operations_struct simple_remap_vm_ops =
{
.open = simple_vma_open,
.close = simple_vma_close,
};


----------
使用nopage映射記憶體
有時驅動程序對mmap的實現必須具有更好的靈活性,在這種情況下,提倡使用VMA的nopage方法實現記憶體映射。

如果要支持mremap系統調用,就必須實現nopage函數。
struct page *(*nopage)(struct vm_area_struct *vma, unsigned long address,int *type);

執行直接I/O訪問
----------
實現直接I/O的關鍵是get_user_pages()函數:

int get_user_pages(struct task_struct *tsk,struct mm_struct *mm,
unsigned long start,int len,int write,int force,
struct page **pages,struct vm_area_struct **vmas);

ssize_t (*aio_read)(),ssize_t (*aio_write)(),ssize_t (*aio_fsync)()

總線地址
----------
使用DMA的設備驅動程序將與連接到總線接口上的硬件通信,硬件使用的是物理地址,而程序代碼使用的是虛擬地址。
實際上,基於DMA的硬件使用總線地址,而非物理地址。

unsigned long virt_to_bus(volatile void *address);
void *bus_to_virt(unsigned long address);

這些函數在核心邏輯地址和總線地址間執行了簡單的轉換。

通用DMA層
----------
DMA操作最終會分配緩沖區,並將總線地址傳遞給設備。
核心提供了一個與總線---體系架構無關的DMA層,它會隱藏大多數問題。
在編寫驅動程序時,為DMA操作使用該層。

處理復雜的硬件
----------
是否給定的設備在當前主機上具備執行DMA操作的能力?
因為有的設備受限於24位尋址。可以用dma_set_mask()函數解決。

DMA映射
----------
一個DMA映射是要分配的DMA緩沖區與為該緩沖區生成的、設備可訪問地址的組合。
DMA映射建立了一個新的結構類型---dma_addr_t來表示總線地址。

dma_addr_t類型的變量對驅動程序是不透明的,
唯一允許的操作是將它們傳遞給DMA支持例程以及設備本身。

根據DMA緩沖區期望保留的時間長短,PCI代碼有兩種DMA映射:
1)一致性映射
2)流式映射(推荐)

一致性DMA映射
----------
void *dma_alloc_coherent(struct device *dev,size_t size, dma_addr_t *dma_handle,int flag);
void dma_free_coherent(struct device *dev,size_t size, void *vaddr,dma_addr_t dma_handle);
該函數處理了緩沖區的分配和映射。
前兩個參數是device結構和所需緩沖區的大小。
函數在兩處返回結果:
1)函數的返回值時緩沖區的核心虛擬地址,可以被驅動程序使用。
2)相關的總線地址則保存在dma_handle中。

DMA池是一個生成小型、一致性DMA映射的機制。
調用dma_alloc_coherent函數獲得的映射,可能其最小大小為單個頁。
如果設備需要的DMA區域比這還小,就要用DMA池了。
struct dma_pool *dma_pool_create(const char *name,struct device *dev,
size_t size,size_t align,
size_t allocation);
void dma_pool_destroy(struct dma_pool *pool);
void *dma_pool_alloc(struct dma_pool *pool,int mem_flags, dma_addr_t *handle);
void dma_pool_free(struct dma_pool *pool,void *vaddr,dma_addr_t addr);

流式DMA映射
----------
流式映射具有比一致性映射更為復雜的接口。
這些映射希望能與已經由驅動程序分配的緩沖區協同工作,
因而不得不處理那些不是它們選擇的地址。
當建立流式映射時,必須告訴核心數據流動的方向。
dma_data_direction:
DMA_TO_DEVICE 數據發送到設備(如write系統調用)
DMA_FROM_DEVICE 數據被發送到CPU
DMA_BIDIRECTIONAL 數據可雙向移動
DMA_NONE 出於調試目的。

當只有一個緩沖區要被傳輸的時候,使用dma_map_single函數映射它:
dma_addr_t dma_map_single(struct device *dev,void *buffer,size_t size, enum dma_data_direction 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映射的幾條原則:
緩沖區只能用於這樣的傳送,即其傳送方向匹配於映射時給定的方向。
一旦緩沖區被映射,它將屬於設備,而不是處理器。
直到緩沖區被撤銷映射前,驅動程序不能以任何方式訪問其中的內容。
在DMA處於活動期間內,不能撤銷對緩沖區映射,否則會嚴重破坏系統的穩定性。

驅動程序需要不經過撤銷映射就訪問流式DMA緩沖區的內容,有如下調用:
void dma_sync_single_for_cpu(struct device *dev,dma_handle_t bus_addr,
size_t size,enum dma_data_direction direction);
將緩沖區所有權交還給設備:
void dma_sync_single_for_device(struct device *dev,dma_handle_t bus_addr,
size_t size,enum dma_data_direction direction);

單頁流式映射
----------
有時候,要為page結構指針指向的緩沖區建立映射,比如
為get_user_pages獲得的用戶空間緩沖區。

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

BB 發表在 痞客邦 PIXNET 留言(0) 人氣()