第九章 虛擬存儲器
虛擬存儲器是計算機系統最重要的概念之一。
虛擬存儲器(Virtual Memory),是硬件異常,硬件地址翻譯,主存,磁盤文件和內核軟件的完善交互,它為每個進程提供一個大的,一致的,私有地址空間。通過一個清晰的機制,虛擬存儲器提供了三個要重的能力:
- 它將主存看成是一個存儲在磁盤上的地址空間的高速緩存,在主存中只保存活動區域,並根據需要在磁盤和主存之間來回傳送數據,通過這種方式,高效的使用了主存
- 它為每個進程提供了一致的地址空間,從而簡化了存儲器管理
- 它保護了每個進程的地址空間不被其他進程破壞
9.1 物理和虛擬尋址
1.計算機系統的主存被組織成一個由M個連續的字節大小的單元組成的數組。每字節都有一個惟一的物理地址。

2.早期的PC使用物理尋址,而且諸如數字信號處理器,嵌入式微控制器以及Cray超級計算機這樣的系統仍然繼續使用這種尋址方式。而為通用計算機設計師的現代處理器使用的是虛擬尋址。使用虛擬尋址時,CPU通過生成一個虛擬地址來訪問主存,這個虛擬地址在被送到存儲器之前先轉換成適當的物理地址(這個過程叫做地址翻譯,相關硬件為存儲器管理單元MMU)

9.2 地址空間
1.地址空間是一個非負整數地址的有序集合:{0,1,2,...},如果地址是連續的,則稱為線性地址空間。如果計算機有n位地址總線,在一個帶有虛擬存儲器的系統中,CPU從一個有N = 2n個地址的地址空間中生成虛擬地址,這個地址稱為虛擬地址空間:{0,1,2,...,N-1}
2.一個地址空間的大小是由表示最大地址所需要的位數來描述的。例如:N=2^n:n位地址空間
3.一個系統還有一個物理地址空間,它與系統中物理存儲器的M個字節相對應:{0,1,2,...M-1}。M不要求是2的冪。
4.主存中的每個字節都有一個選自虛擬地址空間的虛擬地址和一個選自物理地址空間的物理地址。
9.3 虛擬存儲器作為緩存的工具
概念上言,虛擬存儲器被組織為一個由存放在磁盤上的N個連續的字節大小的單元組成的數組。每字節都有一個惟一的虛擬地址,這個惟一的虛擬地址是作為到數組的索引的。磁盤上數組的內容被緩存到主存中。和存儲器層次結構中其他緩存一樣,磁盤(較低層)上的數據被分成塊,這些塊作為磁盤和主存(較高層)之間的傳輸單元。VM系統通過將虛擬存儲器分割成稱為虛擬頁(virtual page,VP)的大小固定的塊,來處理這個問題。每個虛擬頁的大小為P = 2p字節。類似地,物理存儲器被分割成物理頁(physical page,PP),大小也為P字節(物理頁也被稱為頁幀,page frame)。
在任意時刻,虛擬頁面的集合都被分為三個不相交的子集:
- 未分配的:VM系統還沒分配/創建的頁,不占用任何磁盤空間。
- 緩存的:當前緩存在物理存儲器中的已分配頁
- 未緩存的:沒有緩存在物理存儲器中的已分配頁

1.DRAM緩存的組織結構
- 不命中處罰很大
- 是全相聯的——任何虛擬頁都可以放在任何的物理頁中。
- 替換算法精密
- 總是使用寫回而不是直寫。
2.頁表
存放在物理存儲器一個數據結構,叫做頁表(page table)。頁表將虛擬頁映射到物理頁。
頁表就是一個PTE(page table entry,頁表條目)的數組。虛擬地址空間中的每個頁在頁表中的一個固定偏移量處都有一個PTE。
NULL:未分配。
VP3,VP5:已分配,但是還未被緩存。
VP1:已分配,已緩存。

3.頁命中
- 缺頁:就是指DRAM緩存不命中。
- 缺頁異常:會調用內核中的缺頁異常處理程序,選擇一個犧牲頁。
- 頁:虛擬存儲器的習慣說法,就是塊
- 交換=頁面調度:磁盤和存儲器之間傳送頁的活動
- 按需頁面調度:直到發生不命中時才換入頁面的策略,所有現代系統都使用這個。
4.缺頁
DRAM緩存不命中稱為缺頁。
局部性原則保證了在任意時刻,程序將往往在一個較小的活動頁面集合上工作,這個集合叫做工作集/常駐集。
顛簸:工作集大小超出了物理存儲器的大小。
9.4 虛擬存儲器作為存儲器管理的工具


