?? (ldd) ch13-mmap和dma(轉(zhuǎn)載).txt
字號:
(LDD) Ch13-MMAP和DMA(轉(zhuǎn)載)
第十三章 MMAP和DMA
這一章介紹Linux內(nèi)存管理和內(nèi)存映射的奧秘。同時講述設(shè)備驅(qū)動程序是如何使用“直接
內(nèi)存訪問”(DMA)的。盡管你可能反對,認(rèn)為DMA更屬于硬件處理而不是軟件接口,但
我覺得與硬件控制比起來,它與內(nèi)存管理更相關(guān)。
這一章比較高級;大多數(shù)驅(qū)動程序的作者并不需要太深入到系統(tǒng)內(nèi)部。不過理解內(nèi)存如
何工作可以幫助你在設(shè)計驅(qū)動程序時有效地利用系統(tǒng)的能力。
Linux中的內(nèi)存管理
這一節(jié)不是描述操作系統(tǒng)中內(nèi)存管理的理論,而是關(guān)注于這個理論在Linux實現(xiàn)中的主要
這一節(jié)不是描述操作系統(tǒng)中內(nèi)存管理的理論,而是關(guān)注于這個理論在Linux實現(xiàn)中的主要
特征。本節(jié)主要提供一些信息,跳過它不會影響您理解后面一些更面向?qū)崿F(xiàn)的主題。
頁表
當(dāng)一個程序查一個虛地址時,處理器將地址分成一些位域(bit field)。每個位域被用
來索引一個稱做頁表的數(shù)組,以獲得要么下一個表的地址,要么是存有這個虛地址的物
理頁的地址。
為了進(jìn)行虛地址到物理地址的映射,Linux核心管理三級頁表。開始這也許會顯得有些奇
怪。正如大多數(shù)PC程序員所知道的,x86硬件只實現(xiàn)了兩級頁表。事實上,大多數(shù)Linux
支持的32位處理器實現(xiàn)兩級,但不管怎樣核心實現(xiàn)了三級。
在處理器無關(guān)的實現(xiàn)中使用三級,使得Linux可以同時支持兩級和三級(如Alpha)的處
理器,而不必用大量的#ifdef語句把代碼攪得一團(tuán)糟。這種“保守編碼”方式并不會給
核心在兩級處理器上運行時帶來額外的開銷,因為實際上,編譯器已經(jīng)把沒用的一級優(yōu)
化掉了。
但是讓我們看一會兒實現(xiàn)換頁的數(shù)據(jù)結(jié)構(gòu)。為了跟上討論,你應(yīng)該記住大多數(shù)用作內(nèi)存
管理的數(shù)據(jù)都采用unsigned long的內(nèi)部表示,因為它們所表示的地址不會再被復(fù)引用。
下述幾條總結(jié)了Linux的三級實現(xiàn),由圖13-1示意:
l 一個“頁目錄(Page Directory,PGD)”是頂級頁表。PGD是由pgd_t項所組成的數(shù)
組,每一項指向一個二級頁表。每個進(jìn)程都有它自己的頁目錄,你可以認(rèn)為頁目錄是個
頁對齊的pgd_t數(shù)組。
l 二級表被稱做“中級頁目錄(Page Mid_level Directory)”或PMD。 PMD是一個
頁對齊的pmd_t數(shù)組。每個pmd_t是個指向三級頁表的指針。兩級的處理器,如x86和spar
c_4c,沒有物理PMD;它們將PMD聲明為只有一個元素的數(shù)組,這個元素的值就是PMD本身
——馬上我們將會看到C語言是如何處理這種情況以及編譯器是如何把這一級優(yōu)化掉的。
l 再下一級被簡單地稱為“頁表(Page Table)”。同樣地,它也是一個頁對齊的數(shù)
組,每一項被稱為“頁表項(Page Table Entry)”。核心使用pte_t類型表示每一項。
pte_t包含數(shù)據(jù)頁的物理地址。
上面提到的類型都在<asm/page.h>中定義,每個與換頁相關(guān)的源文件都必須包含它。
核心在一般程序執(zhí)行時并不需要為頁表查尋操心,因為這是有硬件完成的。不過,核心
必須將事情組織好,硬件才能正常工作。它必須構(gòu)造頁表,并在處理器報告一個頁面錯
時(即當(dāng)處理器需要的虛地址不在內(nèi)存中時)查找頁表,。
下面的符號被用來訪問頁表。<asm/page.h>和<asm/pgtable.h>必須被包含以使它們可以
被訪問。
(Figure 13.1 Linux的三級頁表)
PTRS_PER_PGD
PTRS_PER_PMD
PTRS_PER_PTE
每個頁表的大小。兩級處理器置PTRS_PER_PMD為1,以避免處理中級。
unsigned long pgd_bal(pgd_t pgd)
unsigned long pmd_val(pmd_t pmd)
unsigned long pte_val(pte_t pte)
這三個宏被用來從有類型數(shù)據(jù)項中獲取無符號長整數(shù)值。這些宏通過在源碼中使用嚴(yán)格
的數(shù)據(jù)類型有助于減小計算開銷。
pgd_t *pgd_offset(struct mm_struct *mm,unsigned long address)
pmd_t *pmd_offset(pgd_t *dir,unsigned long address)
pmd_t *pmd_offset(pgd_t *dir,unsigned long address)
pte_t *pte_offset(pmd_t *dir,unsigned long address)
這些線入函數(shù)是用于獲取與address相關(guān)聯(lián)的pgd,pmd和pte項。頁表查詢從一個指向結(jié)
構(gòu)mm_struct的指針開始。與當(dāng)前進(jìn)程內(nèi)存映射相關(guān)聯(lián)的指針是current->mm。指向核心
空間的指針由init_mm描述,它沒有被引出到模塊,因為它們不需要它。兩級處理器定義
pmd_offset(dir,add)為(pmd_t* )dir,這樣就把pmd折合在pgd上。掃描頁表的函數(shù)總是
被聲明為inline,而且編譯器優(yōu)化掉所有pmd查找。
unsigned long pte_page(pte_t pte)
這個函數(shù)從頁表項中抽取物理頁的地址。使用pte_val(pte)并不可行,因為微處理器使
用pte的低位存貯頁的額外信息。這些位不是實際地址的一部分,而且需要使用pte_page
從頁表中、抽取實際地址。
pte_present(pte_t pte)
這個宏返回布爾值表明數(shù)據(jù)頁當(dāng)前是否在內(nèi)存中。這是訪問pte低位的幾個函數(shù)中最常用
的一個——這些低位被pte_page丟棄。有趣的是注意到不論物理頁是否在內(nèi)存中,頁表
始終在(在當(dāng)前的Linux實現(xiàn)中)。這簡化了核心代碼,因為pgd_offset及其它類似函數(shù)
從不失敗;另一方面,即使一個有零“駐留存貯大小”的進(jìn)程也在實際RAM中保留它的頁
表。
表。
僅僅看看這些列出的函數(shù)不足以使你對Linux的內(nèi)存管理算法熟悉起來;實際的內(nèi)存管理
要復(fù)雜的多,而且還要處理其它一些繁雜的事,如高速緩存一致性。不過,上面列出的
函數(shù)足以給你一個關(guān)于頁面管理實現(xiàn)的初步印象;你可以從核心源碼的include/asm和mm
子樹中得到更好的信息。
虛擬內(nèi)存區(qū)域
盡管換頁位于內(nèi)存管理的最低層,你在能有效地使用計算機(jī)資源之前還需要一些別的知
識。核心需要一種更高級的機(jī)制處理進(jìn)程看到它的內(nèi)存方式。這種機(jī)制在Linux中以“虛
擬內(nèi)存區(qū)域的方式實現(xiàn),我稱之為“區(qū)域”或“VMA”。
一個區(qū)域是在一個進(jìn)程的虛存中的一個同質(zhì)區(qū)間,一個具有同樣許可標(biāo)志的地址的連續(xù)
范圍。它與“段”的概念松散對應(yīng),盡管最好還是將其描述為“具有自己屬性的內(nèi)存對
象”。一個進(jìn)程的內(nèi)存映象由下面組成:一個程序代碼(正文)區(qū)域;一個數(shù)據(jù)、BSS(
未初始化的數(shù)據(jù))和棧區(qū)域;以及每個活動的內(nèi)存映射的區(qū)域。一個進(jìn)程的內(nèi)存區(qū)域可
以通過查看/proc/pid/maps看到。/proc/self是/proc/pid的特殊情況,它總是指向當(dāng)前
進(jìn)程,做為一個例子,下面是三個不同的內(nèi)存映象,我在#字號后面加了一些短的注釋:
(代碼271)
每一行的域為:
每一行的域為:
start_end perm offset major:minor inode
perm代表一個位掩碼包括讀、寫和執(zhí)行許可;它表示對屬于這個區(qū)域的頁,允許進(jìn)程做
什么。這個域的最后一個字符要么是p表示私有的,要么是s表示共享的。
/proc/*/maps的每個域?qū)?yīng)著結(jié)構(gòu)vm_area_struct的一個域,我們將在下面描述這個結(jié)
構(gòu)。
實現(xiàn)mmap的方法的驅(qū)動程序需要填充在映射設(shè)備的進(jìn)程地址空間中的一個VMA結(jié)構(gòu)。因此
,驅(qū)動程序的作者對VMA應(yīng)該有個最起碼的理解以便使用它們。
讓我們看一下結(jié)構(gòu)vm_area_struct(在<linux/mm.h>)中最重要的幾個域。這些域可能
在設(shè)備驅(qū)動程序的mmap實現(xiàn)中被用到。注意核心維護(hù)VMA的列表和樹以優(yōu)化區(qū)域查找,vm
_area_struct的幾個域被用來維護(hù)這個組織。VMA不能按照驅(qū)動程序的意愿被產(chǎn)生,不然
結(jié)構(gòu)將會崩潰。VMA的幾個主要域如下:
unsigned long vm_start
unsigned long vm_end
一個VMA描述的虛地址介于vma->vm_start和vma->vm_end之間。這兩個域是/pro/*/maps
一個VMA描述的虛地址介于vma->vm_start和vma->vm_end之間。這兩個域是/pro/*/maps
中顯示的最先兩個域。
struct inode *vm_inode
如果這個區(qū)域與一個inode相關(guān)聯(lián)(如一個磁盤文件或一個設(shè)備節(jié)點),這個域是指向這
個inode的指針。不然,它為NULL。
unsigned long vm_offset
inode中這個區(qū)域的偏移量。當(dāng)一個文件或設(shè)備被映射時,這是映射到這個區(qū)域的第一個
字節(jié)的文件的位置(filp->f_ops)。
struct vm_operations_struct *vm_ops
vma->vm_ops說明這個內(nèi)存區(qū)域是一個核心“對象”,就象我們在本書中一直在用的結(jié)構(gòu)
file。這個區(qū)域聲明在其內(nèi)容上操作的“方法”,這個域就是用來列出這些方法。
和結(jié)構(gòu)vm_area_struct一樣,vm_operations_struct在<linux/mm.h>中定義;它包括了
列在下面的操作。這些操作是處理進(jìn)程內(nèi)存需要的所有操作,它們以被聲明的順序列出
。列出的原型是2.0的,與1.2.13的區(qū)別在每一項中都有描述。在本章的后面,這些函數(shù)
中的部分會被實現(xiàn),那時會更完全地加以描述。
void(*open)(struct vm_area_struct *vma);
在核心生成一個VMA后,它就把它打開。當(dāng)一個區(qū)域被復(fù)制時,孩子從父親那里繼承它的
操作,就區(qū)域用vm->open打開。例如,當(dāng)fork將存在進(jìn)程的區(qū)域復(fù)制到新的進(jìn)程時,vm_
ops->open被調(diào)用以打開所有的映象。另一方面,只要mmap執(zhí)行,區(qū)域在file->f_ops->m
map被調(diào)用前被產(chǎn)生,此時不調(diào)用vm_ops->open。
void(*close)(struct vm_area_struct *vma);
當(dāng)一個區(qū)域被銷毀時,核心調(diào)用它的close操作。注意VMA沒有相關(guān)的使用計數(shù);區(qū)域只
?? 快捷鍵說明
復(fù)制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -