15.2. mmap 設(shè)備操作

2018-02-24 15:50 更新

15.2.?mmap 設(shè)備操作

內(nèi)存映射是現(xiàn)代 Unix 系統(tǒng)最有趣的特性之一. 至于驅(qū)動(dòng), 內(nèi)存映射可被實(shí)現(xiàn)來(lái)提供用戶程序?qū)υO(shè)備內(nèi)存的直接存取.

一個(gè) mmap 用法的明確的例子可由查看給 X Windows 系統(tǒng)服務(wù)器的虛擬內(nèi)存區(qū)的一個(gè)子集來(lái)見(jiàn)到:


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
...

X 服務(wù)器的 VMA 的完整列表很長(zhǎng), 但是大部分此處不感興趣. 我們確實(shí)見(jiàn)到, 但是, /dev/mm 的 4 個(gè)不同映射, 它給出一些關(guān)于 X 服務(wù)器如何使用視頻卡的內(nèi)幕. 第一個(gè)映射在 a0000, 它是視頻內(nèi)存的在 640-KB ISA 孔里的標(biāo)準(zhǔn)位置. 再往下, 我們見(jiàn)到了大映射在 e8000000, 這個(gè)地址在系統(tǒng)中最高的 RAM 地址之上. 這是一個(gè)在適配器上的視頻內(nèi)存的直接映射.

這些區(qū)也可在 /proc/iomem 中見(jiàn)到:


000a0000-000bffff : Video RAM area
000c0000-000ccfff : Video ROM
000d1000-000d1fff : Adapter ROM
000f0000-000fffff : System ROM
d7f00000-f7efffff : PCI Bus #01

 e8000000-efffffff : 0000:01:00.0
fc700000-fccfffff : PCI Bus #01

 fcc00000-fcc0ffff : 0000:01:00.0 

映射一個(gè)設(shè)備意味著關(guān)聯(lián)一些用戶空間地址到設(shè)備內(nèi)存. 無(wú)論何時(shí)程序在給定范圍內(nèi)讀或?qū)? 它實(shí)際上是在存取設(shè)備. 在 X 服務(wù)器例子里, 使用 mmap 允許快速和容易地存取視頻卡內(nèi)存. 對(duì)于一個(gè)象這樣的性能關(guān)鍵的應(yīng)用, 直接存取有很大不同.

如你可能期望的, 不是每個(gè)設(shè)備都出借自己給 mmap 抽象; 這樣沒(méi)有意義, 例如, 對(duì)串口或其他面向流的設(shè)備. mmap 的另一個(gè)限制是映射粒度是 PAGE_SIZE. 內(nèi)核可以管理虛擬地址只在頁(yè)表一級(jí); 因此, 被映射區(qū)必須是 PAGE_SIZE 的整數(shù)倍并且必須位于是 PAGE_SIZE 整數(shù)倍開(kāi)始的物理地址. 內(nèi)核強(qiáng)制 size 的粒度通過(guò)做一個(gè)稍微大些的區(qū)域, 如果它的大小不是頁(yè)大小的整數(shù)倍.

這些限制對(duì)驅(qū)動(dòng)不是大的限制, 因?yàn)榇嫒≡O(shè)備的程序是設(shè)備依賴的. 因?yàn)槌绦虮仨氈涝O(shè)備如何工作的, 程序員不會(huì)太煩于需要知道如頁(yè)對(duì)齊這樣的細(xì)節(jié). 一個(gè)更大的限制存在當(dāng) ISA 設(shè)備被用在非 x86 平臺(tái)時(shí), 因?yàn)樗鼈兊?ISA 硬件視圖可能不連續(xù). 例如, 一些 Alpha 計(jì)算機(jī)將 ISA 內(nèi)存看作一個(gè)分散的 8 位, 16 位, 32 位項(xiàng)的集合, 沒(méi)有直接映射. 這種情況下, 你根本無(wú)法使用 mmap. 對(duì)不能進(jìn)行直接映射 ISA 地址到 Alph 地址可能只發(fā)生在 32-位 和 64-位內(nèi)存存取, ISA 可只做 8-位 和 16-位 發(fā)送, 并且沒(méi)有辦法來(lái)透明映射一個(gè)協(xié)議到另一個(gè).

使用 mmap 有相當(dāng)?shù)貎?yōu)勢(shì)當(dāng)這樣做可行的時(shí)候. 例如, 我們已經(jīng)看到 X 服務(wù)器, 它傳送大量數(shù)據(jù)到和從視頻內(nèi)存; 動(dòng)態(tài)映射圖形顯示到用戶空間提高了吞吐量, 如同一個(gè) lseek/write 實(shí)現(xiàn)相反. 另一個(gè)典型例子是一個(gè)控制一個(gè) PCI 設(shè)備的程序. 大部分 PCI 外設(shè)映射它們的控制寄存器到一個(gè)內(nèi)存地址, 并且一個(gè)高性能應(yīng)用程序可能首選對(duì)寄存器的直接存取來(lái)代替反復(fù)地調(diào)用 ioctl 來(lái)完成它的工作.

mmap 方法是 file_operation 結(jié)構(gòu)的一部分, 當(dāng)發(fā)出 mmap 系統(tǒng)調(diào)用時(shí)被引用. 用了 mmap, 內(nèi)核進(jìn)行大量工作在調(diào)用實(shí)際的方法之前, 并且, 因此, 方法的原型非常不同于系統(tǒng)調(diào)用的原型. 這不象 ioctl 和 poll 等調(diào)用, 內(nèi)核不會(huì)在調(diào)用這些方法之前做太多.

系統(tǒng)調(diào)用如下一樣被聲明(如在 mmap(2) 手冊(cè)頁(yè)中描述的 );


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

方法中的 filp 參數(shù)象在第 3 章介紹的那樣, 而 vma 包含關(guān)于用來(lái)存取設(shè)備的虛擬地址范圍的信息. 因此, 大量工作被內(nèi)核完成; 為實(shí)現(xiàn) mmap, 驅(qū)動(dòng)只要建立合適的頁(yè)表給這個(gè)地址范圍, 并且, 如果需要, 用新的操作集合替換 vma->vm_ops.

有 2 個(gè)建立頁(yè)表的方法:調(diào)用 remap_pfn_range 一次完成全部, 或者一次一頁(yè)通過(guò) nopage VMA 方法. 每個(gè)方法有它的優(yōu)點(diǎn)和限制. 我們從"一次全部"方法開(kāi)始, 它更簡(jiǎn)單. 從這里, 我們?cè)黾右粋€(gè)真實(shí)世界中的實(shí)現(xiàn)需要的復(fù)雜性.

15.2.1.?使用 remap_pfn_range

建立新頁(yè)來(lái)映射物理地址的工作由 remap_pfn_range 和 io_remap_page_range 來(lái)處理, 它們有下面的原型:


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

由這個(gè)函數(shù)返回的值常常是 0 或者一個(gè)負(fù)的錯(cuò)誤值. 讓我們看看這些函數(shù)參數(shù)的確切含義:

vma
頁(yè)范圍被映射到的虛擬內(nèi)存區(qū)

virt_addr
重新映射應(yīng)當(dāng)開(kāi)始的用戶虛擬地址. 這個(gè)函數(shù)建立頁(yè)表為這個(gè)虛擬地址范圍從 virt_addr 到 virt_addr_size.

pfn
頁(yè)幀號(hào), 對(duì)應(yīng)虛擬地址應(yīng)當(dāng)被映射的物理地址. 這個(gè)頁(yè)幀號(hào)簡(jiǎn)單地是物理地址右移 PAGE_SHIFT 位. 對(duì)大部分使用, VMA 結(jié)構(gòu)的 vm_paoff 成員正好包含你需要的值. 這個(gè)函數(shù)影響物理地址從 (pfn<<PAGE_SHIFT) 到 (pfn<<PAGE_SHIFT)+size.

size
正在被重新映射的區(qū)的大小, 以字節(jié).

prot
給新 VMA 要求的"protection". 驅(qū)動(dòng)可(并且應(yīng)當(dāng))使用在 vma->vm_page_prot 中找到的值.

給 remap_fpn_range 的參數(shù)是相當(dāng)直接的, 并且它們大部分是已經(jīng)在 VMA 中提供給你, 當(dāng)你的 mmap 方法被調(diào)用時(shí). 你可能好奇為什么有 2 個(gè)函數(shù), 但是. 第一個(gè) (remap_pfn_range)意圖用在 pfn 指向?qū)嶋H的系統(tǒng) RAM 的情況下, 而 io_remap_page_range 應(yīng)當(dāng)用在 phys_addr 指向 I/O 內(nèi)存時(shí). 實(shí)際上, 這 2 個(gè)函數(shù)在每個(gè)體系上是一致的, 除了 SPARC, 并且你在大部分情況下被使用看到 remap_pfn_range . 為編寫可移植的驅(qū)動(dòng), 但是, 你應(yīng)當(dāng)使用 remap_pfn_range 的適合你的特殊情況的變體.

另一種復(fù)雜性不得不處理緩存: 常常地, 引用設(shè)備內(nèi)存不應(yīng)當(dāng)被處理器緩存. 常常系統(tǒng) BIOS 做了正確設(shè)置, 但是它也可能通過(guò)保護(hù)字段關(guān)閉特定 VMA 的緩存. 不幸的是, 在這個(gè)級(jí)別上關(guān)閉緩存是高度處理器依賴的. 好奇的讀者想看看來(lái)自 drivers/char/mem.c 的 pgprot_noncached 函數(shù)來(lái)找到包含什么. 我們這里不進(jìn)一步討論這個(gè)主題.

15.2.2.?一個(gè)簡(jiǎn)單的實(shí)現(xiàn)

如果你的驅(qū)動(dòng)需要做一個(gè)簡(jiǎn)單的線性的設(shè)備內(nèi)存映射, 到一個(gè)用戶地址空間, remap_pfn_range 幾乎是所有你做這個(gè)工作真正需要做的. 下列的代碼從 drivers/char/mem.c 中得來(lái), 并且顯示了這個(gè)任務(wù)如何在一個(gè)稱為 simple ( Simple Implementation Mapping Pages with Little Enthusiasm)的典型模塊中進(jìn)行的.


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

如你所見(jiàn), 重新映射內(nèi)存只不過(guò)是調(diào)用 remap_pfn_rage 來(lái)創(chuàng)建必要的頁(yè)表.

15.2.3.?添加 VMA 的操作

如我們所見(jiàn), vm_area_struct 結(jié)構(gòu)包含一套操作可以用到 VMA. 現(xiàn)在我們看看以一個(gè)簡(jiǎn)單的方式提供這些操作. 特別地, 我們?yōu)?VMA 提供 open 和 close 操作. 這些操作被調(diào)用無(wú)論何時(shí)一個(gè)進(jìn)程打開(kāi)或關(guān)閉 VMA; 特別地, open 方法被調(diào)用任何時(shí)候一個(gè)進(jìn)程產(chǎn)生和創(chuàng)建一個(gè)對(duì) VMA 的新引用. open 和 close VMA 方法被調(diào)用加上內(nèi)核進(jìn)行的處理, 因此它們不需要重新實(shí)現(xiàn)任何那里完成的工作. 它們對(duì)于驅(qū)動(dòng)存在作為一個(gè)方法來(lái)做任何它們可能要求的附加處理.

如同它所證明的, 一個(gè)簡(jiǎn)單的驅(qū)動(dòng)例如 simple 不需要做任何額外的特殊處理. 我們已創(chuàng)建了 open 和 close 方法, 它打印一個(gè)信息到系統(tǒng)日志來(lái)通知大家它們已被調(diào)用. 不是特別有用, 但是它確實(shí)允許我們來(lái)顯示這些方法如何被提供, 并且見(jiàn)到當(dāng)它們被調(diào)用時(shí).

到此, 我們忽略了缺省的 vma->vm_ops 使用調(diào)用 printk 的操作:


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,
};

為使這些操作為一個(gè)特定的映射激活, 有必要存儲(chǔ)一個(gè)指向 simple_remap_um_ops 指針在相關(guān) VMA 的 vm_ops 成員中. 這常常在 mmap 方法中完成. 如果你回看 simple_remap_mmap 例子, 你見(jiàn)到這些代碼行:


vma->vm_ops = &simple_remap_vm_ops;
simple_vma_open(vma);

注意對(duì) simple_vma_open 的明確調(diào)用. 因?yàn)?open 方法不在初始化 mmap 時(shí)調(diào)用, 我們必須明確調(diào)用它如果我們要它運(yùn)行.

15.2.4.?使用 nopage 映射內(nèi)存

盡管 remap_pfn_range 對(duì)許多人工作得不錯(cuò), 如果不是大部分人, 驅(qū)動(dòng) mmap 的實(shí)現(xiàn)有時(shí)有點(diǎn)更大的靈活性是必要的. 在這樣的情況下, 一個(gè)使用 nopage VMA 方法的實(shí)現(xiàn)可被調(diào)用.

一種 nopage 方法有用的情況可由 mremap 系統(tǒng)調(diào)用引起, 它被應(yīng)用程序用來(lái)改變一個(gè)被映射區(qū)的綁定地址. 如它所發(fā)生的, 當(dāng)一個(gè)被映射的 VMA 被 mremap 改變時(shí)內(nèi)核不直接通知驅(qū)動(dòng). 如果這個(gè) VMA 的大小被縮減, 內(nèi)核可靜靜地刷出不需要的頁(yè), 而不必告訴驅(qū)動(dòng). 相反, 如果這個(gè) VMA 被擴(kuò)大, 當(dāng)映射必須為新頁(yè)建立時(shí), 驅(qū)動(dòng)最終通過(guò)對(duì) nopage 的調(diào)用發(fā)現(xiàn), 因此沒(méi)有必要進(jìn)行特殊的通知. nopage 方法, 因此, 如果你想支持 mremap 系統(tǒng)調(diào)用必須實(shí)現(xiàn). 這里, 我們展示一個(gè)簡(jiǎn)單的 nopage 實(shí)現(xiàn)給 simple 設(shè)備.

nopage 方法, 記住, 有下列原型:


struct page *(*nopage)(struct vm_area_struct *vma, unsigned long address, int *type);

當(dāng)一個(gè)用戶進(jìn)程試圖存取在一個(gè)不在內(nèi)存中的 VMA 中的一個(gè)頁(yè), 相關(guān)的 nopage 函數(shù)被調(diào)用. 地址參數(shù)包含導(dǎo)致出錯(cuò)的虛擬地址, 向下圓整到頁(yè)的開(kāi)始. nopage 函數(shù)必須定位并返回用戶需要的頁(yè)的 struct page 指針. 這個(gè)函數(shù)必須也負(fù)責(zé)遞增它通過(guò)調(diào)用 get_page 宏返回的頁(yè)的使用計(jì)數(shù).


 get_page(struct page *pageptr); 

這一步是需要的來(lái)保持在被映射頁(yè)的引用計(jì)數(shù)正確. 內(nèi)核為每個(gè)頁(yè)維護(hù)這個(gè)計(jì)數(shù); 當(dāng)計(jì)數(shù)到 0, 內(nèi)核知道這個(gè)頁(yè)可被放置在空閑列表了. 當(dāng)一個(gè) VMA 被去映射, 內(nèi)核遞減使用計(jì)數(shù)給區(qū)中每個(gè)頁(yè). 如果你的驅(qū)動(dòng)在添加一個(gè)頁(yè)到區(qū)時(shí)不遞增計(jì)數(shù), 使用計(jì)數(shù)過(guò)早地成為 0, 系統(tǒng)的整體性被破壞了.

nopage 方法也應(yīng)當(dāng)存儲(chǔ)錯(cuò)誤類型在由 type 參數(shù)指向的位置 -- 但是只當(dāng)那個(gè)參數(shù)不為 NULL. 在設(shè)備驅(qū)動(dòng)中, 類型的正確值將總是 VM_FAULT_MINOR.

如果你使用 nopage, 當(dāng)調(diào)用 mmap 常常很少有工作來(lái)做; 我們的版本看來(lái)象這樣:


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;

}

mmap 必須做的主要的事情是用我們自己的操作來(lái)替換缺省的(NULL)vm_ops 指針. nopage 方法接著進(jìn)行一次重新映射一頁(yè)并且返回它的 struct page 結(jié)構(gòu)的地址. 因?yàn)槲覀冞@里只實(shí)現(xiàn)一個(gè)到物理內(nèi)存的窗口, 重新映射的步驟是簡(jiǎn)單的: 我們只需要定位并返回一個(gè)指向 struct page 的指針給需要的地址. 我們的 nopage 方法看來(lái)如下:


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

因?yàn)? 再一次, 在這里我們簡(jiǎn)單地映射主內(nèi)存, nopage 函數(shù)只需要找到正確的 struct page 給出錯(cuò)地址并且遞增它的引用計(jì)數(shù). 因此, 事件的請(qǐng)求序列是計(jì)算需要地物理地址, 并且通過(guò)右移它 PAGE_SHIFT 位轉(zhuǎn)換它為以頁(yè)幀號(hào). 因?yàn)橛脩艨臻g可以給我們?nèi)魏嗡矚g的地址, 我們必須確保我們有一個(gè)有效的頁(yè)幀; pfn_valid 函數(shù)為我們做這些. 如果地址超范圍, 我們返回 NOPAGE_SIGBUS, 它產(chǎn)生一個(gè)總線信號(hào)被遞交給調(diào)用進(jìn)程.

否則, pfn_to_page 獲得必要的 struct page 指針; 我們可遞增它的引用計(jì)數(shù)(使用調(diào)用 get_page)并且返回它.

nopage 方法正常地返回一個(gè)指向 struct page 的指針. 如果, 由于某些原因, 一個(gè)正常的頁(yè)不能返回(即, 請(qǐng)求的地址超出驅(qū)動(dòng)的內(nèi)存區(qū)), NOPAGE_SIGBUS 可被返回來(lái)指示錯(cuò)誤; 這是上的簡(jiǎn)單代碼所做的. nopage 也可以返回 NOPAGE_OOM 來(lái)指示由于資源限制導(dǎo)致的失敗.

注意, 這個(gè)實(shí)現(xiàn)對(duì) ISA 內(nèi)存區(qū)起作用, 但是對(duì)那些在 PCI 總線上的不行. PCI 內(nèi)存被映射在最高的系統(tǒng)內(nèi)存之上, 并且在系統(tǒng)內(nèi)存中沒(méi)有這些地址的入口. 因?yàn)闆](méi)有 struct page 來(lái)返回一個(gè)指向的指針, nopage 不能在這些情況下使用; 你必須使用 remap_pfn_range 代替.

如果 nopage 方法被留置為 NULL, 處理頁(yè)出錯(cuò)的內(nèi)核代碼映射零頁(yè)到出錯(cuò)的虛擬地址. 零頁(yè)是一個(gè)寫時(shí)拷貝的頁(yè), 它讀作為0, 并且被用來(lái), 例如, 映射 BSS 段. 任何引用零頁(yè)的進(jìn)程都看到: 一個(gè)填滿 0 的頁(yè). 如果進(jìn)程寫到這個(gè)頁(yè), 它最終修改一個(gè)私有頁(yè). 因此, 如果一個(gè)進(jìn)程擴(kuò)展一個(gè)映射的頁(yè)通過(guò)調(diào)用 mremap, 并且驅(qū)動(dòng)還沒(méi)有實(shí)現(xiàn) nopage, 進(jìn)程結(jié)束以零填充的內(nèi)存代替一個(gè)段錯(cuò)誤.

15.2.5.?重新映射特定 I/O 區(qū)

所有的我們至今所見(jiàn)的例子是 /dev/mem 的重新實(shí)現(xiàn); 它們重新映射物理地址到用戶空間. 典型的驅(qū)動(dòng), 但是, 想只映射應(yīng)用到它的外設(shè)設(shè)備的小的地址范圍, 不是全部?jī)?nèi)存. 為了映射到用戶空間只一個(gè)整個(gè)內(nèi)存范圍的子集, 驅(qū)動(dòng)只需要使用偏移. 下面為一個(gè)驅(qū)動(dòng)做這個(gè)技巧來(lái)映射一個(gè) simple_region_size 字節(jié)的區(qū)域, 在物理地址 simple_region_start(應(yīng)當(dāng)是頁(yè)對(duì)齊的) 開(kāi)始:


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; /* spans too high */
remap_pfn_range(vma, vma_>vm_start, physical, vsize, vma->vm_page_prot);

除了計(jì)算偏移, 這個(gè)代碼引入了一個(gè)檢查來(lái)報(bào)告一個(gè)錯(cuò)誤當(dāng)程序試圖映射超過(guò)在目標(biāo)設(shè)備的 I/O 區(qū)可用的內(nèi)存. 在這個(gè)代碼中, psize 是已指定了偏移后剩下的物理 I/O 大小, 并且 vsize 是虛擬內(nèi)存請(qǐng)求的大小; 這個(gè)函數(shù)拒絕映射超出允許的內(nèi)存范圍的地址.

注意, 用戶空間可一直使用 mremap 來(lái)擴(kuò)展它的映射, 可能超過(guò)物理設(shè)備區(qū)的結(jié)尾. 如果你的驅(qū)動(dòng)不能定義一個(gè) nopage method, 它從不會(huì)得到這個(gè)擴(kuò)展的通知, 并且額外的區(qū)映射到零頁(yè). 作為一個(gè)驅(qū)動(dòng)編寫者, 你可能很想阻止這種行為; 映射理由到你的區(qū)的結(jié)尾不是一個(gè)明顯的壞事情, 但是很不可能程序員希望它發(fā)生.

最簡(jiǎn)單的方法來(lái)阻止映射擴(kuò)展是實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 nopage 方法, 它一直導(dǎo)致一個(gè)總線信號(hào)被發(fā)送給出錯(cuò)進(jìn)程. 這樣的一個(gè)方法可能看來(lái)如此:


struct page *simple_nopage(struct vm_area_struct *vma,
 unsigned long address, int *type);
{ return NOPAGE_SIGBUS; /* send a SIGBUS */}

如我們已見(jiàn)到的, nopage 方法只當(dāng)進(jìn)程解引用一個(gè)地址時(shí)被調(diào)用, 這個(gè)地址在一個(gè)已知的 VMA 中但是當(dāng)前沒(méi)有有效的頁(yè)表入口給這個(gè) VMA. 如果有已使用 remap_pfn_range 來(lái)映射全部設(shè)備區(qū), 這里展示的 nopage 方法只被調(diào)用來(lái)引用那個(gè)區(qū)外部. 因此, 它能夠安全地返回 NOPAGE_SIGBUS 來(lái)指示一個(gè)錯(cuò)誤. 當(dāng)然, 一個(gè)更加完整的 nopage 實(shí)現(xiàn)可以檢查是否出錯(cuò)地址在設(shè)備區(qū)內(nèi), 并且如果是這樣進(jìn)行重新映射. 但是, 再一次, nopage 無(wú)法在 PCI 內(nèi)存區(qū)工作, 因此 PCI 映射的擴(kuò)展是不可能的.

15.2.6.?重新映射 RAM

remap_pfn_range 的一個(gè)有趣的限制是它只存取保留頁(yè)和在物理內(nèi)存頂之上的物理地址. 在 Linux, 一個(gè)物理地址頁(yè)被標(biāo)志為"保留的"在內(nèi)存映射中來(lái)指示它對(duì)內(nèi)存管理是不可用的. 在 PC 上, 例如, 640 KB 和 1MB 之間被標(biāo)記為保留的, 如同駐留內(nèi)核代碼自身的頁(yè). 保留頁(yè)被鎖定在內(nèi)存并且是唯一可被安全映射到用戶空間的; 這個(gè)限制是系統(tǒng)穩(wěn)定的一個(gè)基本要求.

因此, remap_pfn_range 不允許你重新映射傳統(tǒng)地址, 這包括你通過(guò)調(diào)用 get_free_page 獲得的. 相反, 它映射在零頁(yè). 所有都看來(lái)正常, 除了進(jìn)程見(jiàn)到私有的, 零填充的頁(yè)而不是它在期望的被重新映射的 RAM. 這個(gè)函數(shù)做了大部分硬件驅(qū)動(dòng)需要來(lái)做的所有事情, 因?yàn)樗軌蛑匦掠成涓叨?PCI 緩沖和 ISA 內(nèi)存.

remap_pfn_range 的限制可通過(guò)運(yùn)行 mapper 見(jiàn)到, 其中一個(gè)例子程序在 misc-progs 在 O'Reilly 的 FTP 網(wǎng)站提供的文件. mapper 是一個(gè)簡(jiǎn)單的工具可用來(lái)快速測(cè)試 mmap 系統(tǒng)調(diào)用; 它映射由命令行選項(xiàng)指定的一個(gè)文件的只讀部分, 并且輸出被映射的區(qū)到標(biāo)準(zhǔn)輸出. 下面的部分, 例如, 顯示 /dev/mem 沒(méi)有映射位于地址 64 KB的物理頁(yè) --相反, 我們看到一個(gè)頁(yè)充滿 0 (例子中的主機(jī)是一臺(tái) PC, 但是結(jié)果應(yīng)該在其他平臺(tái)上相同).


morgana.root# ./mapper /dev/mem 0x10000 0x1000 | od -Ax -t x1
mapped "/dev/mem" from 65536 to 69632
000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
*
001000

remap_pfn_range 處理 RAM 的不能之處說(shuō)明基于內(nèi)存的設(shè)備如 scull 不能輕易實(shí)現(xiàn) mmap, 因?yàn)樗脑O(shè)備內(nèi)存是傳統(tǒng)內(nèi)存, 不是 I/O 內(nèi)存. 幸運(yùn)的是, 一個(gè)相對(duì)容易的方法對(duì)任何需要映射 RAM 到用戶空間的驅(qū)動(dòng)都可用; 它使用我們前面已見(jiàn)過(guò)的 nopage 方法.

15.2.6.1.?使用 nopage 方法重新映射 RAM

映射真實(shí)內(nèi)存到用戶空間的方法是使用 vm_ops-<nopage 來(lái)一次一個(gè)地處理頁(yè)錯(cuò). 一個(gè)簡(jiǎn)單的實(shí)現(xiàn)是 scullp 模塊的一部分, 在第 8 張介紹.

scullp 是一個(gè)面向頁(yè)的字符設(shè)備. 因?yàn)樗敲嫦蝽?yè)的, 它可以在它的內(nèi)存上實(shí)現(xiàn) mmap. 實(shí)現(xiàn)內(nèi)存映射的代碼使用一些在"Linux 中的內(nèi)存管理"一節(jié)中介紹的概念.

在檢查代碼前, 讓我們查看影響在 scullp 中的 mmap 實(shí)現(xiàn)的設(shè)計(jì)選擇.

  • scullp 只要設(shè)備被映射就不會(huì)釋放設(shè)備內(nèi)存. 這是策略問(wèn)題而非一個(gè)需求, 并且它不同于 scull 和類似設(shè)備的行為, 它們被截短為 0 當(dāng)為寫而打開(kāi)時(shí). 對(duì)釋放一個(gè)映射的 scullp 設(shè)備的拒絕, 允許一個(gè)進(jìn)程覆蓋被其他進(jìn)程映射的區(qū)., 因此你可以測(cè)試并且看進(jìn)程和設(shè)備內(nèi)存如何交互. 為避免釋放一個(gè)映射設(shè)備, 驅(qū)動(dòng)必須保持一個(gè)激活映射的計(jì)數(shù); 在設(shè)備結(jié)構(gòu)中的 vmas 成員被用來(lái)作此目的.

  • 內(nèi)存映射僅當(dāng) scullp 的 order 參數(shù)(在模塊加載時(shí)間設(shè)置)是 0 時(shí)進(jìn)行. 這個(gè)參數(shù)控制 get_free_pages 如何被調(diào)用( 見(jiàn)第 8 章"get_free_page 及其友" 一節(jié)). 0 order 的限制( 這強(qiáng)制一次分配一頁(yè), 而不是以大的組)被 get_free_pages 的內(nèi)部所規(guī)定, 它是 scullp 所使用的分配函數(shù). 為最大化分配性能, Linux 內(nèi)核維護(hù)一個(gè)空閑頁(yè)列表給每個(gè)分配級(jí)別, 并且只有在一個(gè)簇中第一頁(yè)的引用計(jì)數(shù)被 get_free_pages 遞增以及被 free_pages 遞減. mmap 方法對(duì)一個(gè) scullp 設(shè)備被禁止, 如果分配級(jí)大于 0, 因?yàn)?nopage 處理單個(gè)頁(yè)而不是一簇頁(yè). scullp 不知道如何正確管理是高級(jí)別分配的一部分的頁(yè)的引用計(jì)數(shù).(如果你需要重新回顧 scullp 和 內(nèi)存分配級(jí)別值, 返回第 8 章的"一個(gè)使用整頁(yè)的 scull: scullp"一節(jié).)

0-級(jí)的限制大部分是用來(lái)保持代碼簡(jiǎn)單. 它可能正確實(shí)現(xiàn) mmap 給多頁(yè)分配, 通過(guò)使用頁(yè)的使用計(jì)數(shù), 但是它可能只增加了例子的復(fù)雜性而沒(méi)有介紹任何有趣的信息.

打算根據(jù)剛剛概括的規(guī)則來(lái)映射 RAM 的代碼, 需要實(shí)現(xiàn) open, close, 和 nopage VMA 方法; 它還需要存取內(nèi)存映射來(lái)調(diào)整頁(yè)使用計(jì)數(shù).

這個(gè) scullp_mmap 的實(shí)現(xiàn)非常短, 因?yàn)樗蕾?nopage 函數(shù)來(lái)做所有的感興趣的工作:


int scullp_mmap(struct file *filp, struct vm_area_struct *vma)
{

 struct inode *inode = filp->f_dentry->d_inode;
 /* refuse to map if order is not 0 */
 if (scullp_devices[iminor(inode)].order)
 return -ENODEV;

 /* don't do anything here: "nopage" will fill the holes */
 vma->vm_ops = &scullp_vm_ops;
 vma->vm_flags |= VM_RESERVED;
 vma->vm_private_data = filp->private_data;
 scullp_vma_open(vma);
 return 0;
}

if 語(yǔ)句的目的是避免映射分配級(jí)別不是 0 的設(shè)備. scullp 的操作存儲(chǔ)在 vm_ops 成員, 并且一個(gè)指向設(shè)備結(jié)構(gòu)的指針藏于 vm_private_data 成員. 最后, vm_ops->open 被調(diào)用來(lái)更新設(shè)備的激活映射的計(jì)數(shù).

open 和 close 簡(jiǎn)單地跟蹤映射計(jì)數(shù)并如下定義:


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

大部分地工作接下來(lái)由 nopage 進(jìn)行. 在 scullp 實(shí)現(xiàn)中, 給 nopage 的地址參數(shù)被用來(lái)計(jì)算設(shè)備中的偏移; 這個(gè)偏移接著被用來(lái)在 scullp 內(nèi)存樹(shù)中查找正確的頁(yè).


struct page *scullp_vma_nopage(struct vm_area_struct *vma, unsigned long address, int *type)
{
        unsigned long offset;
        struct scullp_dev *ptr, *dev = vma->vm_private_data;
        struct page *page = NOPAGE_SIGBUS;
        void *pageptr = NULL; /* default to "missing" */

        down(&dev->sem);
        offset = (address - vma->vm_start) + (vma->vm_pgoff << PAGE_SHIFT);
        if (offset >= dev->size)
                goto out; /* out of range */

        /*
        * Now retrieve the scullp device from the list,then the page.
        * If the device has holes, the process receives a SIGBUS when
        * accessing the hole.
        */
        offset >>= PAGE_SHIFT; /* offset is a number of pages */
        for (ptr = dev; ptr && offset >= dev->qset;)
        {
                ptr = ptr->next;
                offset -= dev->qset;
        }
        if (ptr && ptr->data)
                pageptr = ptr->data[offset];
        if (!pageptr)
                goto out; /* hole or end-of-file */
        page = virt_to_page(pageptr);

        /* got it, now increment the count */
        get_page(page);
        if (type)
                *type = VM_FAULT_MINOR;
out:
        up(&dev->sem);
        return page;
}

scullp 使用由 get_free_pages 獲取的內(nèi)存. 那個(gè)內(nèi)存使用邏輯地址尋址, 因此所有的 scullp_nopage 為獲得一個(gè) struct page 指針不得不做的是調(diào)用 virt_to_page.

現(xiàn)在 scullp 設(shè)備如同期望般工作了, 就象你在這個(gè)從 mapper 工具中的例子輸出能見(jiàn)到的. 這里, 我們發(fā)送一個(gè) /dev 的目錄列表(一個(gè)長(zhǎng)的)到 scullp 設(shè)備并且接著使用 mapper 工具來(lái)查看這個(gè)列表的各個(gè)部分連同 mmap.


morgana% ls -l /dev > /dev/scullp
morgana% ./mapper /dev/scullp 0 140
mapped "/dev/scullp" from 0 (0x00000000) to 140 (0x0000008c)
total 232
crw-------1 root root 10, 10 Sep 15 07:40 adbmouse
crw-r--r--1 root root 10, 175 Sep 15 07:40 agpgart
morgana% ./mapper /dev/scullp 8192 200 mapped "/dev/scullp" from 8192 (0x00002000) to 8392 (0x000020c8) 
d0h1494  
brw-rw---- 1 root  floppy  2,  92 Sep 15 07:40 fd0h1660  
brw-rw---- 1 root  floppy  2,  20 Sep 15 07:40 fd0h360  
brw-rw---- 1 root  floppy  2,  12 Sep 15 07:40 fd0H360  

15.2.7.?重映射內(nèi)核虛擬地址

盡管它極少需要, 看一個(gè)驅(qū)動(dòng)如何使用 mmap 映射一個(gè)內(nèi)核虛擬地址到用戶空間是有趣的. 記住, 一個(gè)真正的內(nèi)核虛擬地址, 是一個(gè)由諸如 vmalloc 的函數(shù)返回的地址 -- 就是, 一個(gè)映射到內(nèi)核頁(yè)表中的虛擬地址. 本節(jié)的代碼來(lái)自 scullv, 這是如同 scullp 但是通過(guò) vmalloc 分配它的存儲(chǔ)的模塊.

大部分的 scullv 實(shí)現(xiàn)如同我們剛剛見(jiàn)到的 scullp, 除了沒(méi)有必要檢查控制內(nèi)存分配的 order 參數(shù). 這個(gè)的原因是 vmalloc 分配它的頁(yè)一次一個(gè), 因?yàn)閱雾?yè)分配比多頁(yè)分配更加可能成功. 因此, 分配級(jí)別問(wèn)題不適用 vmalloc 分配的空間.

此外, 在由 scullp 和 scullv 使用的 nopage 實(shí)現(xiàn)中只有一個(gè)不同. 記住, scullp 一旦它發(fā)現(xiàn)感興趣的頁(yè), 將使用 virt_to_page 來(lái)獲得對(duì)應(yīng)的 struct page 指針. 那個(gè)函數(shù)不使用內(nèi)核虛擬地址, 但是. 相反, 你必須使用 mvalloc_to_page. 因此 scullv 版本的 nopage 的最后部分看來(lái)如此:


/*
* After scullv lookup, "page" is now the address of the page
* needed by the current process. Since it's a vmalloc address,
* turn it into a struct page.
*/
page = vmalloc_to_page(pageptr);

/* got it, now increment the count */
get_page(page);
if (type)
        *type = VM_FAULT_MINOR;
out:
up(&dev->sem);
return page;

基于這個(gè)討論, 你可能也想映射由 ioremap 返回的地址到用戶空間. 但是, 那可能是一個(gè)錯(cuò)誤; 來(lái)自 ioremap 的地址是特殊的并且不能作為正常的內(nèi)核虛擬地址對(duì)待. 相反, 你應(yīng)當(dāng)使用 remap_pfn_range 來(lái)重新映射 I/O 內(nèi)存區(qū)到用戶空間.

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)