1. 程式人生 > >LDD-Memory Mapping and DMA

LDD-Memory Mapping and DMA

Memory Management in Linux

本章內容可以分為以下三部分:

  1. mmap系統呼叫的實現,mmap可以將裝置的記憶體直接對映到使用者程序的地址空間內,並不是所有的裝置都支援mmap系統呼叫,但是有些情況下對映裝置的記憶體能夠帶來顯著的效能提升
  2. 通過get_user_pages將使用者空間的記憶體對映到核心,從而能夠訪問使用者空間的記憶體
  3. DMA I/O操作,外設可以直接訪問系統記憶體

當然,上述內容都需要對Linux的記憶體管理有深入的理解,因此我們先從記憶體管理子系統開始。

Address Type

Linux採用虛擬記憶體系統,能夠將程式的記憶體對映到裝置的記憶體。
Linux系統使用了多種地址型別,每種地址型別都有自己的含義,但是核心的程式碼對這些地址型別的區分並不明顯:

  1. User virtual address:使用者虛擬地址,指使用者程式能夠看到的地址空間,64位系統下,使用者程式的地址空間通常小於0x8000 0000 0000(參見Documentation/x86/x86_64/mm.txt)
  2. Physical address:處理器和系統記憶體間使用的地址
  3. Bus address:外設匯流排和記憶體間使用的地址,通常和處理器使用的實體地址相同;一些結構提供了I/O memory management unit(IOMMU),能夠將匯流排對映到主存
  4. Kernel logical address:核心的地址空間,對映主存,通常和實體地址只相差一個偏移量
  5. Kernel virtual address:核心虛擬地址和核心邏輯地址類似,都將實體地址對映為核心地址;但是核心虛擬地址不想邏輯地址是線性對映。所有的邏輯地址都是虛擬地址,反之不成立

Physical Addresses and Pages

實體記憶體分成頁,大小為PAGE_SIZE,通常為4KB。

High and Low Memory

32位系統中,核心將4GB的地址空間的低3GB分給使用者程式,高1GB分給核心程序,而核心也需要將實體記憶體對映到自己的地址空間內才能訪問,而核心地址空間的一部分還需要分出來執行核心程式碼,於是x86 Linux系統最多隻能安裝小於1GB的實體記憶體。
為了在32位系統下支援更多的記憶體,處理器廠商添加了地址擴充套件特性(PAE),於是很多32位的處理器能夠定址超過4GB的實體記憶體;但是能夠直接對映到線性地址的記憶體仍然受到限制——只有記憶體的最低一部分(Low Memory)有線性地址,另一部分沒有(High Memory)。因此在訪問高記憶體前,核心需要顯式的為其建立對映關係——大部分核心資料結構必須放在低記憶體,高記憶體一般用來服務使用者程序。

  • Low Memory:其邏輯地址存在於核心空間
  • High Memory:其邏輯地址不存在,因為超過核心虛擬地址空間可定址的範圍

在i386系統,低記憶體和高記憶體通常以1GB為界限,儘管可以通過核心配置選項修改——這個值僅僅供核心來分割地址空間,和硬體沒有關係。

The Memory Map and Struct Page

由於歷史原因,核心程式碼中的邏輯地址指實體記憶體的頁,但是高記憶體的存在帶來了問題——高記憶體沒有邏輯地址。於是核心程式碼開始用struct page型別的指標來管理記憶體,這個結構體包含核心所需要的實體記憶體資訊,其中一些資訊如下:

  • atomic_t count:引用該頁框的計數,減少至0時將其新增到空閒列表
  • void *virtual:核心的虛擬地址,未對映為NULL。低記憶體的頁框通常被對映,高記憶體的頁框通常沒有對映——並非所有的結構都會有這個域,只有核心的虛擬地址無法輕易計算才會有;page_address巨集可以得到這個值
  • unsigned long flags:指明頁框的狀態,PG_locked指頁框固定在記憶體中,PG_reserved組織記憶體管理系統操作該頁框

核心可能維護所有物理頁框的一個或多個列表,叫做mem_map。在NUMA系統中,這個列表可能有多個,因此最好不要直接引用這個列表。
struct page相關的函式有很多,但是根據函式名可以得知其功能:

struct page *virt_to_page(void *kaddr);
struct page *pfn_to_page(int pfn);
void *page_address(struct page *page);  
/* 如果存在的話,返回核心虛擬地址 */

#include <linux/highmem.h>
void *kmap(struct page *page);
void kunmap(struct page *page);
/* kmap會返回頁框的核心虛擬地址。對於低記憶體,直接返回其邏輯地址;
   對於高記憶體,在核心的地址空間建立對映,並返回。這種對映的數量有
   限,而且無法建立對映時,可能會休眠。*/

#include <linux/highmem.h>
#include <asm/kmap_types.h>
void *kmap_atomic(struct page *page, enum km_type type);
void kunmap_atomic(void *addr, enum km_type type);
/*  kmap_atomic是kmap的高效能形式,每種體系結構都會維持一小部分用
   來進行原子的kmap的slot(槽),呼叫者通過type引數指明所需的槽型別。
   驅動程式只會用到KM_USER0和KM_USER1(使用者空間直接呼叫的程式碼),
   KM_IRQ0和KM_IRQ1(中斷處理函式)。呼叫atomic函式時不能休眠,核心
   不能阻止兩個函式嘗試使用同一個槽。 */

Page Tables

處理器通過頁表將虛擬地址轉化為實體地址,頁表通常為樹形結構,還有一些標誌位。裝置驅動可能需要操作頁表,不過在2.6核心,驅動程式已經不需要直接對頁表進行操作。

Virtual Memory Areas

虛擬記憶體區域(VMA)是管理不同程序地址空間的核心資料結構:包括一片連續的許可權位一樣的虛擬記憶體區域(並由同樣地物件支援,backed up by the same object)。程序的記憶體空間至少包含以下部分:

  • 程式的可執行程式碼區(text)
  • 多個數據區,包括 初始化過的資料區和未初始化的資料區(bss),還有程式棧
  • 活動的記憶體的對映區(One area for each active memory mapping)

程序的記憶體空間可以根據PID獲取:/proc/<pid>/maps,/proc/self總是指向當前的程序。
/proc/<pid>/maps中的內容和sturct vm_area_struct結構體的成員相對應:

  • start end:記憶體區域的起始虛擬地址
  • perm:指明讀、寫、執行許可權的掩碼,最後一個字元p指私有,s指共享
  • offset:記憶體區域在其對映到的檔案的起始位置
  • major minor:儲存被對映的檔案的裝置的主從裝置號;對於裝置,主從裝置號指儲存裝置檔案的磁碟分割槽,而不是裝置自己的主從裝置號
  • inode:被對映的檔案的inode號
  • image:被對映的檔名,通常為可執行映象

使用者程序呼叫mmap函式將裝置的記憶體對映到自己的地址空間時,系統會建立一個VMA來代表這個對映,支援mmap功能的裝置驅動需要完成VMA的初始化。
struct vm_area_struct中的一些和驅動程式相關的成員為:

  • unsigned long vm_start, vm_end
  • struct file *vm_file:和該區域相關的檔案
  • unsigned long vm_pgoff:對齊到頁,對映到這片區域的第一個頁在檔案中的位置
  • unsigned long vm_flags:指明區域型別,VM_IO指MMIO,防止用於程序核心轉儲;VM_RESERVED防止被交換
  • struct vm_operations_struct *vm_ops:核心可能用來對區域進行操作,其存在指明記憶體區域是一個核心物件,就像struct file
  • void *vm_private_data:驅動可用來儲存自己的資訊

struct vm_operation_struct中的以下操作即可滿足程序的需要:

  • void (*open)(struct vm_area_struct *vma):VMA有新的引用時呼叫(fork),VMA第一次建立時通過mmap函式,不會呼叫此函式
  • void (*close)(struct vm_area_struct *vma):記憶體區域銷燬時,核心呼叫此函式,VMA沒有引用計數,因此開啟和關閉必須相對應
  • struct page *(*nopage)(struct vm_area_struct *vma, unsigned long address, int *type): 程序嘗試訪問VMA內的一個頁面,但是不再記憶體中,會呼叫此函式。函式在將請求的頁面讀入記憶體後會將對應的頁面指標返回。如果沒有定義此函式,核心分配一個空頁面
  • int (*populate)(struct vm_area_struct *vm, unsigned long address, unsigned long len, pgprot_t prot, unsigned long pgoff, int nonblock):在訪問前將其擠出記憶體(prefault)

The Process Memory Map

每個程序都有一個struct mm_struct成員,包含程序的所有虛擬記憶體區域,頁表,以及其他記憶體管理所需的資訊,還有一個訊號量(mmap_sem)和一個自選鎖(page_table_lock)。

The mmap Device Operation

記憶體對映是現代Unix系統最有趣的特點之一,對於驅動,實現記憶體對映能夠幫助使用者程式直接訪問裝置記憶體。
對映裝置的記憶體意味著將一段使用者地址空間和裝置記憶體相關聯,當程式訪問分配的地址範圍時,起始是在訪問裝置。
但是對於串列埠和其他的面相資料流的裝置,mmap沒有意義;而且mmap只能對齊到PAGE_SIZE——核心只能在頁表的基礎上管理虛擬地址,對映的區域在實體記憶體上也必須以頁為單位(因為實體地址和虛擬地址只相差一個偏移量?)。
mmapfile_operations結構體的一部分,進行mmap系統呼叫時會呼叫這個函式。呼叫mmap函式時,核心會進行大量的工作,因此係統呼叫的形式和檔案操作的形式差別很大。
系統呼叫mmap定義如下:
mmap (caddr_t addr, size_t len, int prot, int flags, int fd, off_t offset)
檔案操作定義如下:
int (*mmap)(struct file *filp, struct vm_area_struct *vma);
vma包含訪問裝置所用的虛擬地址範圍,驅動程式只需要為vma內的地址範圍正確建立頁表,如果可能的話給vma->vm_ops賦新值,其他的工作都由核心完成。
建立頁表的方式有兩種:通過函式remap_pfn_range一次性建立所有的頁表,或者通過vm_area_structnopage函式每次為一個頁面建立頁表。

Using remap_pfn_range

要對映一段地址範圍內的實體地址,通過remap_pfn_rangeio_remap_page_range函式:

int remap_pfn_range(struct vm_area_struct *vma,
                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_addrvirt_addr+size間的地址範圍建立頁表;pfn指虛擬地址對應起始的物理頁框號,函式會影響pfn<<PAGE_SHIFTpfn<<PAGE_SHIFT+size間的實體地址;size指對映的區域的大小,位元組計;prot指VMA的許可權保護位。
在對映裝置的記憶體時,要注意cache的影響。

Mapping Memory with nopage

雖然remap_pfn_range函式能滿足大部分需求,但是VMA的nopage函式更加靈活。例如可以調整對映區域邊界的系統呼叫mremap,如果VMA減小,核心會在不告訴驅動的情況下悄悄地將不需要的頁面沖刷出去;如果VMA增大,核心在為新的頁面建立頁表時就會呼叫nopage函式。定義如下:
struct page *(*nopage)(struct vm_area_struct *vma, unsigned long address, int *type);
address是發生缺頁的地址,對齊到頁,nopage函式只需計算出address對應的頁面的指標,增加頁的引用計數get_page(struct page *pageptr);
nopage函式還需要將缺頁的型別儲存在type引數內,如果type不是NULL的話。對於驅動程式而言,type的值只能是VM_FAULT_MINOR
如果VMA的nopage函式為NULL,核心缺頁處理函式會將0頁面對映到中斷的虛擬地址。0頁面是一個寫時複製頁,讀的結果為0,用來對映BSS段。如果程序對0頁面進行寫操作,最終會修改自己頁面的拷貝。也就是說,如果一個程序通過mremap擴充套件記憶體,而驅動程式又沒有實現nopage函式,程序會以0x0000 0000 記憶體結束(unsupported reference to 0x0000 0000?),而不是段錯誤。

Remapping Specific I/O Regions

/dev/mem將所有的實體地址(主存和裝置)都對映到了使用者空間,但驅動程式有時只想對映外設的一部分地址範圍,這時可以先計算出需要對映的實體地址對應的頁框號,然後通過函式remap_pfn_range進行對映。

Remapping RAM

remap_pfn_range函式有一個限制,只能對映保留的頁面和大於實體記憶體的實體地址(因為實體記憶體都和核心邏輯地址相對應?)。在Linux中,標記為保留的實體地址不會被記憶體管理系統操作;在PC上,640KB-1MB的記憶體是保留的,因為儲存著核心程式碼。保留的頁面常駐於記憶體當中,才能安全的對映到使用者空間,這樣才能維持系統穩定。
remap_pfn_range不會允許對映傳統的地址,包括通過get_free_page得到的頁面,他會將這些地址對映到0頁面。

Remapping RAM with the nopage method

要將計算機的主存進行對映,可以通過nopage函式實現,但是還要實現裝置的openclose函式,以及正確調整頁面的引用計數。

Remapping Kernel Virtual Address

驅動程式還可以將核心的虛擬地址通過mmap對映到使用者空間,只有對映到核心頁表的虛擬地址才是真正的核心虛擬地址,比如vmalloc函式的返回值。vmalloc函式每次只會申請一個頁面,因為但頁面的申請遠比多頁面的申請更容易成功。通過vmalloc申請到的頁面需要通過vmalloc_to_page轉化為struct page型別的指標。
但是,通過ioremap得到的地址不能通過同樣地方式對映到使用者空間——ioremap返回的地址十分特殊,不能像普通的核心虛擬地址那樣操作,需要用remap_pfn_range將I/O的記憶體區域對映到使用者空間。

Performing Direct I/O

通常情況I/O的操作都會通過核心進行緩衝,既能夠將使用者程式和裝置隔離,也能帶來顯著的效能提升。但是如果要傳輸大量的資料,將資料從使用者空間拷貝到核心空間,然後再由核心傳送到裝置,會影響傳輸的效率。
實現直接I/O的核心函式是get_user_pages,宣告在<linux/mm.h>中:

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);
  • tsk:進行I/O的程序,用來告知核心建立緩衝區時處理缺頁的程序,通常為current
  • mm:包含對映的地址空間的記憶體管理結構,通常為current->mm
  • start,len:start是使用者空間緩衝區的起始地址,頁對齊;len是緩衝區的大小,以頁計
  • write,force:如果write非零,對映的頁可以寫;force非零告知get_usr_pages用傳入的訪問許可權位覆蓋原來的許可權位
  • pages,vmas:輸出引數,成功的話pages包含指向使用者緩衝區的struct page指標,vmas包含相關的VMA