- 操作系統為每個進程提供了一個獨立的頁表,也就是一個獨立的虛擬地址空間。
- 抖個虛擬頁面可以映射到同一個共享物理頁面上。
- 存儲器映射:將一組連續的虛擬頁映射到任意一個文件中的任意位置的表示法。
VM簡化了鏈接和加載、代碼和數據共享,以及應用程序的存儲器分配。
9.5 虛擬存儲器作為存儲器保護的工具

PTE的三個許可位:
- SUP:表示進程是否必須運行在內核模式下才能訪問該頁
- READ:讀權限
- WRITE:寫權限
9.6 地址翻譯

1.地址翻譯
地址翻譯是一個N元素的虛擬地址空間(VAS)中的元素和一個M元素的物理地址空間(PAS)中元素之間的映射。
2.頁表基址寄存器
CPU中的一個控制寄存器,叫做頁表基址寄存器(page table base register,PTBR)指向當前頁表。N位的虛擬地址包含兩個部分:一個p位的VPO(virtual page offset,虛擬頁面偏移)和一個n-p位的VPN(virtual page number)。MMU利用VPN來選擇適當的PTE。如VPN0選擇PTE0。因為物理和虛擬頁面都是p字節的,所以PPO(physical page offset物理頁面偏移)和VPO是相同的,所以,將頁表條目中PPN(physical page number,物理頁號)和虛擬地址中的VPO串聯起來,就是相應的物理地址。

3.頁面命中完全由硬件處理的,而處理缺頁要求硬件和OS內核協作完成。
4.結合高速緩存和虛擬存儲器
大多數系統是選擇物理尋址的方式來訪問高速緩存。使用物理尋址,多個進程同時在高速緩存中有存儲塊和共享來自相同虛擬頁面的塊成為簡單的事情。而且,高速緩存無需處理保護的問題,因為訪問權限的檢查是地址翻譯過程中一部分。
5.利用TLB加速地址翻譯
在MMU中包括一個關於PTE的小的緩存,稱為TLB。TLB是一個小的,虛擬尋址的緩存,其中每一行都保存着一個由單個PTE組成的塊。


6.多級頁表
多級頁表——采用層次結構,用來壓縮頁表。
(1)以兩層頁表層次結構為例,好處是:
如果一級頁表中的一個PTE是空的,那么相應的二級頁表就根本不會存在
只有一級頁表才需要總是在主存中,虛擬存儲器系統可以在需要時創建、頁面調入或調出二級頁表,只有最經常使用的二級頁表才緩存在主存中。
(2)多級頁表的地址翻譯:

9.7 案例研究:Intel Core i7/Linux存儲器系統
處理器包:四個核、一個大的所有核共享的L3高速緩存和一個DDR3存儲器控制器。

一、 Core i7地址翻譯
二、Linux虛擬存儲器系統
Linux為每個進程維持了一個單獨的虛擬地址空間。內核存儲器包含內核中的代碼和數據結構。一部分區域映射到所有進程共享的物理頁面另一部分包含每個進程都不相同的數據。
1.Linux虛擬存儲器區域
區域:就是已分配的虛擬存儲器的連續片,這些頁是相關聯的。
每個存在的虛擬頁面都保存在某個區域中。內核為系統中的每個進程維護一個單獨的任務結構task_struct:

一個具體區域的區域結構包括:
•vm_start:指向起始處
•vm_end:指向結束處
•vm_prot:描述這個區域包含的所有頁的讀寫許可權限
•vm_flags:是共享的還是私有的
•vm_next:指向下一個區域
2.Linux缺頁異常處理
(1)虛擬地址A是否合法?
不合法,觸發段錯誤,終止進程
合法,進入下一條
(2)存儲器訪問是否合法?即,是否有權限?
不合法,觸發保護異常,終止程序
合法,進入下一條
(3)這時,是針對合法的虛擬地址進行合法的操作。所以:選擇一個犧牲頁面,如果被修改過就換新的並更新頁表。
9.8 存儲器映射
Linux通過將一個虛擬存儲器區域與一個磁盤上的對象關聯起來,以初始化這個虛擬存儲器區域的內容的過程叫做存儲器映射。
映射對象:
1.Unix文件系統中的普通文件 2.匿名文件(由內核創建,全都是二進制0)
一、共享對象和私有對象
-
共享對象
• 共享對象對於所有把它映射到自己的虛擬存儲器進程來說都是可見的 • 即使映射到多個共享區域,物理存儲器中也只需要存放共享對象的一個拷貝。 -
私有對象
• 私有對象運用的技術:寫時拷貝 • 在物理存儲器中只保存有私有對象的一份拷貝fork函數就是應用了寫時拷貝技術,至於execve函數:
二、使用mmap函數的用戶級存儲器映射
1.創建新的虛擬存儲器區域
#include <unistd.h> #include <sys/mman.h> void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset); 成功返回指向映射區域的指針,若出錯則為-1
參數含義:
• start:這個區域從start開始
• fd:文件描述符
• length:連續的對象片大小
• offset:距文件開始處的偏移量
• prot:訪問權限位,具體如下:
• PROT_EXEC:由可以被CPU執行的指令組成 • PROT_READ:可讀 • PROT_WRITE:可寫 PROT_NONE:不能被訪問 • flag:由描述被映射對象類型的位組成,具體如下: •MAP_ANON:匿名對象,虛擬頁面是二進制0 •MAP_PRIVATE:私有的、寫時拷貝的對象 MAP_SHARED:共享對象
2.刪除虛擬存儲器:
include include <sys/mman.h> int munmap(void *start, size_t length); 成功返回0,失敗返回-1
從start開始刪除,由接下來length字節組成的區域。
9.9 動態存儲器分配
-
堆:動態存儲器分配維護着一個進程的虛擬存儲器區域,稱為堆。
假設它是一個請求二進制0的區域,緊接在未初始化的bss區域后開始,並向上(更高的地址)生長。有一個變量brk指向堆的頂部 -
分配器的兩種基本風格:
a.顯示分配器-malloc和free b.隱式分配器/垃圾收集器一、malloc和free函數:
-
系統調用malloc函數,從堆中分配塊:
#include <stdlib.h> void *malloc(size_t size); 返回:成功則為指針,指向大小至少為size字節的存儲器塊,失敗則為NULL -
系統調用free函數來釋放已分配的堆塊:
#include <stdlib.h> void free(void *ptr); 無返回值ptr參數必須指向一個從malloc、calloc或者reallov獲得的已分配塊的起始位置。
二、為什么要使用動態存儲器分配
因為經常知道程序實際運行時,它們才知道某些數據結構的大小。
三、分配器的要求和目標:
-
約束條件
• 處理任意請求序列 • 立即響應請求 • 只使用堆 • 對齊塊 • 不修改已分配的塊 -
目標:
• 最大化吞吐率(吞吐率:每個單位時間里完成的請求數) • 最大化存儲器利用率——峰值利用率最大化四、碎片
雖然有未使用的存儲器,但是不能用來滿足分配請求時,發生這種現象。
-
內部碎片
發生在一個已分配塊比有效載荷大的時候。
易於量化。
-
外部碎片
發生在當空閑存儲器合計起來足夠滿足一個分配請求,但是沒有一個單獨的空間塊足以處理這個請求時發生
難以量化,不可預測。
五、實現問題
一個實際的分配器要在吞吐率和利用率之間把握好平衡,需要考慮:
空閑快組織、放置、分割、合並
六、隱式空閑鏈表
-
堆塊的格式:由一個字的頭部,有效荷載,和可能的額外填充組成。
-
將堆組織成一個連續的已分配塊和空閑塊的序列:

-
空閑塊通過頭部中的大小字段隱含地連接着,分配器可以通過遍歷堆中所有的塊,從而間接地遍歷整個空閑塊的集合。
需要:特殊標記的結束塊。
-
系統對齊要求和分配器對塊格式的選擇會對分配器上的最小塊大小有強制的要求。
七、放置已分配的塊——放置策略
-
首次適配
從頭開始搜索空閑鏈表,選擇第一個合適的空閑塊
-
下一次適配
從上一次搜索的結束位置開始搜索
-
最佳適配
檢索每個空閑塊,選擇適合所需請求大小的最小空閑塊
八、分割空閑塊

九、獲取額外的堆存儲器
用到sbrk函數:
#include
vid *sbrk(intptr_t incr);
成功則返回舊的brk指針,出錯為-1
通過將內核的brk指針增加incr來擴展和收縮堆。
十、合並空閑塊
合並是針對於假碎片問題的,任何實際的分配器都必須合並相鄰的空閑塊。
有兩種策略:
• 立即合並
• 推遲合並
十一、帶邊界的合並
這個合並的意思是,因為頭部的存在,所以向后合並是簡單的,但是向前合並是不方便的,所以在塊的最后加一個腳部,作為頭部的副本,就方便了合並,具體四種情況如下:

