邏輯和內(nèi)核虛擬地址之間的不同在配備大量內(nèi)存的 32-位系統(tǒng)中被突出. 用 32 位, 可能尋址 4 G 內(nèi)存. 但是, 直到最近, 在 32-位 系統(tǒng)的 Linux 被限制比那個少很多的內(nèi)存, 因為它建立虛擬地址的方式.
內(nèi)核( 在 x86 體系上, 在缺省配置里) 在用戶空間和內(nèi)核之間劃分 4-G 虛擬地址; 在 2 個上下文中使用同一套映射. 一個典型的劃分分出 3 GB 給用戶空間, 和 1 GB 給內(nèi)核空間. [47]內(nèi)核的代碼和數(shù)據(jù)結構必須要適合這個空間, 但是內(nèi)核地址空間最大的消費者是物理內(nèi)存的虛擬映射. 內(nèi)核不能直接操作沒有映射到內(nèi)核的地址空間. 內(nèi)核, 換句話說, 需要它自己的虛擬地址給任何它必須直接接觸的內(nèi)存. 因此, 多年來, 能夠被內(nèi)核處理的的最大量的物理內(nèi)存是能夠映射到虛擬地址的內(nèi)核部分的數(shù)量, 減去內(nèi)核代碼自身需要的空間. 結果, 基于 x86 的 Linux 系統(tǒng)可以工作在最多稍小于 1 GB 物理內(nèi)存.
為應對更多內(nèi)存的商業(yè)壓力而不破壞 32-位 應用和系統(tǒng)的兼容性, 處理器制造商已經(jīng)增加了"地址擴展"特性到他們的產(chǎn)品中. 結果, 在許多情況下, 即便 32-位 處理器也能夠尋址多于 4GB 物理內(nèi)存. 但是, 多少內(nèi)存可被直接用邏輯地址映射的限制還存在. 這樣內(nèi)存的最低部分(上到 1 或 2 GB, 根據(jù)硬件和內(nèi)核配置)有邏輯地址; 剩下的(高內(nèi)存)沒有. 在存取一個特定高地址頁之前, 內(nèi)核必須建立一個明確的虛擬映射來使這個也在內(nèi)核地址空間可用. 因此, 許多內(nèi)核數(shù)據(jù)結構必須放在低內(nèi)存; 高內(nèi)存用作被保留為用戶進程頁.
術語"高內(nèi)存"對有些人可能是疑惑的, 特別因為它在 PC 世界里有其他的含義. 因此, 為清晰起見, 我們將定義這些術語:
Low memory
邏輯地址在內(nèi)核空間中存在的內(nèi)存. 在大部分每個系統(tǒng)你可能會遇到, 所有的內(nèi)存都是低內(nèi)存.
High memory
邏輯地址不存在的內(nèi)存, 因為它在為內(nèi)核虛擬地址設置的地址范圍之外.
在 i386 系統(tǒng)上, 低和高內(nèi)存之間的分界常常設置在剛剛在 1 GB 之下, 盡管那個邊界在內(nèi)核配置時可被改變. 這個邊界和在原始 PC 中有的老的 640 KB 限制沒有任何關聯(lián), 并且它的位置不是硬件規(guī)定的. 相反, 它是, 內(nèi)核自身設置的一個限制當它在內(nèi)核和用戶空間之間劃分 32-位地址空間時.
我們將指出使用高內(nèi)存的限制, 隨著我們在本章遇到它們時.
歷史上, 內(nèi)核已使用邏輯地址來引用物理內(nèi)存頁. 高內(nèi)存支持的增加, 但是, 已暴露這個方法的一個明顯的問題 -- 邏輯地址對高內(nèi)存不可用. 因此, 處理內(nèi)存的內(nèi)核函數(shù)更多在使用指向 struct page 的指針來代替(在 <linux/mm.h> 中定義). 這個數(shù)據(jù)結構只是用來跟蹤內(nèi)核需要知道的關于物理內(nèi)存的所有事情.
2.6 內(nèi)核(帶一個增加的補丁)可以支持一個 "4G/4G" 模式在 x86 硬件上, 它以微弱的性能代價換來更大的內(nèi)核和用戶虛擬地址空間.
系統(tǒng)中每一個物理頁有一個 struct page. 這個結構的一些成員包括下列:
atomic_t count;
這個頁的引用數(shù). 當這個 count 掉到 0, 這頁被返回給空閑列表.
void *virtual;
這頁的內(nèi)核虛擬地址, 如果它被映射; 否則, NULL. 低內(nèi)存頁一直被映射; 高內(nèi)存頁常常不是. 這個成員不是在所有體系上出現(xiàn); 它通常只在頁的內(nèi)核虛擬地址無法輕易計算時被編譯. 如果你想查看這個成員, 正確的方法是使用 page_address 宏, 下面描述.
unsigned long flags;
一套描述頁狀態(tài)的一套位標志. 這些包括 PG_locked, 它指示該頁在內(nèi)存中已被加鎖, 以及 PG_reserved, 它防止內(nèi)存管理系統(tǒng)使用該頁.
有很多的信息在 struct page 中, 但是它是內(nèi)存管理的更深的黑魔法的一部分并且和驅動編寫者無關.
內(nèi)核維護一個或多個 struct page 項的數(shù)組來跟蹤系統(tǒng)中所有物理內(nèi)存. 在某些系統(tǒng), 有一個單個數(shù)組稱為 mem_map. 但是, 在某些系統(tǒng), 情況更加復雜. 非一致內(nèi)存存取( NUMA )系統(tǒng)和那些有很大不連續(xù)的物理內(nèi)存的可能有多于一個內(nèi)存映射數(shù)組, 因此打算是可移植的代碼在任何可能時候應當避免直接對數(shù)組存取. 幸運的是, 只是使用 struct page 指針常常是非常容易, 而不用擔心它們來自哪里.
有些函數(shù)和宏被定義來在 struct page 指針和虛擬地址之間轉換:
struct page virt_to_page(void kaddr);
這個宏, 定義在 <asm/page.h>, 采用一個內(nèi)核邏輯地址并返回它的被關聯(lián)的 struct page 指針. 因為它需要一個邏輯地址, 它不使用來自 vmalloc 的內(nèi)存或者高內(nèi)存.
struct page *pfn_to_page(int pfn);
為給定的頁幀號返回 struct page 指針. 如果需要, 它在傳遞給 pfn_to_page 之前使用 pfn_valid 來檢查一個頁幀號的有效性.
void page_address(struct page page);
返回這個頁的內(nèi)核虛擬地址, 如果這樣一個地址存在. 對于高內(nèi)存, 那個地址僅當這個頁已被映射才存在. 這個函數(shù)在 <linux/mm.h> 中定義. 大部分情況下, 你想使用 kmap 的一個版本而不是 page_address.
kmap 為系統(tǒng)中的任何頁返回一個內(nèi)核虛擬地址. 對于低內(nèi)存頁, 它只返回頁的邏輯地址; 對于高內(nèi)存, kmap 在內(nèi)核地址空間的一個專用部分中創(chuàng)建一個特殊的映射. 使用 kmap 創(chuàng)建的映射應當一直使用 kunmap 來釋放;一個有限數(shù)目的這樣的映射可用, 因此最好不要在它們上停留太長時間. kmap 調(diào)用維護一個計數(shù)器, 因此如果 2 個或 多個函數(shù)都在同一個頁上調(diào)用 kmap, 正確的事情發(fā)生了. 還要注意 kmap 可能睡眠當沒有映射可用時.
kmap_atomic 是 kmap 的一種高性能形式. 每個體系都給原子的 kmaps 維護一小列插口( 專用的頁表項); 一個 kmap_atomic 的調(diào)用者必須在 type 參數(shù)中告知系統(tǒng)使用這些插口中的哪個. 對驅動有意義的唯一插口是 KM_USER0 和 KM_USER1 (對于直接從來自用戶空間的調(diào)用運行的代碼), 以及 KM_IRQ0 和 KM_IRQ1(對于中斷處理). 注意原子的 kmaps 必須被原子地處理; 你的代碼不能在持有一個時睡眠. 還要注意內(nèi)核中沒有什么可以阻止 2 個函數(shù)試圖使用同一個插口并且相互干擾( 盡管每個 CPU 有獨特的一套插口). 實際上, 對原子的 kmap 插口的競爭看來不是個問題.
在本章后面和后續(xù)章節(jié)中當我們進入例子代碼時, 我們看到這些函數(shù)的一些使用,
在任何現(xiàn)代系統(tǒng)上, 處理器必須有一個機制來轉換虛擬地址到它的對應物理地址. 這個機制被稱為一個頁表; 它本質上是一個多級樹型結構數(shù)組, 包含了虛擬-到-物理的映射和幾個關聯(lián)的標志. Linux 內(nèi)核維護一套頁表即便在沒有直接使用這樣頁表的體系上.
設備驅動通常可以做的許多操作能涉及操作頁表. 幸運的是對于驅動作者, 2.6 內(nèi)核已經(jīng)去掉了任何直接使用頁表的需要. 結果是, 我們不描述它們的任何細節(jié); 好奇的讀者可能想讀一下 Understanding The Linux Kernel 來了解完整的內(nèi)容, 作者是 Daniel P. Bovet 和 Marco Cesati (O' Reilly).
虛擬內(nèi)存區(qū)( VMA )用來管理一個進程的地址空間的獨特區(qū)域的內(nèi)核數(shù)據(jù)結構. 一個 VMA 代表一個進程的虛擬內(nèi)存的一個同質區(qū)域: 一個有相同許可標志和被相同對象(如, 一個文件或者交換空間)支持的連續(xù)虛擬地址范圍. 它松散地對應于一個"段"的概念, 盡管可以更好地描述為"一個有它自己特性的內(nèi)存對象". 一個進程的內(nèi)存映射有下列區(qū)組成:
給程序的可執(zhí)行代碼(常常稱為 text)的一個區(qū).
給數(shù)據(jù)的多個區(qū), 包括初始化的數(shù)據(jù)(它有一個明確的被分配的值, 在執(zhí)行開始), 未初始化數(shù)據(jù)(BBS), [48]以及程序堆棧.
給每個激活的內(nèi)存映射的一個區(qū)域.
一個進程的內(nèi)存區(qū)可看到通過 /proc/<pid/maps>(這里 pid, 當然, 用一個進程的 ID 來替換). /proc/self 是一個 /proc/id 的特殊情況, 因為它常常指當前進程. 作為一個例子, 這里是幾個內(nèi)存映射(我們添加了簡短注釋)
# cat /proc/1/maps look at init
08048000-0804e000 r-xp 00000000 03:01 64652
0804e000-0804f000 rw-p 00006000 03:01 64652
0804f000-08053000 rwxp 00000000 00:00 0
40000000-40015000 r-xp 00000000 03:01 96278
40015000-40016000 rw-p 00014000 03:01 96278
40016000-40017000 rw-p 00000000 00:00 0
42000000-4212e000 r-xp 00000000 03:01 80290
4212e000-42131000 rw-p 0012e000 03:01 80290
42131000-42133000 rw-p 00000000 00:00 0
bffff000-c0000000 rwxp 00000000 00:00 0
ffffe000-fffff000 ---p 00000000 00:00 0
/sbin/init text /sbin/init data zero-mapped BSS /lib/ld-2.3.2.so text /lib/ld-2.3.2.so data BSS for ld.so /lib/tls/libc-2.3.2.so text /lib/tls/libc-2.3.2.so data BSS for libc Stack segment vsyscall page
# rsh wolf cat /proc/self/maps #### x86-64 (trimmed)
00400000-00405000 r-xp 00000000 03:01 1596291 /bin/cat text
00504000-00505000 rw-p 00004000 03:01 1596291 /bin/cat data
00505000-00526000 rwxp 00505000 00:00 0 bss
3252200000-3252214000 r-xp 00000000 03:01 1237890 /lib64/ld-2.3.3.so
3252300000-3252301000 r--p 00100000 03:01 1237890 /lib64/ld-2.3.3.so
3252301000-3252302000 rw-p 00101000 03:01 1237890 /lib64/ld-2.3.3.so
7fbfffe000-7fc0000000 rw-p 7fbfffe000 00:00 0 stack
ffffffffff600000-ffffffffffe00000 ---p 00000000 00:00 0 vsyscall
每行的字段是:
start-end perm offset major:minor inode image
每個在 /proc/*/maps (出來映象的名子) 對應 struct vm_area_struct 中的一個成員:
start end
這個內(nèi)存區(qū)的開始和結束虛擬地址.
perm
帶有內(nèi)存區(qū)的讀,寫和執(zhí)行許可的位掩碼. 這個成員描述進程可以對屬于這個區(qū)的頁做什么. 成員的最后一個字符要么是給"私有"的 p 要么是給"共享"的 s.
offset
內(nèi)存區(qū)在它被映射到的文件中的起始位置. 0 偏移意味著內(nèi)存區(qū)開始對應文件的開始.
major minor
持有已被映射文件的設備的主次編號. 易混淆地, 對于設備映射, 主次編號指的是持有被用戶打開的設備特殊文件的磁盤分區(qū), 不是設備自身.
inode
被映射文件的 inode 號.
image
已被映射的文件名((常常在一個可執(zhí)行映象中).
當一個用戶空間進程調(diào)用 mmap 來映射設備內(nèi)存到它的地址空間, 系統(tǒng)通過一個新 VMA 代表那個映射來響應. 一個支持 mmap 的驅動(并且, 因此, 實現(xiàn) mmap 方法)需要來幫助那個進程來完成那個 VMA 的初始化. 驅動編寫者應當, 因此, 為支持 mmap 應至少有對 VMA 的最少的理解.
讓我們看再 struct vm_area_struct 中最重要的成員( 在 <linux/mm.h> 中定義). 這些成員應當被設備驅動在它們的 mmap 實現(xiàn)中使用. 注意內(nèi)核維護 VMA 的鏈表和樹來優(yōu)化區(qū)查找, 并且 vm_area_struct 的幾個成員被用來維護這個組織. 因此, VMA 不是有一個驅動任意創(chuàng)建的, 否則這個結構破壞了. VMA 的主要成員是下面(注意在這些成員和我們剛看到的 /proc 輸出之間的相似)
unsigned long vm_start;unsigned long vm_end;
被這個 VMA 覆蓋的虛擬地址范圍. 這些成員是在 /proc/*/maps中出現(xiàn)的頭 2 個字段.
struct file *vm_file;
一個指向和這個區(qū)(如果有一個)關聯(lián)的 struct file 結構的指針.
unsigned long vm_pgoff;
文件中區(qū)的偏移, 以頁計. 當一個文件和設備被映射, 這是映射在這個區(qū)的第一頁的文件位置.
unsigned long vm_flags;
描述這個區(qū)的一套標志. 對設備驅動編寫者最感興趣的標志是 VM_IO 和 VM_RESERVUED. VM_IO 標志一個 VMA 作為內(nèi)存映射的 I/O 區(qū). 在其他方面, VM_IO 標志阻止這個區(qū)被包含在進程核轉儲中. VM_RESERVED 告知內(nèi)存管理系統(tǒng)不要試圖交換出這個 VMA; 它應當在大部分設備映射中設置.
struct vm_operations_struct *vm_ops;
一套函數(shù), 內(nèi)核可能會調(diào)用來在這個內(nèi)存區(qū)上操作. 它的存在指示內(nèi)存區(qū)是一個內(nèi)核"對象", 象我們已經(jīng)在全書中使用的 struct file.
void *vm_private_data;
驅動可以用來存儲它的自身信息的成員.
象 struct vm_area_struct, vm_operations_struct 定義于 <linux/mm.h>; 它包括下面列出的操作. 這些操作是唯一需要來處理進程的內(nèi)存需要的, 它們以被聲明的順序列出. 本章后面, 一些這些函數(shù)被實現(xiàn).
void (open)(struct vm_area_struct vma);
open 方法被內(nèi)核調(diào)用來允許實現(xiàn) VMA 的子系統(tǒng)來初始化這個區(qū). 這個方法被調(diào)用在任何時候有一個新的引用這個 VMA( 當生成一個新進程, 例如). 一個例外是當這個 VMA 第一次被 mmap 創(chuàng)建時; 在這個情況下, 驅動的 mmap 方法被調(diào)用來替代.
void (close)(struct vm_area_struct vma);
當一個區(qū)被銷毀, 內(nèi)核調(diào)用它的關閉操作. 注意沒有使用計數(shù)關聯(lián)到 VMA; 這個區(qū)只被使用它的每個進程打開和關閉一次.
struct page (nopage)(struct vm_area_struct vma, unsigned long address, int type);
當一個進程試圖存取使用一個有效 VMA 的頁, 但是它當前不在內(nèi)存中, nopage 方法被調(diào)用(如果它被定義)給相關的區(qū). 這個方法返回 struct page 指針給物理頁, 也許在從第 2 級存儲中讀取它之后. 如果 nopage 方法沒有為這個區(qū)定義, 一個空頁由內(nèi)核分配.
int (populate)(struct vm_area_struct vm, unsigned long address, unsigned long len, pgprot_t prot, unsigned long pgoff, int nonblock);
這個方法允許內(nèi)核"預錯"頁到內(nèi)存, 在它們被用戶空間存取之前. 對于驅動通常沒有必要來實現(xiàn)這個填充方法.
內(nèi)存管理難題的最后部分是進程內(nèi)存映射結構, 它保持所有其他數(shù)據(jù)結構在一起. 每個系統(tǒng)中的進程(除了幾個內(nèi)核空間幫助線程)有一個 struct mm_struct ( 定義在 <linux/sched.h>), 它含有進程的虛擬內(nèi)存區(qū)列表, 頁表, 和各種其他的內(nèi)存管理管理信息, 包括一個旗標( mmap_sem )和一個自旋鎖( page_table_lock ). 這個結構的指針在任務結構中; 在很少的驅動需要存取它的情況下, 通常的方法是使用 current->mm. 注意內(nèi)存關聯(lián)結構可在進程之間共享; Linux 線程的實現(xiàn)以這種方式工作, 例如.
這總結了我們對 Linux 內(nèi)存管理數(shù)據(jù)結構的總體. 有了這些, 我們現(xiàn)在可以繼續(xù) mmap 系統(tǒng)調(diào)用的實現(xiàn).
[47] 許多非-x86體系可以有效工作在沒有這里描述的內(nèi)核/用戶空間的劃分, 因此它們可以在 32-位系統(tǒng)使用直到 4-GB 內(nèi)核地址空間. 但是, 本節(jié)描述的限制仍然適用這樣的系統(tǒng)當安裝有多于 4GB 內(nèi)存時.
[48] BSS 的名子是來自一個老的匯編操作符的歷史遺物, 意思是"由符號開始的塊". 可執(zhí)行文件的 BSS 段不存儲在磁盤上, 并且內(nèi)核映射零頁到 BSS 地址范圍.
更多建議: