Transcript MMAP*DMA

Chapter 15
MMAP與DMA
601430026 許名宏
15.1 Linux的記憶體管理
主要是描述用於控管記憶體的各種資料結構,相
當冗長。有了必要的基礎知識後,我們就可以開
始使用這些結構。

2
15.1.1 位址的分類(1/4)
作業系統的分類上,Linux是一種虛擬記憶系統。
虛擬記憶系統將邏輯世界(軟體)與現實世界(硬體)分隔開
來,最大的好處是軟體可配置的空間超過RAM的實際容
量。
另一項優點是核心可在執行期間改變行程的部分記憶空
間。
Linux系統上不只有兩種位址(虛擬、實體),而且每種位址
都有其特殊用途。但核心原始程式裡沒有明確定義何種位
址適用何種情況,所以必須相當謹慎小心。




3
15.1.1 位址的分類(2/4)
4
15.1.1 位址的分類(3/4)

使用者虛擬位址(User Virtual Address)

簡稱為虛擬位址,也就是 user-process 所見到的一般位址。虛擬位址寬度隨CPU架構而
定。

實體位址(Physical Address)

用於CPU與系統記憶體之間的位址。寬度依CPU而定,但不一定與CPU暫存器的寬度相
符。

匯流排位址(Bus Address)

用於周邊匯流排與記憶體的位址,具有高度的平台相依性。

核心邏輯位址(Kernel Logical Address)
這類位址構成核心的正常位址空間,他們對應到所有主記憶體,而且通常被當作實體位
址來使用。邏輯位址與實體位址只差距一段固定偏移量,通常存放在unsigned long或void
*型別變數上。kmalloc()所傳回的記憶體,就是以邏輯位址來定位。


核心虛擬位址(Kernel Virtual Address)
核心虛擬位址跟邏輯位址不同之處,在於核心虛擬位址與實體位址不一定有直接對應關
係,虛擬位址通常存放在指標變數中。vmalloc()配置而來得記憶體位址是以虛擬位址來表
5 示。
15.1.1 位址的分類(4/4)

<asm/page.h>定義了兩個可換算位址的巨集。
如果你有一個邏輯位址, _ _pa()巨集可換算出其對應的實
體位址。
_ _va()可將實體位址換算回邏輯位址,但僅限於低記憶體
的實體位址才有效,因為高記憶體沒有邏輯位址。


6
15.1.2 高低記憶體
核心邏輯位址與核心虛擬位址之間的差異,在配備超大量
記憶體的32-bits系統上才凸顯出來。
 低記憶體(Low memory)
 存在於kernel-space裡,具有邏輯位址的記憶體。
 高記憶體(High memory)
沒有邏輯位址的記憶體,因為超過了核心的虛擬位址空間。
 高低記憶體之間的分界線
核心在開機期間依據BIOS提供的資訊來決定的。在i386系
統,分界通常位於1GB的位置。這是核心自己設下的限制,
因為核心必須將32-bit位址空間劃分成kernel-space與userspace兩大部份。

7
15.1.3 記憶體對應表與struct page(1/2)
由於高記憶體沒有邏輯位址,核心裡負責管理記憶體的函式,
紛紛改用struct page來代替邏輯位址。
page結構含有關於實體記憶體的一切資訊。系統上的每一個實
體記憶頁,都有一個專屬的struct page,以下是page結構裡幾個
比較重要的欄位。
atomic_t count;
此記憶頁的用量計次。當count值降為0時,記憶頁會被釋放回自
由串列。
void *virtual;
本記憶頁對應的核心虛擬位址;若無對應的虛擬位址則指向
NULL 。
unsigned long flags;
一組描述記憶頁狀態的位元旗標。如PG_locked(代表記憶頁是否
已被鎖定)、PG_reserved(是否受記憶體管理系統的管轄) 。








8
15.1.3 記憶體對應表與struct page(2/2)

為了方便在struct page指標與虛擬位址之間轉換,Linux定義了一組方便的函式與巨集:

struct page *virt_to_page(void *kaddr) ;

將核心邏輯位址轉換成對應的struct page指標。

void *page_address(struct page *page);

傳回指定的page的核心虛擬位址。位於高記憶體的記憶頁,除非已事先映射到虛擬
位址空間,否則沒有虛擬位址。

#include <linux/highmem.h>

void *kmap(struct page *page) ;

kmap()可傳回系統上任何記憶頁的核心虛擬位址。

Page在低記憶體→則傳回該記憶頁的邏輯位址。

Page在高記憶體→kmap()主動將它映射到特殊的虛擬空間。

如果分頁表剛好沒有空位,kmap()有可能會休眠。

void kunmap(struct page *page) ;

將kmap()所建立的特殊對應解除。
9
15.1.4 虛擬記憶體分區(1/6)



核心需要一個較高層級的機制,才能處理行程所見到
的記憶體佈局。在Linux,這機制稱為虛擬記憶體分區
(virtual memory areas),通常簡稱為分區或VMA。
用來管理使用者行程的虛擬位址空間的各個區域。
一個行程的虛擬位址空間,至少含有下列幾個VMA:
• 一個存放程式碼(executable binary)的區域,通
常稱為text。
• 多個資料分區,包括有初值的資料、沒初值的資
料,以及程式堆疊。
• 每一個有效的記憶體對映(memory mapping),各有
一個分區。
10
15.1.4 虛擬記憶體分區(2/6)

例:以下是init行程VMA的分布情形。cat /proc/1/maps
08048000-0804e000 r-xp 00000000 03:01 64652
/sbin/init text
0804e000-0804f000 rw-p 00006000 03:01 64652
/sbin/init data
0804f000-08053000 rwxp 00000000 00:00 0
zero-mapped BSS
40000000-40015000 r-xp 00000000 03:01 96278
/lib/ld-2.3.2.so text
40015000-40016000 rw-p 00014000 03:01 96278
/lib/ld-2.3.2.so data
40016000-40017000 rw-p 00000000 00:00 0
BSS for ld.so
42000000-4212e000 r-xp 00000000 03:01 80290 /lib/tls/libc-2.3.2.so text
4212e000-42131000 rw-p 0012e000 03:01 80290
/lib/tls/libc-2.3.2.so data
42131000-42133000 rw-p 00000000 00:00 0
BSS for libc
bffff000-c0000000 rwxp 00000000 00:00 0
Stack segment
ffffe000-fffff000 ---p 00000000 00:00 0
vsyscall page
各欄位的格式如下:
start-end|perm|offset|major:minor|inode|imagename
11
15.1.4 虛擬記憶體分區(3/6)

上面每一欄除了imagename之外,都分別對應到struct
vm_area_struct裡的欄位,這些欄位意義如下:
• start、end
VMA前後邊界的虛擬位址
• perm
VMA的存取位元遮罩(r、w、x、p/s)
• offset
VMA所映射的檔案內容之起點
• major:minor 持有映射檔的裝置的主、次編號
• inode
被映射檔案的inode編號
• imagename
被映射檔案(通常是可執行檔)的名稱

mmap() 是Unix 系統的一個重要的系統呼叫,其作用是將
裝置記憶體映射到 user-space 行程的虛擬記憶空間。
12
15.1.4 虛擬記憶體分區(4/6)



Linux核心是以 struct vm_area_struct 來表示VMA。
驅動程式不能任意建立新的VMA,否則會破壞整個組織,
在核心內部,VMA是以串列與樹狀結構組織在一起(為了
提升查詢VMA的效率) 。
vm_area_struct的主要欄位:
• unsigned long vm_start;
• unsigned long vm_end;
 VMA所涵蓋的虛擬位址範圍。
• struct file *vm_file;
 如果VMA的映射對象是檔案,則vm_file指向該檔案的
struct file結構。
13
15.1.4 虛擬記憶體分區(5/6)
•

•

•

•

unsigned long vm_pgoff;
此區域在檔案裡的相對位置(以page為單位)。當一個檔案或裝置
被映射到記憶體, vm_pgoff就是映射到此區域的第一頁的檔案
位置。
unsigned long vm_flags;
描述VMA性質的一組旗標。對裝置驅動程式而言,最可能用到
的旗標是VM_IO 與VM_RESERVED。其中VM_IO表示該VMA
是映射到硬體裝置上的I/O 位址區;VM_RESERVED要求記憶體
管理系統不要將該VMA置換到磁碟上。
struct vm_operations_struct *vm_ops;
一組可供核心用來操作此VMA的函式。這函式指標的存在,表
示核心將VMA當成一種物件來看待。
void *vm_private_data;
供驅動程式用於儲存私有資訊的欄位。
14
15.1.4 虛擬記憶體分區(6/6)
•
•
•
•
•
•
void (*open)(struct vm_area_struct *vma);
核心會呼叫open作業方法,讓實作VMA的子系統有機會初
始VMA 、調整用量計次...等等。
void (*close)(struct vm_area_struct *vma);
當VMA被摧毀,核心會呼叫它的close作業方法。
struct page *(*nopage)(struct vm_area_struct *vma, unsigned
long address, int type);
行程試圖讀取某個VMA記憶頁,但該記憶頁目前不在主記
憶體裡,則會執行VMA的nopage作業方法。nopage作業方
法通常會從磁碟上的交換區讀回記憶頁的內容,然後傳回
一個指向實體記憶頁的struct page的指標。若果VMA沒定義
它自己的nopage作業方法,則核心會配置一個空的記憶頁。
15
remap_pfn_range() 與 nopage 作法圖示 ︰
16
15.2 mmap作業方法(1/2)


就驅動程式的觀點而言,記憶體映射可用來提供直接存取裝置記憶體
的能力給user-space應用程式。
mmap()最經典的應用,就是 X Window System server 利用它來存取顯
示卡的視訊記憶體。以下是 X server 行程的記憶體對應表:
cat /proc/731/maps
000a0000-000c0000 rwxs 000a0000 03:01 282652 /dev/mem
000f0000-00100000 r-xs 000f0000 03:01 282652 /dev/mem
00400000-005c0000 r-xp 00000000 03:01 1366927 /usr/X11R6/bin/Xorg
006bf000-006f7000 rw-p 001bf000 03:01 1366927 /usr/X11R6/bin/Xorg
2a95828000-2a958a8000 rw-s fcc00000 03:01 282652 /dev/mem
2a958a8000-2a9d8a8000 rw-s e8000000 03:01 282652 /dev/mem


a0000:VGA卡的視訊記憶體的標準位置
e8000000:位於系統記憶體的頂端,直接對應到顯卡上的視訊記憶體
17
15.2 mmap作業方法(2/2)


由於X server時常需要傳輸大量資料到視訊記憶體,如果使用傳統的
lseek()、write(),勢必引發相當頻繁的context switch,而傳輸效率當然
就很差勁;然而如果將視訊記憶體直接映射到user-space,則應用程式
可以直接填寫視訊記憶體,所以傳輸效率得以大幅提升。
mmap作業方法屬於file_operations結構的一部份,由mmap()系統呼叫
觸發。
•

mmap (caddr_t addr, size_t length, int prot, int flags, int fd, off_t
offset); /此為mmap()系統呼叫的宣告形式/
• int (*mmap)(struct file *filp, struct vm_area_struct *vma);
/mmap作業方法宣告方式/
有兩種方法可以製作分頁表:全部交給remap_pfn_range()函式一次搞
定。或者透過VMA的nopage作業方法,在VMA被存取時,才一次處理
一頁。
18
15.2.1 使用remap_pfn_range()
要將一段虛擬位址映射到一段實體位址,必須另外產生新
的分頁表,這個任務就交給它來完成。
 int remap_pfn_range(struct vm_area_struct *vma,
unsigned long virt_addr, unsigned long pfn, unsigned long size,
pgprot_t prot);
 映射成功,則傳回0,否則傳回一個負值錯誤碼。
• vma
記憶頁所要映射到的VMA。
• virt_addr
實體位址所要映射到的虛擬位址範圍之起
點。
• pfn
虛擬位址所要映射到的實體位址的頁框編
號。
• size
映射區的規模(以byte為計算單位) 。
• prot
新VMA的保護方式。驅動程式能使用
vma->vm_page_port所提供的值。
19

15.2.2 mmap實例
簡單、線性的映射作法,讓應用程式可透過user-space的某段虛擬位
址來存取裝置記憶體










static int simple_remap_mmap(struct file *filp, struct vm_area_struct *vma)
{
if (remap_pfn_range(vma, vma->vm_start, vm->vm_pgoff,
vma->vm_end - vma->vm_start,
vma->vm_page_prot))
return -EAGAIN;
vma->vm_ops = &simple_remap_vm_ops;
simple_vma_open(vma);
return 0;
}
20
15.2.3 增添新的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,

};
21
15.2.4 使用nopage映射記憶體(1/3)




雖然remap_page_range()已足以應付許多驅動程式的
mmap,但偶爾會需要多一點彈性。對於這類情況,VMA
的nopage作業方法或許是可以考慮的選擇。
適合使用nopage作業方法來映射位址空間的情況,是當驅
動程式只需在VMA發生變化才會有處理動作時。應用程式
可透過mremap()系統呼叫來改變VMA的邊界或大小。
發生mremap()系統呼叫時,核心不一定會通知驅動程式;
假如改變的結果是VMA範圍縮減,核心可以默默清理掉多
出來的記憶頁,而不必通知驅動程式;但如果是範圍擴
張,核心才會呼叫該VMA的nopage作業方法來配置新的記
憶頁。
之所以不讓驅動程式收到映射區擴張通知,是因為記憶體
被實際應用之前,沒有處理的必要,而當真的有必要時,
核心可觸發nopage來處理。
22
15.2.4 使用nopage映射記憶體(2/3)
struct
page *simple_vma_nopage(struct vm_area_struct *vma, unsigned long address, int *type)
{

struct page *pageptr;

unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;

unsigned long physaddr = address - vma->vm_start + offset;

unsigned long pageframe = physaddr >> PAGE_SHIFT;

if (!pfn_valid(pageframe))

return NOPAGE_SIGBUS;

pageptr = pfn_to_page(pageframe);

get_page(pageptr); //遞增用量計次

if (type)


*type = VM_FAULT_MINOR;
return pageptr;
}
23
15.2.4 使用nopage映射記憶體(3/3)
static int simple_nopage_mmap(struct file *filp, struct vm_area_struct *vma)
{
unsigned long offset = vma ->vm_pgoff << PAGE_SHIFT;
if (offset >= _ _ pa(high_memory) || (filp->f_flags & O_SYNC))
vma->vm_flags |= VM_IO;
vma->vm_flags |= VM_RESERVED;
vma->vm_ops = &simple_nopage_vm_ops;
simple_vma_open(vma);
return 0;
}
24
15.2.5 重新映射特定I/O區(1/2)










如果只想將整段位址中的一小段映射到user-space,驅動程
式必須自己處理偏移位置(offset)。
例如,若要將實體位置simple_region_start開始的
simple_region_size個位元組映射到user-space:
unsigned long off = vma->vm_pgoff << PAGE_SHIFT;
unsigned long physical = simple_region_start + off;
unsigned long vsize = vma->vm_end - vma->vm_start;
unsigned long psize = simple_region_size - off;
if (vsize > psize)
return -EINVAL; //跨越範圍太大
remap_pfn_range(vma, vma->vm_start, physical, vsize,
vma->vm_page_prot);
25
15.2.5 重新映射特定I/O區( 2/2)

要避免映射範圍擴張,最簡單的辦法是實作一個
簡單的nopage作業方法,讓它回覆一個SIGBUS信
號給發生失誤的行程。例如:

struct page *simple_nopage(struct vm_area_struct
*vma, unsigned long address, int *type)
{
return NOPAGE_SIGBUS; /* 傳回一個 SIGBUS */
}



26
15.2.6 重新映射 RAM



如果要適度容許映射擴張,比較完善的做法,是檢查引發
分頁失誤的位址,是否在有效的實體範圍內,如果是,才
容許映射。
remap_page_range() :只有保留頁,以及在實體記憶體
(RAM)頂端之上的實體位址,它才有作用。保留頁被鎖在
記憶體裡(不會被換出到磁碟上) ,所以可以安全地映射到
user-space;這項限制式系統穩定度的基本要求。
由於remap_page_range()沒有處理RAM的能力,這表示類
似scullp那樣的裝置將難以作出自己的mmap ,因為其裝置
記憶體是一般的RAM而非I/O memory。幸好,還是可以使
用nopage作業方法。
27
15.2.6.1 使用nopage重新映射RAM(1/3)


先看看有哪些設計抉擇會影響scullp的mmap:
•
在裝置被映射之後,scullp就不釋放其裝置記憶體,而且不能
像scull或類似裝置那樣,在被開啟成write模式時,裝置長度
就被截為0;要避免釋放已映射的裝置,驅動程式必須自己計
算有效的映射次數,scullp_device結構中的vmas欄位,可當此
用途來使用。
•
只有在scullp的order參數值為0 ,才容許映射記憶體。因為
get_free_pages()和free_pages()只修改串列中第一個空頁計次
值。
要遵循上述規則來映射RAM的程式,需要實作出open、close和
nopage,而且還必須存取記憶對應表,調整記憶頁的用量計
次。
28
15.2.6.1 使用nopage重新映射RAM(2/3)













int scullp_mmap(struct file *filp, struct vm_area_struct *vma)
{
struct inode *inode = filp ->f_dentry->d_inode;
/* 如果order不等於0,則拒絕映射 */
if (scullp_devices[iminor(inode)].order)
return -ENODEV;
/* 這裡不作任何事。交給“nopage”搞定 */
vma->vm_ops = &scullp_vm_ops;
vma->vm_flags |= VM_RESERVED;
vma->vm_private_data = filp->private_data;
scullp_vma_open(vma);
return 0;
}
29
15.2.6.1 使用nopage重新映射RAM(3/3)


void scullp_vma_open(struct vm_area_struct *vma)
{

struct scullp_dev *dev = vma->vm_private_data;

dev->vmas++;

}


void scullp_vma_close(struct vm_area_struct *vma)
{

struct scullp_dev *dev = vma->vm_private_data;


dev->vmas--;
}
30
15.3 直接I/O


大部份的I/O作業,透過kernel-space的緩衝區做緩
衝,某種程度上隔離了user-space與實際裝置。好
處:讓程式比較好寫,提升效能。
但在傳輸大量資料時,讓user-space直接與裝置I/O
做溝通反而會比較好。
31
15.3.1直接I/O的關鍵函式

在2.6版核心實作直接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);

struct task_struct *tsk :此指標指向要執行I/O的task_struct。

struct mm_struct *mm:此指標所指的mm_struct結構,是行程的虛擬位址空間的所有VMA。

unsigned long start:user-space緩衝區的起始位址。

int len:該緩衝區的長度,以頁為計算單位。

int write:若不是零,則要映射的記憶頁是供write存取之用。

int force:要求 get_user_pages() 不理會指定記憶頁的保護旗標,對驅動程式應設為零。

struct page **pages:page應該指向一個描述user-space緩衝區的struct page串列。

struct vm_area_struct **vmas :vmas應含有相關的VMA的指標。

get_user_pages() 的回傳值:是實際映射的記憶頁數量,此數量有可能少於要求的數量,但
至少會大於零。
32
15.3.2釋放記憶頁之前要做的事情




完成直接I/O作業之後,必須釋放user-space記憶頁。
1.檢查記憶頁是否為記憶體對應表中的保留部份,因為保
留頁絕對不會被置換出去,可使用PageReserved()來檢查特
定記憶頁。
• 非絕對必要:user-space記憶體都不是保留頁。
2.若改變了記憶頁內容,則使用SetPageDirty(struct page
*page);將記憶頁做標記,否則核心會直接釋放該記憶
頁,而不寫回儲存裝置。
3.不管是否有改變記憶頁內容,都應將page從page cache釋
放,這使用void page_cache_release(struct page *page);
33
15.3.3非同步I/O


非同步 I/O 讓 user-space 可發動 I/O 作業要求,而不必
等待 I/O 作業完成,也就是說在 I/O 進行過程中,應
用程式仍可以做其它事。對於複雜的高效率應用程
式,非同步 I/O 可讓它們同時進行多項工作。
核心沒強制要求驅動程式必須提供非同步 I/O 的能
力,也只有非常少數的驅動程式設計者需要考慮這項
能力,因為這通常無助於裝置的傳輸效率。
34
15.4 直接記憶體存取(DMA)



DMA是一種硬體機制,讓周邊元件可以直接與主
記憶體交換I/O資料,而不必經過系統處理器。
由於DMA是“硬體”機制,其設定程序完全隨系
統架構而定。
DMA機制可大幅提昇周邊裝置的資料吞吐量,同
時減輕CPU的運算負擔。
35
15.4.1 DMA資料傳輸的流程(1/3)
有兩種機會可觸發DMA資料傳輸:軟體主動要求,
或周邊硬體主動將資料推入系統。
 第一種情況(軟體觸發)所涉及的步驟:
1.當行程發出一次read(),驅動程式的read作業方
法就配置一塊DMA緩衝區,並指示周邊硬體開始
傳輸資料。行程會進入休眠狀態。
2.周邊硬體將資料寫到DMA緩衝區,在完成傳輸之
後,對CPU發出一次中斷訊號。
3.ISR(中斷服務程式)收下輸入資料、回應中斷、
然後喚醒行程,讓行程讀走資料。

36
15.4.1 DMA資料傳輸的流程(2/3)

第二種情況,發生在DMA被當成非同步傳輸機制使用
時的步驟:
1.周邊硬體觸發一次中斷,讓系統處理器知道新資料
已經到達。
2.ISR配置一個緩衝區,並將該緩衝區的位置告訴周邊
硬體,使其知道資料應該傳送到何處。
3.周邊硬體將資料寫入指定的緩衝區,在完成傳輸之
後,觸發另一次中斷。
4.ISR將新資料存放在適當位置,喚醒任何相關行程,
並處理一些例行工作。
37
15.4.1 DMA資料傳輸的流程(3/3)




網路卡與CPU之間通常是透過主記憶體上的一塊環
型緩衝區(稱為DMA ring buffer)互相交換資料。
當網路卡從外界收到一個封包,就將它放入環型
緩衝區裡的下一個空位,然後發出中斷通知。
驅動程式將網路封包傳給核心裡的其它部門,並
將一個新的DMA空位放回環型緩衝區。
大多數驅動程式在初始期就預先配置好所需的緩
衝區,並全程使用同一塊緩衝區,直到關閉時才
予以釋放。
38
15.4.2 配置DMA緩衝區







並非所有記憶體都可以用來當成DMA緩衝區,因此要配置一塊
適合DMA的緩衝區,不是隨意配置一塊普通記憶體就了事了。
DMA緩衝區必須是實體記憶體上的連續頁,因為周邊裝置使用
ISA或PCI匯流排來傳輸資料,而兩種匯流排都使用實體位址。
自助配置法:
需靠核心的mem=開機期參數配合,如原有256M的記憶體,而你
需要1MB記憶體來當DMA緩衝區使用,則可使用mem=255M參
數要求核心將最高的1MB保留給你。保留記憶體的模組程式為:
dmabuf=ioremap(0x1f00000 /* 255M*/ ,0x100000 /* 1M */)。
積極配置法:
使用 GFP_NOFAIL 積極配置足夠的DMA緩衝空間;不過這應該
被視為最後手段,除非其它所有辦法都無效,因為積極配置會導
致沉重的系統負載,甚至可能鎖死系統。
39
15.4.3 匯流排位址