函式的返回值為對映的頁面數,可能返回少於請求的頁面數。而且呼叫get_user_pages函式前需要請求使用者空間的讀寫訊號量。
函式成功返回後,呼叫者會得到一個指向使用者空間的緩衝區的struct page型別的陣列,要直接對緩衝區操作,核心程式碼需要通過kmapkmap_atomic將其轉化為核心的虛擬地址。
直接I/O完成後,必須將緩衝區佔用的使用者頁釋放,在釋放前一定要通知核心對這些記憶體頁所做的更改,否則核心會將這些頁視為乾淨的,在交換裝置上發現匹配的拷貝後,直接將其釋放,而不將這些資料寫回到硬碟中,導致資料丟失。
因此,對於修改過的記憶體頁,通過void SetPageDirty(struct page *page);將其設為髒的;很多程式碼在呼叫這個函式前,還會先判斷記憶體頁是否是保留頁,不會被記憶體換出PageReserved(page)
無論頁面是否被修改,都必須從頁面的快取記憶體中釋放,否則會永遠駐留,在設定髒位後呼叫void page_cache_release(struct page *page);

Asynchronous I/O

非同步I/O使得使用者程式能夠在等待I/O操作的過程中執行其他操作。塊裝置和網路裝置的驅動總是完全非同步的,只有字元裝置才需要顯式的非同步I/O。
實現非同步I/O的驅動需要包含標頭檔案<linux/aio.h>,實現三個檔案操作方法:

ssize_t (*aio_read)(struct kiocb *iocb, char *buffer, 
              size_t count, loff_t offset);
ssize_t (*aio_write)(struct kiocb *iocb, const char *buffer,
              size_t count, loff_t offset);
int (*aio_fsync)(struct kiocb *iocb, int datasync);

aio_fsync函式和檔案系統相關,aio_readaio_write函式的偏移量通過值直接傳入,因為非同步操作不會改變檔案的位置,函式還需要iocb的引數,指明I/O控制塊,I/O control block。
aio_readaio_write函式發起一個讀寫操作,在返回之前可能不會完成;如果操作能夠立即完成,需要返回傳輸的位元組數,或者錯誤碼。
核心可能會建立同步的IOCB,這些是必須同步執行的非同步操作。同步的操作在IOCB中會有標記,可以通過int is_sync_kiocb(struct kiocb *iocb);來查詢,如果返回非零值,驅動程式必須同步執行操作。
如果驅動能夠初始化一個操作,必須記住和操作相關的所有所需資訊,然後返回-EIOCBQUEUED給呼叫者,表明操作尚未完成,最終的狀態需要後續通知。
當“後續”到來時,驅動必須通知核心操作已經完成,通過int aio_complete(struct kiocb *iocb, long res, long res2);完成。res是操作的完成狀態碼,res2是返回使用者空間的狀態碼,通常為0。一旦呼叫aio_complete函式,就不能再修改IOCB和使用者緩衝區。

Direct Memory Access

DMA是允許外設直接和主存進行I/O資料傳輸而不需要處理器的硬體機制,可以顯著提升裝置的吞吐量,因為省去了許多計算的開銷。

Overview of a DMA Data Transfer

DMA傳輸可能有兩種觸發方式:軟體請求資料(例如read函式)或者硬體非同步的將資料傳輸到系統。第一種方式涉及到的操作如下:

  1. 程序呼叫read函式,驅動分配一個DMA緩衝區,命令硬體將資料傳輸到緩衝區中,程序被設為休眠狀態
  2. 硬體將資料寫入DMA緩衝區中,完成時產生一箇中斷
  3. 中斷處理函式獲取到輸入資料,ACK中斷,喚醒休眠的程序

第二種方式在非同步使用DMA時會用到,例如資料採集裝置會將資料傳送到系統,計時沒有程序讀取。這種情況驅動需要維護一個緩衝區,以便後續的讀操作能夠得到所有積累的資料,涉及到的操作如下:

  1. 硬體產生中斷,表明新資料的到來
  2. 中斷處理函式分配一個緩衝區,告知硬體傳輸資料傳輸的地址
  3. 外設將資料寫入緩衝區,在完成時產生另一箇中斷
  4. 中斷處理函式喚醒相關的程序,並分發新資料

非同步的方式經常在網絡卡中用到,這些網絡卡在記憶體中有一個迴圈緩衝區(DMA ring buffer),和核心共享;新到來的資料包放在環中下一個空閒的緩衝區中,然後產生一箇中斷。驅動將資料包傳送給核心,並在環中放置一個新的DMA緩衝區。
通常情況下DMA緩衝區會在初始化時分配給驅動,因此上述的分配緩衝區其實指的是獲取之前分配的緩衝區。

Allocating the DMA Buffer

如果DMA緩衝區大於一個PAGE,必須在實體地址上連續,因為裝置通過ISA或者PCI匯流排傳輸資料,二者通過實體地址進行操作(SBus不同)。
儘管DMA緩衝區可以在系統啟動或者執行時分配,模組只能在執行時分配自己的緩衝區。驅動程式在分配DMA緩衝區時必須注意記憶體的型別,不是所有的記憶體區域都可以用來進行DMA傳輸——在某些系統中,高記憶體不適合用來DMA傳輸,因為超出了一些裝置的定址範圍。比如一些PCI裝置只能工作在32位地址下,ISA裝置只能定址24位。
對於這些裝置,在分配DMA緩衝區時要指明GFP_DMA標誌給函式get_free_pages,分配24位地址以下的記憶體,也可以通過通用的DMA層來分配緩衝區。

雖然get_free_pages可以申請多達數MB的記憶體,但是這種分配可能會失敗,即使請求的記憶體遠少於128KB,因為系統的記憶體支離破碎。
如果要分配大量連續的記憶體給DMA緩衝區,可以在系統啟動時指明mem=SIZE引數,將核心可用的記憶體限定在SIZE之內,然後在驅動中將預留的記憶體分配給DMA緩衝區。
還可以通過指明GFP_NOFAIL標誌,但是會給記憶體管理系統帶來嚴重的負擔,有一定的風險鎖死系統(這個標誌可能會強制記憶體管理系統進行頁面的遷移,以便拼湊出滿足要求的記憶體空間)。

Bus Addresses

採用DMA裝置的驅動需要通過匯流排和硬體打交道,支援DMA的硬體使用匯流排地址。儘管ISA和PCI匯流排地址就是PC上的實體地址,但一些匯流排通過橋接將I/O地址對映到不同的實體地址。
核心提供了將匯流排地址轉化為虛擬地址的函式,但是隻有在很簡單的I/O結構的系統中才能正常工作。

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

正確的地址轉換應該通過通用DMA層實現。

The Generic DMA Layer

DMA操作最終歸結於分配一個緩衝區,然後將匯流排地址傳送給裝置。DMA傳輸可能帶來cache一致性的問題,如果不能正確處理,會損壞記憶體的資料。幸運的是,核心提供了獨立於匯流排和架構的DMA層,幫助處理這些問題。

核心假定裝置能夠對任何32位地址進行DMA傳輸,否則需要呼叫int dma_set_mask(struct device *dev, u64 mask);告知核心。mask指明限制的位數,比如24位值為0xFF FFFF。如果可以,返回非零值。
如果裝置支援32位DMA操作,不需要呼叫dma_set_mask函式。

DMA對映指分配DMA緩衝區,併為其產生一個裝置可訪問的地址。
分配緩衝區時,可能會需要建立跳板緩衝區(bounce buffer)。跳板緩衝區用來幫助在超過外設可定址的地址範圍進行DMA操作——資料通過跳板緩衝區傳送到定址範圍外的地址,會降低資料傳輸的效率。
DMA對映還必須考慮cache一致性的問題。處理器會將經常訪問的記憶體資料放在一個快取記憶體中,以獲得顯著的效能提升。如果記憶體中的資料發生變化,處理器需要將cache中對應的資料無效,以免發生錯誤。因此,如果裝置通過DMA讀取記憶體中的資料,必須現將cache中對應的資料刷出。通用DMA層有大量程式碼保證cache一致性,但是必須遵守一些規則。
DMA對映用dma_addr_t來代表匯流排地址,驅動程式不能使用,只能將其傳送給支援DMA的例程和裝置。如果CPU直接使用dma_addr_t,可能出現意外的問題。
PCI程式碼根據DMA緩衝區存在的時間將DMA對映分為兩種:

  • Coherent DMA mapping:一致性的DMA對映。存在於驅動的整個生命週期中,必須同時可被CPU和外設使用,因此必須存在於cache一致的記憶體中,建立和使用的代價較高
  • Streaming DMA mapping:流式DMA對映。通常用來進行單個操作,一些架構在使用流式對映時會有顯著的效能提升,但是和訪問其的方式有關。核心開發者推薦儘可能的使用流式對映。例如在支援暫存器對映的系統中,每個DMA對映可能使用總線上的一個或多個暫存器,由於一致性DMA對映存在於驅動的整個生命週期,會長時間佔用這些暫存器。還有在某些硬體上,流式對映能夠以一致性對映不可得的方法進行加速。

Setting up coherent DMA mappings

驅動程式通過dma_alloc_coherent建立一致性DMA對映void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, int flag);。函式會分配並對映DMA緩衝區,將緩衝區的核心虛擬地址返回,同時儲存在dma_handle中,flag指明記憶體分配的方式,通常為GFP_KERNEL或者GFP_ATOMIC(執行在原子上下文中)。
緩衝區不再使用時,即模組解除安裝階段,通過void dma_free_coherent(struct device *dev, size_t size, void *vaddr, dma_addr_t dma_handle);將緩衝區返還系統。

DMA pools

DMA池是用來分配小的一致性DMA對映的機制,dma_alloc_coherent分配的對映最小為一個頁,要分配小於一個頁的DMA緩衝區,需要通過DMA池。
DMA池在使用前需要先建立struct dma_pool *dma_pool_create(const char *name, struct device *dev, size_t size, size_t align, size_t allocation);。name是池的名字,size是pool可分配的緩衝區的個數,align是分配的緩衝區的大小,allocation如果非零,指明分配不能超過的邊界。
DMA池使用完畢後需要釋放void dma_pool_destroy(struct dma_pool *pool);,獲取和釋放DMA緩衝區:

void *dma_pool_alloc(struct dma_pool *pool, int mem_flags,
              dma_addr_t *handle);
void dma_pool_free(struct dma_pool *pool, void *addr,
              dma_addr_t addr);

Setting up streaming DMA mappings

流式DMA對映比一致性對映的結構更復雜:流式對映會用到已經由驅動分配的緩衝區,需要處理不是由自己選擇的地址;在某些架構,流式對映會使用多個不連續的分散/聚集(sactter/gather)緩衝區。
建立流式對映時,必須告知核心資料傳輸的方向:

  • DMA_TO_DEVICE,DMA_FROM_DEVICE:見名知義
  • DMA_BIDIRECTIONAL:雙向傳輸
  • DMA_NONE:除錯需求,會引起核心panic

似乎選擇雙向傳輸總是沒錯的,但是在某些架構上會影響效能。
如果只需要傳輸一個單獨的緩衝區,可以採用以下函式:

dma_addr_t dma_map_single(struct device *dev, void *buffer, 
        size_t size, enum dma_data_direction directrion);
void dma_unmap_single(struct device *dev, dma_addr_t dma_addr,
        size_t size, enum dma_data_direction direction);

流式DMA對映的使用要遵守以下規則:

  • 只能按照建立對映時指定的方向傳輸資料
  • 緩衝區建立後,屬於裝置所有,而不是處理器。在緩衝區被解除對映之前,不能修改緩衝區中的內容;只有呼叫unmap函式後才能修改緩衝區的內容。這條規則意味著正在寫入裝置的緩衝區在包含所有要寫入的資料前不能被對映。
  • DMA處於活躍狀態時緩衝區不能解除對映

對於要對映的記憶體區域超出裝置的定址範圍的情況,有些架構通過跳板緩衝區來實現對映。如果一個對映的資料傳輸方向為DMA_TO_DEVICE,同時又需要跳板緩衝區,原緩衝區的資料會首先拷貝到跳板緩衝區,然後再拷貝到裝置中。很明顯,如果原緩衝區的資料在拷貝到跳板緩衝區後發生了改變,並不會影響到最終傳送給裝置的資料。類似的,對於DMA_FROM_DEVICE的對映,跳板緩衝區的資料在呼叫dma_unmap_single函式後才會拷貝到原緩衝區。(書中還說跳板緩衝區是保證傳輸方向正確的原因之一:DMA_BIDIRECTIONAL的跳板緩衝區中的內容會在操作的前後都進行拷貝,導致效能下降——從這個說法中可以推斷出跳板緩衝區的資料拷貝操作只和設定的DMA資料傳輸方向有關)。
驅動程式要訪問DMA緩衝區的資料,要通過函式void dma_sync_single_for_cpu(struct deivce *dev, dma_handle_t bus_addr, size_t size, enmu 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);

