15.1. Linux 中的內(nèi)存管理

2018-02-24 15:50 更新

15.1.3.?高和低內(nèi)存

邏輯和內(nèi)核虛擬地址之間的不同在配備大量內(nèi)存的 32-位系統(tǒng)中被突出. 用 32 位, 可能尋址 4 G 內(nèi)存. 但是, 直到最近, 在 32-位 系統(tǒng)的 Linux 被限制比那個(gè)少很多的內(nèi)存, 因?yàn)樗⑻摂M地址的方式.

內(nèi)核( 在 x86 體系上, 在缺省配置里) 在用戶空間和內(nèi)核之間劃分 4-G 虛擬地址; 在 2 個(gè)上下文中使用同一套映射. 一個(gè)典型的劃分分出 3 GB 給用戶空間, 和 1 GB 給內(nèi)核空間. [47]內(nèi)核的代碼和數(shù)據(jù)結(jié)構(gòu)必須要適合這個(gè)空間, 但是內(nèi)核地址空間最大的消費(fèi)者是物理內(nèi)存的虛擬映射. 內(nèi)核不能直接操作沒有映射到內(nèi)核的地址空間. 內(nèi)核, 換句話說, 需要它自己的虛擬地址給任何它必須直接接觸的內(nèi)存. 因此, 多年來, 能夠被內(nèi)核處理的的最大量的物理內(nèi)存是能夠映射到虛擬地址的內(nèi)核部分的數(shù)量, 減去內(nèi)核代碼自身需要的空間. 結(jié)果, 基于 x86 的 Linux 系統(tǒng)可以工作在最多稍小于 1 GB 物理內(nèi)存.

為應(yīng)對更多內(nèi)存的商業(yè)壓力而不破壞 32-位 應(yīng)用和系統(tǒng)的兼容性, 處理器制造商已經(jīng)增加了"地址擴(kuò)展"特性到他們的產(chǎn)品中. 結(jié)果, 在許多情況下, 即便 32-位 處理器也能夠?qū)ぶ范嘤?4GB 物理內(nèi)存. 但是, 多少內(nèi)存可被直接用邏輯地址映射的限制還存在. 這樣內(nèi)存的最低部分(上到 1 或 2 GB, 根據(jù)硬件和內(nèi)核配置)有邏輯地址; 剩下的(高內(nèi)存)沒有. 在存取一個(gè)特定高地址頁之前, 內(nèi)核必須建立一個(gè)明確的虛擬映射來使這個(gè)也在內(nèi)核地址空間可用. 因此, 許多內(nèi)核數(shù)據(jù)結(jié)構(gòu)必須放在低內(nèi)存; 高內(nèi)存用作被保留為用戶進(jìn)程頁.

術(shù)語"高內(nèi)存"對有些人可能是疑惑的, 特別因?yàn)樗?PC 世界里有其他的含義. 因此, 為清晰起見, 我們將定義這些術(shù)語:

Low memory
邏輯地址在內(nèi)核空間中存在的內(nèi)存. 在大部分每個(gè)系統(tǒng)你可能會遇到, 所有的內(nèi)存都是低內(nèi)存.

High memory
邏輯地址不存在的內(nèi)存, 因?yàn)樗跒閮?nèi)核虛擬地址設(shè)置的地址范圍之外.

在 i386 系統(tǒng)上, 低和高內(nèi)存之間的分界常常設(shè)置在剛剛在 1 GB 之下, 盡管那個(gè)邊界在內(nèi)核配置時(shí)可被改變. 這個(gè)邊界和在原始 PC 中有的老的 640 KB 限制沒有任何關(guān)聯(lián), 并且它的位置不是硬件規(guī)定的. 相反, 它是, 內(nèi)核自身設(shè)置的一個(gè)限制當(dāng)它在內(nèi)核和用戶空間之間劃分 32-位地址空間時(shí).

我們將指出使用高內(nèi)存的限制, 隨著我們在本章遇到它們時(shí).

15.1.4.?內(nèi)存映射和 struct page

歷史上, 內(nèi)核已使用邏輯地址來引用物理內(nèi)存頁. 高內(nèi)存支持的增加, 但是, 已暴露這個(gè)方法的一個(gè)明顯的問題 -- 邏輯地址對高內(nèi)存不可用. 因此, 處理內(nèi)存的內(nèi)核函數(shù)更多在使用指向 struct page 的指針來代替(在 <linux/mm.h> 中定義). 這個(gè)數(shù)據(jù)結(jié)構(gòu)只是用來跟蹤內(nèi)核需要知道的關(guān)于物理內(nèi)存的所有事情.

2.6 內(nèi)核(帶一個(gè)增加的補(bǔ)丁)可以支持一個(gè) "4G/4G" 模式在 x86 硬件上, 它以微弱的性能代價(jià)換來更大的內(nèi)核和用戶虛擬地址空間.

系統(tǒng)中每一個(gè)物理頁有一個(gè) struct page. 這個(gè)結(jié)構(gòu)的一些成員包括下列:

atomic_t count;
這個(gè)頁的引用數(shù). 當(dāng)這個(gè) count 掉到 0, 這頁被返回給空閑列表.

void *virtual;
這頁的內(nèi)核虛擬地址, 如果它被映射; 否則, NULL. 低內(nèi)存頁一直被映射; 高內(nèi)存頁常常不是. 這個(gè)成員不是在所有體系上出現(xiàn); 它通常只在頁的內(nèi)核虛擬地址無法輕易計(jì)算時(shí)被編譯. 如果你想查看這個(gè)成員, 正確的方法是使用 page_address 宏, 下面描述.

unsigned long flags;
一套描述頁狀態(tài)的一套位標(biāo)志. 這些包括 PG_locked, 它指示該頁在內(nèi)存中已被加鎖, 以及 PG_reserved, 它防止內(nèi)存管理系統(tǒng)使用該頁.

有很多的信息在 struct page 中, 但是它是內(nèi)存管理的更深的黑魔法的一部分并且和驅(qū)動編寫者無關(guān).

內(nèi)核維護(hù)一個(gè)或多個(gè) struct page 項(xiàng)的數(shù)組來跟蹤系統(tǒng)中所有物理內(nèi)存. 在某些系統(tǒng), 有一個(gè)單個(gè)數(shù)組稱為 mem_map. 但是, 在某些系統(tǒng), 情況更加復(fù)雜. 非一致內(nèi)存存取( NUMA )系統(tǒng)和那些有很大不連續(xù)的物理內(nèi)存的可能有多于一個(gè)內(nèi)存映射數(shù)組, 因此打算是可移植的代碼在任何可能時(shí)候應(yīng)當(dāng)避免直接對數(shù)組存取. 幸運(yùn)的是, 只是使用 struct page 指針常常是非常容易, 而不用擔(dān)心它們來自哪里.

有些函數(shù)和宏被定義來在 struct page 指針和虛擬地址之間轉(zhuǎn)換:

struct page virt_to_page(void kaddr);
這個(gè)宏, 定義在 <asm/page.h>, 采用一個(gè)內(nèi)核邏輯地址并返回它的被關(guān)聯(lián)的 struct page 指針. 因?yàn)樗枰粋€(gè)邏輯地址, 它不使用來自 vmalloc 的內(nèi)存或者高內(nèi)存.

struct page *pfn_to_page(int pfn);
為給定的頁幀號返回 struct page 指針. 如果需要, 它在傳遞給 pfn_to_page 之前使用 pfn_valid 來檢查一個(gè)頁幀號的有效性.

void page_address(struct page page);
返回這個(gè)頁的內(nèi)核虛擬地址, 如果這樣一個(gè)地址存在. 對于高內(nèi)存, 那個(gè)地址僅當(dāng)這個(gè)頁已被映射才存在. 這個(gè)函數(shù)在 <linux/mm.h> 中定義. 大部分情況下, 你想使用 kmap 的一個(gè)版本而不是 page_address.

include <linux/highmem.h>void kmap(struct page page);void kunmap(struct page *page);

kmap 為系統(tǒng)中的任何頁返回一個(gè)內(nèi)核虛擬地址. 對于低內(nèi)存頁, 它只返回頁的邏輯地址; 對于高內(nèi)存, kmap 在內(nèi)核地址空間的一個(gè)專用部分中創(chuàng)建一個(gè)特殊的映射. 使用 kmap 創(chuàng)建的映射應(yīng)當(dāng)一直使用 kunmap 來釋放;一個(gè)有限數(shù)目的這樣的映射可用, 因此最好不要在它們上停留太長時(shí)間. kmap 調(diào)用維護(hù)一個(gè)計(jì)數(shù)器, 因此如果 2 個(gè)或 多個(gè)函數(shù)都在同一個(gè)頁上調(diào)用 kmap, 正確的事情發(fā)生了. 還要注意 kmap 可能睡眠當(dāng)沒有映射可用時(shí).

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 的一種高性能形式. 每個(gè)體系都給原子的 kmaps 維護(hù)一小列插口( 專用的頁表項(xiàng)); 一個(gè) kmap_atomic 的調(diào)用者必須在 type 參數(shù)中告知系統(tǒng)使用這些插口中的哪個(gè). 對驅(qū)動有意義的唯一插口是 KM_USER0 和 KM_USER1 (對于直接從來自用戶空間的調(diào)用運(yùn)行的代碼), 以及 KM_IRQ0 和 KM_IRQ1(對于中斷處理). 注意原子的 kmaps 必須被原子地處理; 你的代碼不能在持有一個(gè)時(shí)睡眠. 還要注意內(nèi)核中沒有什么可以阻止 2 個(gè)函數(shù)試圖使用同一個(gè)插口并且相互干擾( 盡管每個(gè) CPU 有獨(dú)特的一套插口). 實(shí)際上, 對原子的 kmap 插口的競爭看來不是個(gè)問題.

在本章后面和后續(xù)章節(jié)中當(dāng)我們進(jìn)入例子代碼時(shí), 我們看到這些函數(shù)的一些使用,

15.1.5.?頁表

在任何現(xiàn)代系統(tǒng)上, 處理器必須有一個(gè)機(jī)制來轉(zhuǎn)換虛擬地址到它的對應(yīng)物理地址. 這個(gè)機(jī)制被稱為一個(gè)頁表; 它本質(zhì)上是一個(gè)多級樹型結(jié)構(gòu)數(shù)組, 包含了虛擬-到-物理的映射和幾個(gè)關(guān)聯(lián)的標(biāo)志. Linux 內(nèi)核維護(hù)一套頁表即便在沒有直接使用這樣頁表的體系上.