空閑塊總是需要腳部的。
十二、實現簡單的分配器
注意:
• 序言塊和結尾塊:序言塊是初始化時創建的,而且永不釋放;結尾塊是一個特殊的塊,總是以它為結束。
• 有一個技巧,就是將重復使用的,操作復雜又有重復性的,這些可以定義成宏,方便使用也方便修改。
• 需要注意強制類型轉換,尤其是帶指針的,非常復雜。
• 因為規定了字節對齊方式為雙字,就代表塊的大小是雙字的整數倍,不是的舍入到是。
十三、顯式空閑鏈表
- 區別
(1)分配時間
隱式的,分配時間是塊總數的線性時間
但是顯式的,是空閑塊數量的線性時間。
(2)鏈表形式
隱式——隱式空閑鏈表
顯式——雙向鏈表,有前驅和后繼,比頭部腳部好使。
-
排序策略:
• 后進先出 • 按照地址順序維護
十四、分離的空閑鏈表
-
分離存儲,是一種流行的減少分配時間的方法。一般思路是將所有可能的塊大小分成一些等價類/大小類。
-
分配器維護着一個空閑鏈表數組,每個大小類一個空閑鏈表,按照大小的升序排列。
-
有兩種基本方法:
1.簡單分離存儲
每個大小類的空閑鏈表包含大小相等的塊,每個塊的大小就是這個大小類中最大元素的大小。
(1)操作
如果鏈表非空:分配其中第一塊的全部
如果鏈表為空:分配器向操作系統請求一個固定大小的額外存儲器片,將這個片分成大小相等的塊,並且連接起來成為新的空閑鏈表。
(2)優缺點
優點:時間快,開銷小
缺點:容易造成內部、外部碎片
2.分離適配
每個空閑鏈表是和一個大小類相關聯的,並且被組織成某種類型的顯示或隱式鏈表,每個鏈表包含潛在的大小不同的塊,這些塊的大小是大小類的成員。
這種方法快速並且對存儲器使用很有效率。
3.伙伴系統——分離適配的特例
其中每個大小類都是2的冪
這樣,給定地址和塊的大小,很容易計算出它的伙伴的地址,也就是說:一個塊的地址和它的伙伴的地址只有一位不同。
優點:快速檢索,快速合並。
9.10 垃圾收集
垃圾收集器是一種動態存儲分配器,它自動釋放程序不再需要的已分配塊,這些塊被稱為垃圾,自動回收堆存儲的過程叫做垃圾收集。
一、基本知識
垃圾收集器將存儲器視作一張有向可達圖,只有當存在一條從任意根節點出發並到達p的有向路徑時,才說節點p是可達的,而不可達點就是垃圾。
二、Mark&Sweep垃圾收集器
-
有兩個階段:
• 標記:標記出根節點的所有可達的和已分配的后繼 • 清楚:釋放每個未被標記的已分配塊。 -
相關函數:
ptr定義為typedef void *ptr
• ptr isPtr(ptr p):如果p指向一個已分配塊中的某個字,那么就返回一個指向這個塊的起始位置的指針b,否則返回NULL • int blockMarked(ptr b):如果已經標記了塊b,那么就返回true • int blockAllocated(ptr b):如果塊b是已分配的,那么久返回ture • void markBlock(ptr b):標記塊b • int length(ptr b):返回塊b的以字為單位的長度,不包括頭部 • void unmarkBlock(ptr b):將塊b的狀態由已標記的改為未標記的 • ptr nextBlock(ptr b):返回堆中塊b的后繼
三、C保守的Mark&Sweep
——平衡二叉樹
C的Mark&Sweep收集器必須是保守的,根本原因是C語言不會用類型標記來標記存儲器位置。
9.11 C程序中常見的與存儲器有關的錯誤
-
間接引用壞指針
常見錯誤——scanf錯誤
-
讀未初始化的存儲器
常見錯誤——假設堆存儲器被初始化為0
-
允許棧緩沖區溢出
常見錯誤——緩沖區溢出錯誤
-
假設指針和它們指向的對象是相同大小的
在遠處起作用action at distance
-
造成錯位錯誤
-
引用指針,而不是它所指向的對象
-
誤解指針運算
-
引用不存在的變量
-
引用空堆塊中的數據
-
引起存儲器泄露