Single-page streaming mappings

有時可能建立的緩衝區的大小剛好為一個頁面,可以通過下列函式:

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

offsetsize引數可以用來對映一個頁面內的部分空間,建議不要對映部分頁面,如果對映的記憶體只覆蓋了部分快取記憶體行(cache line),可能帶來cache一致性的問題。

Scatter/gather mappings

分散/集中對映是一種特殊的流式DMA對映。假設現在有多個緩衝區都需要向裝置傳送資料,或者從裝置接收資料,可以分別將這些緩衝區一一對映,然後分別進行傳輸操作。
許多裝置支援scatterlist,包括一系列的陣列指標和長度資訊,將所有的資料通過一次DMA操作傳輸。在某些系統中,物理連續的頁面能夠被裝置視為一個單獨的連續的陣列,當scatterlist的表項大小都為一個頁面時(除了第一項和最後一項),可以將多個操作通過一個DMA完成,提高傳輸效率。
在使用跳板緩衝區時,將scatterlist中的多個操作整合到一個緩衝區中,也能夠帶來效能提升。
scatterlist定義在asm/scatterlist.h中,通常包括:

  • struct page *page;
    scatter/gather操作中使用的緩衝區所在的頁
  • unsigned int length;
  • unsigned int offset;
    緩衝區的長度及其在頁面內的偏移量

要對映scatter/gather DMA操作,驅動需要將每個緩衝區的pagelengthoffset引數設定好,然後呼叫int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);函式,nents是scatterlist中的表項數,返回值是要傳輸的緩衝區的數量,可能小於nents
對於scatterlist中的每一個緩衝區,dma_map_sg都會判斷其正確的匯流排地址,將記憶體上連續的緩衝區組合。如果系統中有I/O記憶體管理單元,dma_map_sg還會設定管理單元的對映暫存器,使得裝置有一定可能性傳輸單個連續的緩衝區。
驅動程式需要將每一個dma_map_sg返回的緩衝區進行傳輸,struct scatterlist表項中包含每個緩衝區的匯流排地址和長度。可以通過下列函式獲得:

dma_addr_t sg_dma_address(struct scatterlist *sg);
unsigned int sg_dma_len(struct scatterlist *sg);

當然,兩個函式返回的結果可能和傳入的結果不一致。
傳輸完成後,通過void dma_unmap_sg(struct device *dev, strcut scatterlist *list, int nents, enum dma_data_direction direction);解除scatter/gather對映。nents必須和呼叫dma_map_sg函式時傳入的引數一致,而不是dma_map_sg函式的返回值。
scatter/gather對映也是流式DMA對映,因此必須遵守同樣地規則;如果要訪問已經對映的scatterlist,必須先進行同步操作:

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

PCI double-address cycle mappings

PCI匯流排支援64位地址,即double-address cycle(DAC)。通用DMA層不支援這種模式,因為只是PCI匯流排才有這種特性。而且DAC實現存在一些問題,還會帶來效能的下降。但是,如果驅動程式需要訪問很大的地址空間來建立緩衝區,可以使用PCI匯流排的DAC特性。
要使用DAC,必須包含linux/pci.h,設定DMA地址的掩碼int pci_dac_set_dma_mask(struct pci_dev *pdev, u64 mask);,成功返回0。DAC對映使用的地址為dma64_addr_t型別,通過函式dma64_addr_t pci_dac_page_to_dma(struct pci_dev *pdev, struct page *page, unsigned long offset, int direction);建立對映。
從函式的引數可以看出,DAC對映接收的引數型別為struct page,意味著要對映的記憶體地址位於高記憶體中——如果不是,就沒有使用DAC對映的必要;而且每次必須對映單獨一個頁面。
DAC對映不需要外部資源,使用後不需要顯式釋放,但是對緩衝區進行操作時需要向流式對映一樣首先獲得緩衝區的所有權:

void pci_dac_dma_sync_single_for_cpu(struct pci_dev *pdev,
            dma64_addr_t dma_addr, size_t len, int direction);
void pci_dac_dma_sync_single_for_device(struct pci_dev *pdev,
            dma64_addr_t dma_addr, size_t len, int direction);

DMA for ISA Devices