設(shè)備驅(qū)動通??梢宰龅脑S多操作能涉及操作頁表. 幸運(yùn)的是對于驅(qū)動作者, 2.6 內(nèi)核已經(jīng)去掉了任何直接使用頁表的需要. 結(jié)果是, 我們不描述它們的任何細(xì)節(jié); 好奇的讀者可能想讀一下 Understanding The Linux Kernel 來了解完整的內(nèi)容, 作者是 Daniel P. Bovet 和 Marco Cesati (O' Reilly).

15.1.6.?虛擬內(nèi)存區(qū)

虛擬內(nèi)存區(qū)( VMA )用來管理一個(gè)進(jìn)程的地址空間的獨(dú)特區(qū)域的內(nèi)核數(shù)據(jù)結(jié)構(gòu). 一個(gè) VMA 代表一個(gè)進(jìn)程的虛擬內(nèi)存的一個(gè)同質(zhì)區(qū)域: 一個(gè)有相同許可標(biāo)志和被相同對象(如, 一個(gè)文件或者交換空間)支持的連續(xù)虛擬地址范圍. 它松散地對應(yīng)于一個(gè)"段"的概念, 盡管可以更好地描述為"一個(gè)有它自己特性的內(nèi)存對象". 一個(gè)進(jìn)程的內(nèi)存映射有下列區(qū)組成:

  • 給程序的可執(zhí)行代碼(常常稱為 text)的一個(gè)區(qū).

  • 給數(shù)據(jù)的多個(gè)區(qū), 包括初始化的數(shù)據(jù)(它有一個(gè)明確的被分配的值, 在執(zhí)行開始), 未初始化數(shù)據(jù)(BBS), [48]以及程序堆棧.

  • 給每個(gè)激活的內(nèi)存映射的一個(gè)區(qū)域.

一個(gè)進(jìn)程的內(nèi)存區(qū)可看到通過 /proc/<pid/maps>(這里 pid, 當(dāng)然, 用一個(gè)進(jìn)程的 ID 來替換). /proc/self 是一個(gè) /proc/id 的特殊情況, 因?yàn)樗3V府?dāng)前進(jìn)程. 作為一個(gè)例子, 這里是幾個(gè)內(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 

每個(gè)在 /proc/*/maps (出來映象的名子) 對應(yīng) struct vm_area_struct 中的一個(gè)成員:

start end
這個(gè)內(nèi)存區(qū)的開始和結(jié)束虛擬地址.

perm
帶有內(nèi)存區(qū)的讀,寫和執(zhí)行許可的位掩碼. 這個(gè)成員描述進(jìn)程可以對屬于這個(gè)區(qū)的頁做什么. 成員的最后一個(gè)字符要么是給"私有"的 p 要么是給"共享"的 s.

offset
內(nèi)存區(qū)在它被映射到的文件中的起始位置. 0 偏移意味著內(nèi)存區(qū)開始對應(yīng)文件的開始.

major minor
持有已被映射文件的設(shè)備的主次編號. 易混淆地, 對于設(shè)備映射, 主次編號指的是持有被用戶打開的設(shè)備特殊文件的磁盤分區(qū), 不是設(shè)備自身.

inode
被映射文件的 inode 號.

image
已被映射的文件名((常常在一個(gè)可執(zhí)行映象中).

15.1.6.1.?vm_area_struct 結(jié)構(gòu)

當(dāng)一個(gè)用戶空間進(jìn)程調(diào)用 mmap 來映射設(shè)備內(nèi)存到它的地址空間, 系統(tǒng)通過一個(gè)新 VMA 代表那個(gè)映射來響應(yīng). 一個(gè)支持 mmap 的驅(qū)動(并且, 因此, 實(shí)現(xiàn) mmap 方法)需要來幫助那個(gè)進(jìn)程來完成那個(gè) VMA 的初始化. 驅(qū)動編寫者應(yīng)當(dāng), 因此, 為支持 mmap 應(yīng)至少有對 VMA 的最少的理解.

讓我們看再 struct vm_area_struct 中最重要的成員( 在 <linux/mm.h> 中定義). 這些成員應(yīng)當(dāng)被設(shè)備驅(qū)動在它們的 mmap 實(shí)現(xiàn)中使用. 注意內(nèi)核維護(hù) VMA 的鏈表和樹來優(yōu)化區(qū)查找, 并且 vm_area_struct 的幾個(gè)成員被用來維護(hù)這個(gè)組織. 因此, VMA 不是有一個(gè)驅(qū)動任意創(chuàng)建的, 否則這個(gè)結(jié)構(gòu)破壞了. VMA 的主要成員是下面(注意在這些成員和我們剛看到的 /proc 輸出之間的相似)

unsigned long vm_start;unsigned long vm_end;
被這個(gè) VMA 覆蓋的虛擬地址范圍. 這些成員是在 /proc/*/maps中出現(xiàn)的頭 2 個(gè)字段.

struct file *vm_file;
一個(gè)指向和這個(gè)區(qū)(如果有一個(gè))關(guān)聯(lián)的 struct file 結(jié)構(gòu)的指針.

unsigned long vm_pgoff;
文件中區(qū)的偏移, 以頁計(jì). 當(dāng)一個(gè)文件和設(shè)備被映射, 這是映射在這個(gè)區(qū)的第一頁的文件位置.

unsigned long vm_flags;
描述這個(gè)區(qū)的一套標(biāo)志. 對設(shè)備驅(qū)動編寫者最感興趣的標(biāo)志是 VM_IO 和 VM_RESERVUED. VM_IO 標(biāo)志一個(gè) VMA 作為內(nèi)存映射的 I/O 區(qū). 在其他方面, VM_IO 標(biāo)志阻止這個(gè)區(qū)被包含在進(jìn)程核轉(zhuǎn)儲中. VM_RESERVED 告知內(nèi)存管理系統(tǒng)不要試圖交換出這個(gè) VMA; 它應(yīng)當(dāng)在大部分設(shè)備映射中設(shè)置.

struct vm_operations_struct *vm_ops;
一套函數(shù), 內(nèi)核可能會調(diào)用來在這個(gè)內(nèi)存區(qū)上操作. 它的存在指示內(nèi)存區(qū)是一個(gè)內(nèi)核"對象", 象我們已經(jīng)在全書中使用的 struct file.

void *vm_private_data;
驅(qū)動可以用來存儲它的自身信息的成員.

象 struct vm_area_struct, vm_operations_struct 定義于 <linux/mm.h>; 它包括下面列出的操作. 這些操作是唯一需要來處理進(jìn)程的內(nèi)存需要的, 它們以被聲明的順序列出. 本章后面, 一些這些函數(shù)被實(shí)現(xiàn).

void (open)(struct vm_area_struct vma);
open 方法被內(nèi)核調(diào)用來允許實(shí)現(xiàn) VMA 的子系統(tǒng)來初始化這個(gè)區(qū). 這個(gè)方法被調(diào)用在任何時(shí)候有一個(gè)新的引用這個(gè) VMA( 當(dāng)生成一個(gè)新進(jìn)程, 例如). 一個(gè)例外是當(dāng)這個(gè) VMA 第一次被 mmap 創(chuàng)建時(shí); 在這個(gè)情況下, 驅(qū)動的 mmap 方法被調(diào)用來替代.

void (close)(struct vm_area_struct vma);
當(dāng)一個(gè)區(qū)被銷毀, 內(nèi)核調(diào)用它的關(guān)閉操作. 注意沒有使用計(jì)數(shù)關(guān)聯(lián)到 VMA; 這個(gè)區(qū)只被使用它的每個(gè)進(jìn)程打開和關(guān)閉一次.

struct page (nopage)(struct vm_area_struct vma, unsigned long address, int type);
當(dāng)一個(gè)進(jìn)程試圖存取使用一個(gè)有效 VMA 的頁, 但是它當(dāng)前不在內(nèi)存中, nopage 方法被調(diào)用(如果它被定義)給相關(guān)的區(qū). 這個(gè)方法返回 struct page 指針給物理頁, 也許在從第 2 級存儲中讀取它之后. 如果 nopage 方法沒有為這個(gè)區(qū)定義, 一個(gè)空頁由內(nèi)核分配.

int (populate)(struct vm_area_struct vm, unsigned long address, unsigned long len, pgprot_t prot, unsigned long pgoff, int nonblock);
這個(gè)方法允許內(nèi)核"預(yù)錯(cuò)"頁到內(nèi)存, 在它們被用戶空間存取之前. 對于驅(qū)動通常沒有必要來實(shí)現(xiàn)這個(gè)填充方法.

15.1.7.?進(jìn)程內(nèi)存映射

內(nèi)存管理難題的最后部分是進(jìn)程內(nèi)存映射結(jié)構(gòu), 它保持所有其他數(shù)據(jù)結(jié)構(gòu)在一起. 每個(gè)系統(tǒng)中的進(jìn)程(除了幾個(gè)內(nèi)核空間幫助線程)有一個(gè) struct mm_struct ( 定義在 <linux/sched.h>), 它含有進(jìn)程的虛擬內(nèi)存區(qū)列表, 頁表, 和各種其他的內(nèi)存管理管理信息, 包括一個(gè)旗標(biāo)( mmap_sem )和一個(gè)自旋鎖( page_table_lock ). 這個(gè)結(jié)構(gòu)的指針在任務(wù)結(jié)構(gòu)中; 在很少的驅(qū)動需要存取它的情況下, 通常的方法是使用 current->mm. 注意內(nèi)存關(guān)聯(lián)結(jié)構(gòu)可在進(jìn)程之間共享; Linux 線程的實(shí)現(xiàn)以這種方式工作, 例如.

這總結(jié)了我們對 Linux 內(nèi)存管理數(shù)據(jù)結(jié)構(gòu)的總體. 有了這些, 我們現(xiàn)在可以繼續(xù) mmap 系統(tǒng)調(diào)用的實(shí)現(xiàn).

[47] 許多非-x86體系可以有效工作在沒有這里描述的內(nèi)核/用戶空間的劃分, 因此它們可以在 32-位系統(tǒng)使用直到 4-GB 內(nèi)核地址空間. 但是, 本節(jié)描述的限制仍然適用這樣的系統(tǒng)當(dāng)安裝有多于 4GB 內(nèi)存時(shí).

[48] BSS 的名子是來自一個(gè)老的匯編操作符的歷史遺物, 意思是"由符號開始的塊". 可執(zhí)行文件的 BSS 段不存儲在磁盤上, 并且內(nèi)核映射零頁到 BSS 地址范圍.

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號