具有DMA能力的周邊硬體,其實是使用匯流排位址,而非實體
位址。在x86 PC上,ISA與PCI匯流排的位址確實等於x86 CPU
的實體位址,但並非所有平台都這樣,有些平台的介面匯流排
是透過橋接電路連接在一起,它們的I/O位址被映射到不同的實
體位址。

在最底層,Linux核心提供一套通用的解決方案,以下兩個函式:
unsigned long virt_to_bus(volatile void * address);
void *bus_to_virt(unsigned long address);
這兩個函式只是在虛擬位址與匯流排位址之間做轉換。通常不
應該使用它們,因為它們僅適用於 I/O 架構非常簡單的平台,
若是遇到有 I/O MMU 的平台就無能為力了。



40
15.4.4 DMA抽象層




從驅動程式的觀點來看,DMA終究不過是配置一個緩
衝區,並傳遞匯流排位置給裝置。
對於快取而言,如何保持快取的一致性,各家系統各
有自己獨到的邏輯,若你的驅動程式沒能正確處理這
方面的問題,可能會造成系統記憶錯亂。
並非所有系統的每一塊記憶體都可以配合DMA作業。
例如: x86平台的高記憶體就不能當成DMA緩衝區。
核心提供了一個DMA抽象層,可跨越各種不同平台與
匯流排之間的差異。
41
15.4.4.1 排除不支援的硬體


嘗試進行DMA作業之前,第一個要回答的問題是當時的平台是
否支援目標裝置所需的DMA能力。
原則上核心假設裝置可在任何32-bit位址執行DMA作業,如果裝
置不合這項假設,可以下列方式通知核心:
int dma_set_mask (struct device *dev, u64 mask);
其中mask代表目標裝置的定址空間的位元數。
假如驅動的是只有24-bit定址能力的ISA裝置:
if (dma_set_mask (dev, 0xffffff))
card->use_dma=1;
else {
card->use_dma=0;
printk (KERN_WARN, “mydev: DMA not supported\n”);
}
42
15.4.4.2 DMA 對應


配置一個DMA緩衝區,並為該緩衝區產生一個可供裝置存取的位址,這
兩個動作的組合就稱為 DMA 對應。
在PCI匯流排上的DMA對應被分成兩種類型,主要差別在於
DMA緩衝區的存活時間長短。這兩種對應模式如下:
 常態性DMA對應(coherent DMA mapping)
 若DMA緩衝區的生命期與驅動程式一樣長,就稱為常態性DMA對
應。DMA緩衝區必須能夠同時被CPU與周邊使用,甚至應該被排除在
快取機制之外,以免一方看不見另一方的更新。
 臨時性DMA對應(streaming DMA mapping)
 為了單次DMA作業而臨時設置的DMA對應;這比較有彈性但必須遵
守一些限制;建議盡可能使用臨時性對應基於兩項理由。首先,在配
置對應暫存器的系統上,每組DMA對應都需要用掉一或多個暫存器,
常態性對應會長期佔用這些暫存器;其次,有不少硬體平台特地針對
臨時性對應做了最佳化,這些最佳化措施不能運行在常態性對應。
43
15.4.4.3 設定常態性DMA對應



驅動程式可呼叫dma_alloc_coherent() 來設定一組常態性的DMA對應,此
函式包辦了緩衝區的配置與對應工作。
void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t
*dma_handle, int flag);
dma_alloc_coherent() 內部使用 get_free_pages() 來配置指定的記憶空間,
以便得到可以配置 DMA 作業的緩衝區;flag通常是設定為
GFP_KERNEL (可能會休眠)或 GFP_ATOMIC (絕不會休眠)。
當不再需要緩衝區時(通常是在卸載模組時,就應該盡快使用
dma_free_coherent() 將緩衝區還給系統,此函式需同時提供CPU位址與匯
流排位址。
void dma_free_coherent(struct device *dev, size_t size, void *vaddr,
dma_addr_t dma_handle);
44
15.4.4.4 設定臨時性DMA對應(1/3)

臨時性 DMA 對應的軟體介面稍微複雜些,因為驅動程式自己得事先配置好緩衝區,並處
理它們所沒選擇的位址。在某些平台上,臨時性對應甚至接受多個不連續的記憶頁。

設定臨性對應時,必須讓核心知道資料的移動方向,方向是以 enum dma_data_direction
型別的符號來描述,如下:

DMA_TO_DEVICE
如果是要將資料傳送到裝置上(為了回應 write() 系統呼叫)。

DMA_FROM_DEVICE
如果是要將資料傳送到裝置上(為了回應 read() 系統呼叫)。

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);