ISA匯流排支援兩種DMA傳輸:本地DMA和ISA匯流排DMA。本地DMA使用主機板上的DMA控制器控制ISA總線上的訊號進行傳輸,ISA匯流排DMA完全由外設自己進行傳輸,已經很少使用,從驅動的角度來看和PCI裝置的DMA很想,核心程式碼中有一個例項,1542 SCSI控制器,drivers/scsi/aha1542.c
對於本地DMA,資料傳輸包括以下三部分:

  • The 8237 DMA controller(DMAC)
    控制器包含DMA傳輸的方向,記憶體地址,傳輸的長度等資訊,還有一個計數器儲存正在進行的傳輸的狀態資訊。控制器收到DMA請求訊號時,控制匯流排的訊號線來進行傳輸。
  • The peripheral device
    裝置準備好傳輸資料後,啟用DMA請求訊號;傳輸完成後通常會產生中斷。
  • The device driver
    提供傳輸方向、匯流排地址、傳輸的長度資訊給DMA控制器,通知外設準備好傳輸的資料,在傳輸完成後處理產生的中斷

PC的DMAC通常有4個通道,每個通道都有自己的暫存器。較新的PC包含兩個DMAC,主控制器和處理器直接相連,從控制器連線到主控制器的第0個通道。從整體上看,從控制器的0-3通道和主控制器的5-7通道可用,主控制器的通道4不可用,用來串聯從控制器。每一個DMA傳輸的大小,儲存在控制器中,是一個代表匯流排cycle數的16位數。因此,從控制器的最大傳輸長度為64KB(每個cycle傳輸8bit),主控制器的最大傳輸長度為128KB(每個cycle傳輸16bit資料)。
DMAC是系統資源,因此核心提供了DMA註冊方法來請求和釋放DMA通道,以及一些函式來配置DMA控制器中的通道資訊。

Registering DMA usage

和I/O埠類似,DMA通道註冊函式定義在asm/dma.h,申請和釋放函式如下:

int request_dma(unsigned int channel, const char *name);
void free_dma(unsigned int channel);

channel引數是一個在0和MAX_DMA_CHANNELS之間的值,name用來標識裝置,會出現在/proc/dma檔案中,能夠被使用者程式讀取。請求成功返回0,否則返回錯誤碼。
和I/O埠以及中斷線一樣,建議在open時請求,不要在模組初始化時請求,以免其他程式無法使用。
還建議在請求中斷線以後再請求DMA通道,在釋放中斷線以前釋放DMA通道,以免發生死鎖(DMA通道需要在傳輸完成後通過中斷告知系統)。

Talking to the DMA controller

註冊DMA後線,驅動的主要任務就是配置DMAC,核心向驅動提供了所需的函式。DMAC是共享資源,為了防止多個CPU同時使用一個DMAC,DMAC有自旋鎖,dma_spin_lock。核心提供了獲取鎖的函式unsigned long claim_dma_lock();和釋放鎖的函式void release_dma_lock(unsigned long flags);。獲取函式會阻塞當前處理器的中斷,因此返回值是一組描述之前中斷狀態的標誌位,需要傳遞給釋放函式。
在執行下列操作時,必須持有自旋鎖。但是在進行真正的I/O時,不能持有自旋鎖;驅動程式持有自旋鎖時,不能休眠。
驅動程式必須設定好控制器傳輸的地址,長度和方向。asm/dma.h中聲明瞭下列函式來快速設定DMAC:

  • void set_dma_mode(unsigned int channel, char mode);
    設定DMA傳輸的方向,DMA_MODE_READ時從裝置讀取資料,DMA_MODE_WRITE是向裝置寫入資料,DMA_MODE_CASCADE是從控制器和主控制器相連的方式。
  • void set_dma_addr(unsigned int channel, unsigned int addr);
    設定DMA緩衝區的地址,函式將addr的低24位儲存在控制器中,addr必須是一個匯流排地址。
  • void set_dma_count(unsigned int channel, unsigned int count);
    設定傳輸的位元組數。

核心還提供了函式來設定DMA裝置(這些操作也必須在持有鎖的條件下進行):

  • void disable_dma(unsigned int channel);
    關閉控制器內的一個DMA通道,控制器在正確配置前應該關閉通道,防止不正確的操作。
  • void enable_dma(unsigned int channel);
    通知控制此DMA通道包含有效的資料。
  • int get_dma_residue(unsigned int channel);
    驅動有時候需要判斷通道內的DMA傳輸是否完成,這個函式可以返回需要傳輸的位元組數,如果傳輸已經完成返回0。
  • void clear_dma_ff(unsigned int channel);
    清除DMA觸發器,DMA觸發器用來控制16位暫存器的訪問。暫存器通過兩次連續的8位操作訪問,觸發器清空時,可以訪問低8位;觸發器設定時,可以訪問高8位。觸發器在傳輸8位後自動翻轉,因此在訪問DMA暫存器時,必須進行清除,將其置為確定的狀態。