傳回值是可以傳給目標裝置的匯流排位址。如果映射失敗,則會傳回 NULL。

完成傳輸之後,應該立刻使用 pci_unmap_single() 來解除對應。

void pci_unmap_single(struct pci_dev *pdev, dma_addr_t bus_addr, size_t size, int direction);

這裡的 size 和 direction 引數,必須符合當初映射緩衝區時所用的引數值。
45
供除錯用途。
15.4.4.4 設定臨時性DMA對應(2/3)


臨時性 DMA 對應必須遵守三點重要法則:
 緩衝區的使用,必須符合映射時所設定的傳輸方向。
 在緩衝區映射到匯流排位址之後,就屬於裝置,而非處理
器。在解除對應之前,驅動程式不能以任何方式接觸緩衝
區。只有在呼叫 dma_unmap_single() 之後,驅動程式才能安
全存取緩衝區的內容。這意味著你必須先將要寫入裝置的資
料放在緩衝區,然後才能映射它。
 在DMA動作期間,不能解除對應,否則保證系統一定會嚴重
錯亂。
為何驅動程式不能接觸已被對應的緩衝區?有兩項原因,第
一,如果要輸出資料到裝置上,核心必須確保要放在DMA緩衝
區的資料,已經確實全數寫入記憶體;第二,如果被映射的緩
衝區位於周邊裝置無法存取的區域時,某些平台會直接讓DMA
作業失敗,而其它平台則可能會建立一個轉進緩衝區(bounce
buffer),轉進緩衝區只是另一塊裝置可以存取的記憶區。
46
15.4.4.4 設定臨時性DMA對應(3/3)




偶爾,驅動程式需要在解除對應之前,先存取臨時 DMA 緩衝區
的內容,可用以下函式:
void dma_sync_single_for_cpu(struct device *dev, dma_handle_t
bus_addr, size_t size, enum dma_data_direction direction);
此函式的呼叫時機,必須在處理器存取
DMA_FROM_DEVICE 緩衝區之前。一旦此函式
返回,CPU就全權擁有 DMA 緩衝區了。
在裝置存取 DMA 緩衝區之前,必須將所有權轉移回去:
void dma_sync_single_for_device(struct device *dev, dma_handle_t
bus_addr, size_t size, enum dma_data_direction direction);
呼叫了 void dma_sync_single_for_device() 函式之後,CPU就不
應該碰觸 DMA 緩衝區了。
47
15.4.4.5 臨時性的單頁對應




偶爾,可能會想要將 DMA 緩衝區映射到一個已有其 struct page 指標的
記憶頁;比如當想要讓裝置直接將資料傳輸到一個用 get_user_page ()
所取得的 user-space 暫存區時,就會有這樣的需要。
要讓 DMA 緩衝區暫時映射到 struct page 指標所指的記憶頁,可用下列
函式:
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);
offset 與 size 引數可用來縮限映射範圍,不過應該盡量避免不滿一頁的
映射。當映射範圍不滿一個記憶頁時,而配置範圍只包含部份的快取
線,就有可能導致快取失調的問題,進而導致記憶錯亂。
48
Demo(1/8)
49
Demo(2/8)
50
Demo(3/8)
51
Demo(4/8)
52
Demo(5/8)
53
Demo(6/8)
54
Demo(7/8)
55
Demo(8/8)
56