9.虛擬存儲器 為了更加有效地管理 存儲器 且少出錯,現代系統提供了對 主存 的抽象概念,叫做 。 是硬體異常,硬體地址翻譯,主存,磁碟文件和內核軟體的完美交互。 為每個進程提供一個 大的 , 一致的 和 私有的 地址空間。 提供了3個重要能力。 將主存看成磁碟地址空間的 高速緩存 。 只保留了活動 ...
9.虛擬存儲器
為了更加有效地管理存儲器且少出錯,現代系統提供了對主存的抽象概念,叫做虛擬存儲器(VM)
。
虛擬存儲器
是硬體異常,硬體地址翻譯,主存,磁碟文件和內核軟體的完美交互。- 為每個進程提供一個大的,一致的和 私有的地址空間。
- 提供了3個重要能力。
- 將主存看成磁碟地址空間的高速緩存。
- 只保留了活動區域,並根據需要在磁碟和主存間來回傳送數據,高效使用主存。
- 為每個進程提供一致的地址空間
- 簡化存儲器管理
- 保護了每個進程的地址空間不被其他進程破壞。
- 將主存看成磁碟地址空間的高速緩存。
- 程式員為什麼要理解它?
虛擬存儲器
是中心的。- 遍佈在電腦系統所有層次,硬體異常,彙編器,連接器,載入器,共用對象,文件和進程中扮演重要角色。
虛擬存儲器
是強大的。- 可以創建和銷毀存儲器片(chunk)
- 將存儲器片映射到磁碟文件的某個部分。
- 其他進程共用存儲器。
- 例子
- 能讀寫存儲器位置來修改磁碟文件內容。
- 載入文件到存儲器不需要顯式的拷貝。
虛擬存儲器
是危險的- 引用變數,間接引用指正,調用
malloc
動態分配程式,就會和虛擬存儲器交互。 - 如果使用不當,將遇到複雜危險的與存儲器有關的錯誤。
- 例子
- 一個帶有錯誤指針的程式可以立即崩潰於段錯誤或者保護錯誤。
- 運行完成,卻不產生正確結果。
- 引用變數,間接引用指正,調用
- 本章從兩個角度分析。
虛擬存儲器
如何工作。- 應用程式如何使用和管理
虛擬存儲器
。
9.1 物理與虛擬定址
物理地址(Physical Address,PA)
:電腦系統的主存被組織為M個連續的位元組大小的單元組成的數組。每個位元組的地址叫物理地址
.CPU訪問存儲器的最自然的方式使用
物理地址
,這種方式稱為物理定址
。
- 早期的PC,數字信號處理器,嵌入式微控制器以及Cray超級電腦使用物理定址
。現代處理器使用的是
虛擬定址(virtual addressing)
的定址形式。- CPU通過生成一個
虛擬地址(Virtual address,VA)
來訪問主存。- 將
虛擬地址
轉換為物理地址
叫做地址翻譯(address translation)
。
- 將
地址翻譯
也需要CPU硬體和操作系統之間的緊密結合。- CPU晶元上有叫做
存儲器管理單元(Memory Management Unit,MMU)
的專用硬體。- 利用存儲在主存中的查詢表來動態翻譯虛擬地址。
- 查詢表由操作系統管理。
- CPU晶元上有叫做
- CPU通過生成一個
9.2 地址空間
地址空間(address space)
是一個非負整數地址
的有序集合。
- 如果地址空間中整數是連續的,我們說它是
線性地址空間(linear address space)
。- 為了簡化討論,我們總是假設使用線性地址空間。
在一個帶虛擬存儲器的系統中,CPU從一個有
N=2^n
個地址的地址空間
中生成虛擬地址,這個地址空間稱為虛擬地址空間(virtual address space)
。- 一個
地址空間
大小是由表示最大地址所需要的位數來描述的。- 如
N=2^n
個地址的虛擬地址空間叫做n
位地址空間。 - 現在操作系統支持
32位
或64位
。
- 如
一個系統還有
物理地址空間
,它與系統中物理存儲器的M=2^m
(假設為2的冪)個位元組相對應。
地址空間
的概念很重要,因為它區分了數據對象(位元組)和 它們的屬性(地址)。
- 每個
位元組(數據對象)
一般有多個 獨立的地址(屬性)
。每個地址都選自不同的地址空間。- 比如一般來說。
位元組
有一個在虛擬地址空間
的虛擬地址
。- 還有一個在
物理地址空間
的物理地址
。 - 兩個地址都能訪問到這個
位元組
。
- 類似現實世界的門牌號, 和經緯度。
- 比如一般來說。
9.3 虛擬存儲器作為緩存的工具
在講述這一小章之前,必須交代一下我對
虛擬存儲器
概念的存疑。
原本我以為虛擬存儲器
=虛擬記憶體
。
以下是虛擬記憶體
的定義
虛擬記憶體
是電腦系統記憶體管理的一種技術。它使得應用程式認為它擁有連續的可用的記憶體(一個連續完整的地址空間),而實際上,它通常是被分隔成多個物理記憶體碎片
,還有部分暫時存儲在外部磁碟存儲器
上,在需要時進行數據交換而在下麵的定義我們可以看到
CSAPP
中認為虛擬存儲器
是存放在磁碟上的。
在此,我們姑且當做兩者是不同的東西,以後有更深刻的理解,再思考。
虛擬存儲器(VM)
被組織為一個存放在磁碟上的N個連續位元組大小的單元組成的數組。
- 每個位元組都有一個唯一的
虛擬地址
,這個虛擬地址作為到數組的索引。 磁碟
上數組的內容被緩存到主存
中。- 同存儲器層次結構其他緩存一樣,
磁碟
上的數據被分割稱塊
。- 這些
塊
作為磁碟和主存之間的傳輸單元。 虛擬頁(Virtual Page,VP)
就是這個塊
- 每個
虛擬頁
大小為P=2^p
位元組。
- 每個
- 這些
- 物理存儲器被分割為
物理頁
,大小也為P
位元組- 也被稱為
頁幀(page frame)
- 也被稱為
- 同存儲器層次結構其他緩存一樣,
- 任何時候,
虛擬頁
的集合都被分為3個不相交的子集。- 未分配的:VM系統還未分配(或者創建)的頁。未分配的
塊
沒有任何數據與之相關聯。- 不占用磁碟空間
- 通過
malloc
來分配
- 緩存的:當前緩存在物理存儲器的已分配頁。
- 未緩存的:沒有緩存在物理頁面存儲器中的已分配頁。
- 未分配的:VM系統還未分配(或者創建)的頁。未分配的
9.3.1 DRAM緩存的組織結構
DRAM
表示虛擬存儲器系統的緩存,在主存中緩存虛擬頁
,有兩個特點。
DRAM
緩存不命中處罰十分嚴重。- 因為
磁碟
比DRAM
慢100000多倍。
- 因為
- 訪問一位元組開銷
- :從一個磁碟的一個扇區讀取第一個位元組的時間開銷要比從該扇區中讀連續的位元組慢大約100000倍
DRAM
緩存的組織結構由這種巨大的不命中開銷驅動。因此有以下特點。
(有些地方不是特別懂,之後看完第六章應該會好點)
虛擬頁
往往很大。- 4KB~2MB
- 訪問一位元組開銷的原因才要這麼大。
DRAM
緩存是全相聯
- 也就是: 任何
虛擬頁
都能放在任何物理頁
中。 - 原因在於大的不命中懲罰
- 也就是: 任何
- 更精密的替換演算法
- 替換錯了虛擬頁的懲罰很高。
DRAM
緩存總是寫回
- 因為對磁碟的訪問時間很長
- 而不用
直寫
9.3.2 頁表
判斷命中和替換又多種軟硬體聯合提供。
- 操作系統軟體,MMU中的地址翻譯硬體和
頁表(page table)
。頁表
是存放在物理存儲器的數據結構。頁表
將虛擬頁映射到物理頁。- 地址翻譯硬體將虛擬地址轉換為物理地址都會讀取
頁表
。
- 操作系統負責維護
頁表
的內容,以及磁碟及DRAM之間來回傳送頁。
頁表
就是一個頁表條目(Page Table Entry,PTE)
的數組.- 虛擬地址空間 中每個頁在頁表的固定偏移量處都有一個
PTE
. - 每個
PTE
由一個有效位
和n位地址欄位
。有效位
表明虛擬頁是否被緩存。- 如果有效位存在,那麼地址欄位指向對應的物理存儲器。
- 如果有效位不存在。
- 地址欄位要麼為NULL
- 要麼指向虛擬頁在磁碟所在的位置。
- 虛擬地址空間 中每個頁在頁表的固定偏移量處都有一個
9.3.3 頁命中
- 一個頁命中的過程。
- 一個虛擬地址轉換為物理地址的過程。
9.3.4 缺頁
在虛擬存儲器的習慣說法中,DRAM緩存不命中稱為缺頁
。
處理過程如下:
- 讀取虛擬地址所指向的
PT
。 - 讀取
PTE
有效位,發現未被緩存,觸發缺頁異常。 - 調用缺頁異常處理程式
- 選擇犧牲頁。
- 如果犧牲頁發生了改變,將其拷貝回磁碟(因為是
寫回
) - 需要讀取的頁代替了犧牲頁的位置。
- 結果:犧牲也不被緩存,需要讀取的頁被緩存。
- 中斷結束,重新執行最開始的指令。
- 在
DRAM
中讀取成功。
虛擬存儲器
是20世紀60年代發明的,因此即使與SRAM緩存使用了不同的術語。
塊
被稱為頁
。- 磁碟和DRAM之間傳送
頁
的活動叫做交換(swapping)
或者頁面調度(paging)
。 - 有
不命中
發生時,才換入頁面,這種策略叫做按需頁面調度(demand paging)
。- 現代系統基本都是用這個。
9.3.5 分配頁面
比如某個頁面
所指向地址為NULL
,將這個地址指向磁碟某處,那麼這就叫分配頁面
。
此時虛擬頁
從未分配
狀態 變為 未緩存
。
9.3.6 又是局部性拯救了我們
虛擬存儲器
工作的相當好,主要歸功於老朋友局部性(locality)
儘管從頭到尾的活動頁面數量大於物理存儲器大小。
但是在局部內,程式往往在一個較小的活動頁面集合工作
- 這個集合叫做
工作集(working set)
或者叫常駐集(resident set)
- 初始載入開銷比較大。
- 程式有良好的
時間局部性
,虛擬存儲器
都工作的相當好。 - 如果程式實在很爛,或者物理空間很小,
工作集
大於物理存儲器
大小。這種狀態叫顛簸(thrashing)
.- 這時,頁面不斷換進換出。性能十分低。
統計缺頁次數
可以利用Unix的getrusage
函數檢測缺頁數量。
9.4 虛擬存儲器作為存儲器的管理工具
實際上,操作系統為每個進程提供一個獨立的頁表
。
因此,VM
簡化了鏈接
和載入
,代碼
和數據共用
,以及應用程式的存儲器
分配。
- 簡化鏈接
- 獨立的空間地址意味著每個進程的存儲器映像使用相同的格式。
- 文本節總是從
0x08048000
(32位)處或0x400000
(64位)處開始。 - 然後是數據,bss節,棧。
- 文本節總是從
- 一致性極大簡化了
鏈接器
的設計和實現。
- 獨立的空間地址意味著每個進程的存儲器映像使用相同的格式。
簡化載入
載入器
可以從不實際拷貝任何數據從磁碟到存儲器。基本都是虛擬存儲系統完成。
將一組連續的
虛擬頁
映射到任意一個文件中的任意位置的表示法稱作存儲器映射
。Unix提供一個稱為mmap
的系統調用,允許程式自己做存儲器映射。在9.8詳細講解。
簡化共用
- 獨立地址空間為操作系統提供了一個管理用戶進程和操作系統自身之間的一致
共用
機制. - 例子
- 操作相同的操作系統內核代碼
- C標準庫的
printf
.
- 因此操作系統需要將不同進程的適當的虛擬頁映射到相同的物理頁面。
- 多個進程共用這部分代碼的一個拷貝。
- 而不是每個進程都要載入單獨的內核和C標準庫的拷貝。
- 獨立地址空間為操作系統提供了一個管理用戶進程和操作系統自身之間的一致
- 簡化存儲器分配.
- 即
虛擬頁
連續(虛擬頁還是單獨的),物理頁
可以不連續。使得分配更加容易。
- 即
9.5 虛擬存儲器作為存儲器保護的工具
任何現代操作系統必須為操作系統提供手段來控制對 存儲器系統的訪問。
- 不應該允許用戶進程修改它的只讀文本段。
- 不允許它讀或修改任何內核的代碼和數據結構
- 不允許讀寫其他進程的私有存儲器。
- 不允許修改共用的虛擬頁,除非所有共用者顯示允許這麼做(通過調用明確的進程間通信)
方式:在PTE
上添加一些格外的許可位
來控制訪問。
SUP
:是否只有在內核模式下才能訪問?READ
:讀許可權。WRITE
:寫許可權。
如果指令違反了許可條件,觸發一般保護性異常,然後交給異常處理程式,Shell
一般會報告為段錯誤(segmentaion fault)
。
9.6 地址翻譯
認識到硬體在支持虛擬存儲器中的角色
以下是接下來可能要用到的符號,作參考。
形式上來說,地址翻譯是一個N元素的虛擬地址空間(
VAS
)中的元素和一個M元素的物理地址空間(PAS
)元素之間的映射,以下展示了
MMU
(Memory Management Unit
,存儲器管理單元)如何利用頁表實現這樣的功能頁表基址寄存器(Page Table Base Register,PTBR)
指向當前頁表。n
位的虛擬地址
包含兩個部分- 一個
p
位的虛擬頁面偏移(Virtual Page Offset
,VPO
) - 一個
n-p
位的虛擬頁號(Virtual Page Number
,VPN
)MMU
利用VPN
選取適當的PTE(頁麵條目,Page Tabe Entry,PTE)
- 一個
- 頁麵條目 (
PTE
)中物理頁號(PPN
)和虛擬地址中的VPO
串聯起啦,即是物理地址
PPO
和VPO
是相同的- 不要忘記
VPN
,PPN
都是塊,都是首地址而已,所以需要偏移地址PPO
,VPO
圖(a)展示頁面命中,CPU硬體執行過程
- 第一步:處理器生成虛擬地址,把它傳送給
MMU
。 - 第二步:
MMU
生成PTE
地址(PTEA
),並從高速緩存/主存請求中得到它。 - 第三步: 高速緩存/主存向MMU返回
PTE
。 - 第四步:
MMU
構造物理地址(PA),並把它傳送給高速緩存/主存。 - 第五步: 高速緩���/主存返回所請求的數據字給處理器。
頁面命中完全由硬體處理,與之不同的是,處理缺頁需要 硬體和操作系統內核協作完成。
- 第一到三步: 與命中時的一樣
- 第四步:
PTE
有效位是零,所以MMU
觸發異常,傳遞CPU中的控制到操作系統內核中的 缺頁異常處理程式。 - 第五步:缺頁異常處理程式確定出物理存儲頁中的犧牲頁,如果這個頁面已經被修改,則把它換出到磁碟。
- 第六步:缺頁異常處理程式調入新的頁面,並更新存儲器中的
PTE
。 - 第七部:缺頁異常處理程式返回到原來的進程,再次執行導致缺頁的指令,之後就是頁面命中一樣的步驟。
9.6.1 結合高速緩存和虛擬存儲器(PA->記憶體)
在任何使用虛擬存儲器又使用SRAM
高速緩存的系統中,都存在應該使用虛擬地址 還是 使用 物理地址 來訪問SRAM高速緩存
的問題。
使用虛擬地址的優點,就是類似於使用虛擬存儲器的優點,更好的利用空間。但是設計更複雜。兩者的使用需要權衡。
大多數系統是選擇物理定址。
- 使用物理定址,多個進程同時在高速緩存中有存儲塊和共用來自相同虛擬頁面的塊稱為簡單的事。
- 而且還無需處理保護問題,因為 訪問許可權的檢查在地址翻譯中(
PTE
)的一部分。
- 而且還無需處理保護問題,因為 訪問許可權的檢查在地址翻譯中(
- 以下是一個例子(將
PTE
進行高速緩存)。
9.6.2 利用TLB加速地址翻譯(VA->PA)
每次CPU產生一個虛擬地址,MMU
就必須查閱一個PTE
,以便將虛擬地址翻譯為 物理地址。
- 在最糟糕的情況下,會從記憶體中取數據,代價是幾十 到幾百個周期
- 如果
PTE
碰巧緩存在L1
中,那麼開銷就下降到一到兩個周期
許多系統都試圖消除這樣的開銷,他們在MMU
中包含了一個關於PTE
的小緩存,稱為翻譯後備緩衝器(Translation Lookaside Buffer,TLB)
。
TLB
是一個小的,虛擬定址的緩存。- 每一行都保存著一個由單個
PTE
組成的塊。 TLB
通常用於高度的相連性如圖所示
- 用於組選擇和行匹配的
索引
和標記欄位
是從虛擬地址中的虛擬頁號中提取出來的。 - 如果
TLB
有T=2^t個組- 那麼
TLB索引
(TLBI
)是由VPN的t
個最低位組成。(對應於VPO
) TLB標記
(TLBT
)是由VPN中剩餘位組成(對應於VPN
)
- 那麼
- 用於組選擇和行匹配的
- 每一行都保存著一個由單個
- 下圖展示了
TLB
命中步驟- 關鍵點:所有的地址翻譯步驟都是在晶元上的MMU中執行的,因此非常快
TLB
命中- 第一步:CPU產生虛擬地址。
- 第二步和第三部:
MMU
從TLB
取出對應的PTE
。 - 第四步:
MMU
將這個虛擬地址翻譯成一個物理地址,發送到高速緩存/主存
- 第五步:
高速緩存/主存
所請求的數據字返回給CPU
- 當
TLB
不命中的時候,MMU
必須從L1
緩存或記憶體中取出相應的PTE
,併進行類似缺頁處理過程。
9.6.3 多級頁表
如果我們有一個32位地址空間,4KB
大小的頁面(p=2^12
)和一個4B
的PTE
,即使應用所引用的只是虛擬地址空間中很小的一部分,也總是需要一個4MB
的頁表駐留在存儲器中。
所以多級頁表
的誕生用於解決在很少使用時有一個很大的頁表常駐於記憶體。
計算方式,最多可能要
2^32/4KB=1MB
個頁面,每個頁面需要4B的PTE
所以需要4MB
大小的頁表。
思考虛擬地址是31~p
,p-1~0
即VPN,VPO。
VPN
即可表示頁面個數(上文中的1MB
),VPO
即頁面大小(上文中的4KB
),顯然知道兩者相乘為2^32 次方、
用來壓縮頁表的常用方式是使用層次結構的頁表。
頁表本身一個優點就是用來解決 記憶體不夠裝載程式所用記憶體的情況,進行動態分配。那麼當我們發現記憶體裝載那麼大的頁表也是負擔的時候,顯然也可以用類似頁表的形式來解決,這就是多級頁表。
以下用上圖的 兩層 作為例子。
總共有
9KB
個頁面,PTE為4個位元組。- 前
2KB
個頁面分配給代碼和數據。 - 接下來
6KB
個頁面未分配 - 再接下來
1023
個頁面也未分配 - 接下一個頁面分配給用戶棧
- 前
- 一級頁表中的每個
PTE
負責映射虛擬地址空間中一個4MB
大小的片(chunk)
.- 每一個
片
都是由1024個連續的頁面組成。 4MB=1024個頁面*PTE大小4位元組
。
- 每一個
- 如果
片i
中每個頁面都沒有分配,那麼一級PTE i
就為空。- 例如圖中的
PTE 2
~PTE 7
- 但是如果
片i
中有一個被分配了,那麼PTE i
就不能為空。- 是不是覺得這樣很浪費啊~所以說,
三級四級頁表
的原由也是如此。 - 而且後文會發現,頁表級數即使很大,複雜度也不會怎麼變化。
- 是不是覺得這樣很浪費啊~所以說,
- 例如圖中的
- 這種方法從兩個方面減少了存儲器要求。
- 如果一級頁表
PTE
為空,那麼相應的二級頁表就根本不會存在。- 一種巨大的潛在節約,大部分時候記憶體都是未分配的。
- 只有一級頁表才需要總是在主存中。
- 虛擬存儲器系統可以在需要時創建,頁面調入,調出二級頁面,減少主存壓力。
- 如果一級頁表
k級頁表層次結構的地址翻譯。
虛擬地址
被分為k
個VPN
和一個VPO
。每個VPN i
都是i-1
級頁表到i
級頁表的索引。PPN
存於k級頁表。PPO
依舊與VPO
相同。
此時TLB能發揮作用,因為層次更細,更利於緩存。使得多級頁表的地址翻譯不比單級頁表慢很多。
9.6.4 綜合:端到端的地址翻譯
在這一節里,我們通過一個具體的端到端的地址翻譯示例,來綜合一下我們學過的內容。
一個在有一個TLB
和L1 d-cache
的小系統上。作出如下假設:
- 存儲器都是按位元組定址的。(?)
- 存儲器訪問是針對一位元組的字的。(?)
- 虛擬地址是
14
位長(n=14) - 物理地址是
12
位長(m=12) - 頁面大小是
64
位元組(P=2^6) TLB
是四路組相連的,總共有16
個條目(?)L1 d-cache
是物理定址,高速緩存,直接映射(E=1)的,行大小為4位元組,而總共有16個組。(?)
存儲結構快照
TLB
:TLB
利用VPN
的位進行緩存。頁表
: 這個頁表是一個單級設計。一個有256個,但是這裡只列出16個。高速緩存
:直接映射的緩存通過物理地址的欄位來定址。- 因為是直接映射,通過索引就能直接找到。且
E=1
。 - 直接能判定是否命中。
- 因為是直接映射,通過索引就能直接找到。且
9.7 案例研究: Intel Core i7/Linux 存儲器系統
處理器包(processor package)
- 四個核
- 層次結構的
TLB
- 虛擬定址
- 四路組相連
Linux
一頁4kb
- 層次結構的
數據和指令
高速緩存。- 物理定址
L1
,L2
八路組相連L3
十六路組相連塊
大小64位元組。
- 快速的點到點鏈接。
- 基於
Intel QuickPath
技術。 - 為了讓核與其他核和外部
I/O
橋直接通信。
- 基於
- 層次結構的
L3
高速緩存DDR3存儲器控制器
。
- 四個核
9.7.1 Core i7地址翻譯
上圖完整總結了Core i7
地址翻譯過程,從虛擬地址到找到數據傳入CPU。
Core i7
採用四級頁表層次結構。CR3
控制寄存器指向第一級頁表(L1)的起始位置CR3
也是每個進程上下文的一部分。- 上下文切換的時候,
CR3
也要被重置。
一級,二級,三級頁表PTE
的格式:
P=1
時 地址欄位包含了一個40位物理頁號(PPN)
,指向適當的頁表開始處。- 強加了一個要求,要求物理頁
4kb
對齊。- 因為
PPO
為12
位 =4kb
PPO
的大小就跟物理頁的大小有關。
- 因為
四級頁表的PTE
格式:
PTE
有三個許可權位,控制對頁的訪問R/W
位確定頁的內容是可以 讀寫還是 只讀。U/S
位確定用戶模式是否能夠訪問,從而保護操作系統內核代碼不被用戶程式訪問。XD
(禁止執行) 位是在64位系統引入,禁止某些存儲器頁取指令。- 這是一個重要的新特性,限制只能執行只讀文本段,降低緩衝區溢出的風險。
- 當
MMU
翻譯虛擬地址時,還會更新兩個內核缺頁處理程式會用到的位。A
位- 每次訪問一個頁,
MMU
都會設置A
位,稱為引用位(reference bit)
. - 可以利用這個
引用位
來實現它的頁替換演算法。
- 每次訪問一個頁,
D
位- 每次對一個頁進行了
寫
就會設置D
位,又稱臟位(dirty bit)
. 臟位
告訴內核在拷貝替換頁前是否要寫回
。
- 每次對一個頁進行了
- 內核通過調用一條特殊的內核模式指令來清除
引用位
或臟位
。
四級頁表如何將VPN
翻譯成物理地址
- 每個
VPN
被用作頁表的偏移量。 CR3
寄存器包含L1頁的物理地址
優化地址翻譯
在對地址翻譯中,我們順序執行這兩個過程
MMU
將虛擬地址翻譯成物理地址。- 物理地址傳送到
L1
高速緩存。
然而實際的硬體實現使用了一個靈巧的技巧,允許這兩個步驟並行。加速了對高速緩存的訪問
例如:頁面大小為4KB
的Core i7
上的虛擬地址有12
位的VPO
,且PPO
=VPO
.而且物理地址的緩存,也是
6
位索引+6
位偏移,剛好是VPO
的12位。這不是巧合
- 一方面通過
VPN
找PPN
。- 另一方面直接通過
PPO
對高速緩存進行組選擇。- 等找到
VPN
後就能立即進行關鍵字匹配。
9.7.2 Linux 虛擬存儲系統
目標:對Linux的虛擬存儲系統做一個描述,大致瞭解操作系統如何組織虛擬存儲器,如何處理缺頁
。
內核虛擬存儲器
內核虛擬存儲器
包含內核中的代碼和數據。內核虛擬存儲器
的某些區域被映射到所有進程共用的物理頁面- 如:內核代碼,全局數據結構。
Linux
也將一組連續的虛擬頁面
(大小等同於系統DRAM
總量)映射到相應的一組物理頁面
。(這句話啥意思???????????????????????????????)
內核虛擬存儲器
包含每個進程不相同的數據。- 頁表,內核在進程上下文中時使用的棧,等等。
Linux 虛擬存儲器區域
Linux
將虛擬存儲器組織成一些區域
(也叫做段
)的集合。
- 一個
區域
就是已經存在著的(已分配的) 虛擬存儲器的連續片
,這些片/頁已某種形式相關聯。- 代碼段,數據段,堆,共用庫段,用戶棧。
- 所有存在的
虛擬頁
都保存在某個區域。
區域
的概念很重要- 允許
虛擬地址空間
有間隙。
- 允許
一個進程中虛擬存儲器的內核數據結構。
內核
為系統中每個進程維護了一個單獨的任務結構
。任務結構
中的元素包含或指向內核運行該進程所需要的全部信息。
task_struct
mm_struct
- 描述了虛擬存儲器的當前狀態。
pgd
- 指向
第一級頁表
的基址。 - 當進程運行時,內核將
pgd
存放在CR3
控制寄存器
- 指向
mmap
vm_area_structs(區域結構)
- 每個
vm_area_structs
都描述了當前虛擬地址空間的一個區域(area)
. vm_start
:指向這個區域的起始處。vm_end
:指向這個區域的結束處。vm_port
:描述這個區域內包含的所有頁的讀寫許可許可權。vm_flags
:描述這個區域頁面是否與其他進程共用,還是私有。- 還有一些其他事情
vm_next
: 指向鏈表的下一個區域。
- 每個
Linux 缺頁異常處理
MMU
在試圖翻譯虛擬地址A時,觸發缺頁。這個異常導致控制轉移到缺頁處理程式
,執行一下步驟。
- 虛擬地址A是合法的嗎?
- A在某個區域結構定義的區域內嗎?
- 解決方法:
- 缺頁處理程式搜索
區域結構
鏈表。 - 把A和每個區域的
vm_start
和vm_end
做比較。- 通過某種
樹的數據結構演算法
查找
- 通過某種
- 缺頁處理程式搜索
- 如果不合法,觸發段錯誤。
- 試圖訪問的存儲器是否合法?
- 即是否有讀,寫,執行這個頁面的許可權?
- 如果不合法,觸發
保護異常
,終止進程。
- 一切正常的話
- 選擇犧牲頁,替換,重新執行指令
9.8 存儲器映射
存儲器映射
: Linux通過將一個虛擬存儲器區域
與一個磁碟
上的對象關聯起來,以初始化這個虛擬存儲器區域
的內容,這個過程叫做存儲器映射
。
虛擬存儲器區域
可以映射到以下兩種類型文件。
- Unix文件系統中的普通文件:一個
區域
可以映射到一個普通磁碟文件的連續部分。- 例如,一個可執行文件。
文件區(section)
被分成頁
大小的片,每一片包含一個虛擬頁面
的初始化內容。- 僅僅是
初始化
,虛擬頁面
此時還並未進入物理存儲器
。- 直到
CPU
第一次引用這個頁面。
- 直到
- 匿名文件 : 一個
區域
可以映射到一個匿名文件
。匿名文件
由內核創建,包含的全是二進位零。CPU
第一次引用這樣區域(匿名文件)的虛擬頁面
時。- 將存儲器中
犧牲頁面
全部用二進位零
覆蓋。 - 並將
虛擬頁面
標記為駐留在存儲器中。 - 註意: 實際上,虛擬頁面並沒有跟存儲器進行數據傳送。
- 反正是送零過去,不如我自己用零賦值,這樣子更快。
- 將存儲器中
- 又叫
請求二進位零的頁(demand-zero page)
。
交換文件
,交換空間
。(win
下叫做paging file
)
一旦一個虛擬頁面被初始化了,它就在一個由內核維護的專門的
交換文件(swap file)
之間換來換去。交換文件
也叫交換空間
或者交換區域
。需要意識到,在任何時刻,
交換空間
都限制著當前運行著的進程分配的虛擬頁面總數。這一段不太明白。
9.8.1 再看共用對象
共用對象
的由來
- 許多進程有同樣的
只讀文本區域
。printf
- 運行
Uinx shell
的tcsh
- 如果每個進程都載入進記憶體一次,極其浪費。
- 存儲器映射提供一種機制,來
共用對象
。
一個對象被映射到虛擬存儲器
的一個區域,一定屬於以下兩種。
- 共有對象
- 一個進程將一個
共有對象
映射到它的虛擬地址空間的一個區域。- 進程對這個
區域
的寫操作,對於那些也把這個共用對象映射它的虛擬存儲器的進程
是可見的。 - 這���變化也會反映到
磁碟
上的原始對象。
- 進程對這個
- 映射到的虛擬存儲器那個
區域
叫做共用區域
。
- 一個進程將一個
- 私有對象
- 對一個映射到
私有對象
的區域做出的改變,對於其他進程不可見. - 並且進行的寫操作不會反映到
磁碟
上。 - 映射到的虛擬存儲器那個
區域
叫做私有區域
。
- 對一個映射到
9.8.1.1 共用對象
進程1
,將共用對象映射到虛擬存儲器
中,然後虛擬存儲器
將這一段找一塊物理存儲器
存儲。- 當
進程2
也要引用同樣的共用對象時。- 內核迅速判定,
進程1
已經映射了這個對象。 - 使
進程2
的虛擬存儲器
直接指向了那一塊進程1
指向的物理存儲器
。
- 內核迅速判定,
- 即使
對象
被映射到多個共用區域,物理存儲器依舊只有一個共用對象的拷貝。- 大大解決了物理存儲器記憶體。
9.8.1.2 私有對象
私有對象
使用一種叫做寫時拷貝(conpy-on-write)
的巧妙技術。
私有對象
開始生命周期的方式基本與共用對象
一樣。- 即使對象被多個引用,在物理記憶體都只保留一個拷貝。
- 對於每個映射
私有對象
的進程,相應私有區域
的頁表條目
都被標記為只讀。- 並且
區域結構(vm_area_structs)
被標記為私有的寫時拷貝
。
- 並且
- 過程:只要有進程試圖寫
私有區域
內的某個頁面,那麼這個寫操作
觸發保護異常
。故障處理程式
會在物理存儲器
中創建被修改頁面的一個新拷貝。- 更新
頁表條目(PTE)
指向這個新的拷貝,恢復被修改頁面的可寫許可權。 故障處理程式
返回,CPU重新執行這個寫操作
。
- 通過延遲私有對象中的拷貝直到最後可能的時刻,
寫時拷貝
充分使用了稀缺的物理存儲器。
9.8.2 再看fork函數(私有對象的應用)
瞭解fork
函數如何創建一個帶有自己獨立虛擬地址空間的新進程。
- 當
fork
函數被當前進程調用時。- 內核為
新進程
創建內核數據結構,並分配給它唯一一個PID
。 - 為了給
新進程
創建虛擬存儲器。- 創建了當前進程的
mm_struct
,區域結構
和頁表的原樣拷貝。 - 將兩個進程的每個頁面都標記為只讀。並給兩個區域進程的每個區域結構都標記為
私有的寫時拷貝
。 - 註意:並沒有對物理存儲器進行拷貝哦~,利用的是
私有對象
的寫時拷貝
技術。
- 創建了當前進程的
- 內核為
- 當
fork
函數在新進程返回時。- 新進程現在的虛擬存儲器剛好和調用
fork
時存在的虛擬存儲器相同。 - 當兩個進程中任一個需要被
寫
時,觸發寫時拷貝機制
。
- 新進程現在的虛擬存儲器剛好和調用
9.8.3 再看execve函數
理解execve
函數實際上如何載入和運行程式。
- 假設運行在當前的進程中的程式執行瞭如下的調用:
Execve("a.out",NULL,NULL);
execve
函數在當前進程載入並執行目標文件a.out
中的程式,用a.out
代替當前程式。- 載入並運行需要以下幾個步驟。
- 刪除已存在的用戶區域。
- 刪除當前進程虛擬地址的用戶部分中已存在的
區域結構
。
- 刪除當前進程虛擬地址的用戶部分中已存在的
- 映射私有區域。
- 為新程式的文本,數據,
bss
和棧區域創建新的區域結構
。- 所有新的
區域結構
都是私有的
,寫時拷貝
的。 - 文本和數據區域被映射到
a.out
文件中的文件和數據區。 bss
區域是請求二進位零
,映射到匿名文件。- 大小包含在
a.out
中
- 大小包含在
堆,棧
區域也是請求二進位零
。
- 所有新的
- 為新程式的文本,數據,
- 映射共用區域
a.out
程式與共用對象鏈接。- 這些對象都是動態鏈接到這個程式。
- 然後映射到用戶虛擬地址的共用區域。
- 設置程式計數器(PC)
execve
最後一件事設置PC
指向文本區域的入口點。
- 刪除已存在的用戶區域。
- 載入並運行需要以下幾個步驟。
9.8.4 使用mmap
函數的用戶級存儲器映射
Unix進程
可以使用mmap
函數來創建新的虛擬存儲器區域
,並將對象映射到這些區域中。
#include <unistd.h>
#include <sys/mman.h>
void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offset);
返回:若成功時則為指向映射區域的指正,若出錯則為MAP_FAILED(-1).
參數解釋:
fd
,start
,length
,offset
:
mmap
函數要求內核創建一個新的虛擬存儲器區域,最好是從地址start
開始的一個區域,並將文件描述符fd
指定的對象的一個連續的片chunk
映射到這個新的區域。
- 連續對象片大小為
length
位元組 - 從據文件開始處偏移量為
offset
位元組的地方開始。 statr
地址僅僅是個暗示- 一般被定義為
NULL
,讓內核自己安排。
- 一般被定義為
prot
參數prot
包含描述新映射的虛擬存儲器區域的訪問許可權位
。(對應區域結構中的vm_prot
位)
PROT_EXEC
:這個區域內的頁面由可以被CPU執行的指令組成。PROT_READ
:這個區域內的頁面可讀。PROT_WRITE
: 這個區域內的頁面可寫。PROT_NONE
: 這個區域內的頁面不能被訪問。
flag
參數flag
由描述被映射對象類型的位
組成。
MAP_ANON
標記位:映射對象是一個匿名對象
。MAP_PRIVATE
標記位:被映射對象是一個私有
的,寫時拷貝
的對象。MAP_SHARED
標記位:被映射對象是一個共用
對象。
例子
bufp = mmap(NULL,size,PROT_READ,MAP_PRIVATE|MAP_ANON,0,0);
- 讓內核創建一個新的包含size位元組的只讀,私有,請求二進位零的虛擬存儲區域。
- 如果調用成功,那麼
bufp
包含新區域地址。
munmap
函數刪除虛擬存儲器的區域:
9.9 動態存儲器分配
雖然可以使用更低級的mmap
和munmap
函數來創建和刪除虛擬存儲器的區域。
但是C程式員還是覺得用動態存儲器分配器(dynamic memory allocator)
更方便。
動態存儲器分配器
維護著一個進程的虛擬存儲區域,稱為堆(heap)
。- 系統之間細節不同,但是不失通用型。
- 假設
堆
是一個請求二進位零的區域。- 緊接著未初始化的
bss
區域,並向上生長(向更高的地址)。 - 對於每個進程,內核維護一個變數
brk(break)
,指向堆頂。
分配器
將堆
視為一組不同大小的塊block
的集合來維護。每個塊
就是一個連續的虛擬存儲器片
,即頁面大小。- 要麼是
已分配
,要麼是空閑
。已分配
已分配的塊
顯式地保留供應用程式使用。已分配
的塊保持已分配狀態,直到它被釋放
。- 這種
釋放
要麼是應用程式顯示執行。
- 要麼是存儲器分配器自身
隱式
執行(JAVA)。
- 這種
空閑
空閑塊
可用於分配。空閑快
保持空閑,直到顯式地被應用分配。
分配器
有兩種基本分格。- 都要求應用
顯式
分配。 - 不同之處在於那個實體負責釋放已分配的塊。
顯式分配器(explict allocator)
- 要求應用程式顯式地
釋放
。 - C語言中提供一種叫
malloc
程式顯示分配器。malloc
和free
- C++
new
和delete
- 要求應用程式顯式地
隱式分配器(implicit allocator)
- 要求
分配器
檢測一個已分配塊何時不再被程式所使用,那麼就釋放
這個塊。 隱式分配器
又叫做垃圾收集器(garbage collector)
.- 自動釋放未使用的已分配的塊的過程叫做
垃圾收集(garbage collection)
.
- 自動釋放未使用的已分配的塊的過程叫做
Lisp
,ML
以及Java
等依賴這種分配器。
- 要求
- 都要求應用
本節剩餘的部分討論的是顯示分配器
的設計與實現。
9.9.1 malloc和free 函數
malloc
C標準庫提供了一個稱為malloc
程式包的顯示分配器
。
#include<stdlib.h>
void* malloc(size_t size);
返回:成功則為指針,失敗為NULL
malloc
返回一個指針,指向大小為至少size
位元組的存儲器塊。- 不一定是
size
位元組,很有可能是4
或8
的倍數
- 這個
塊
會為可能包含在這個塊
內的任何數據對象
類型做對齊。 Unix
系統用8
位元組對齊。
- 這個
malloc
不初始化它返回的存儲器。- 如果想要初始化,可以用
calloc
函數。calloc
是malloc
一個包裝函數。
- 如果想要初始化,可以用
- 想要改變已分配塊大小。
- 用
realloc
h函數
- 用
- 不一定是
- 如果
malloc
遇到問題。- 返回
NULL
, 並設置errno
。
- 返回
動態存儲分配器
,可以通過使用mmap
和munmap
函數,顯示分配和釋放堆存儲器。或者可以使用
sbrk
函數。#include<unistd.h> void *sbrk(intptr_t incr); 返回:若成功則為舊的brk指針,若出錯則為-1,並設置errno為ENOMEML.
sbrk
函數通過將內核的brk
指針增加incr
(可為負)來收縮和擴展堆。
free
程式通過調用free
函數來釋放已分配的堆塊。
#include<stdlib.h>
void free(void *ptr);
返回:無
ptr
參數必須指向一個從malloc
,calloc
,realloc
獲得的已分配塊的起始位置。- 如果不是,那麼
free
行為未定義。 - 更糟糕的是,
free
沒有返回值,不知道是否錯了。
- 如果不是,那麼
這裡的字=4位元組,且malloc是8位元組對齊。
9.9.2 為什麼要使用動態存儲器分配
程式使用動態存儲器分配的最重要原因是:
- 經常直到程式實際運行時,它們才知道某些數據結構的大小。
9.9.3 分配器的要求和目標
約束
顯式分配器有如下約束條件
- 處理任意請求序列。
- 立即響應請求。
- 不允許為提高性能重新排列或
緩衝
請求。
- 不允許為提高性能重新排列或
- 只使用
堆
。 - 對齊
塊
。- 上文的
8
位元組。
- 上文的
- 不修改已分配的塊。
目標
吞吐率最大化
和存儲器使用率
最大化。這兩個性能要求通常是相互衝突的。
- 目標1:
最大化吞吐率
- 假定n個分配和釋放請求的某種序列
R1,R2,R3.....Rn
吞吐率 :
每個單位時間完成的請求數。
- 通過使
分配和釋放請求
的平均時間最小化 來最大化吞吐率
- 假定n個分配和釋放請求的某種序列
- 目標2:
最大化存儲器利用率
- 設計優秀的分配演算法。
需要增加分配和釋放請求的時間。
評估使用
堆
的效率,最有效的標準是峰值利用率(peak utilization)
- 假定n個分配和釋放請求的某種序列
R1,R2,R3.....Rn
有效載荷(payload)
:如果一個應用程式請求一個p
位元組的塊,那麼得到的已分配塊
的有效載荷
是p
位元組。(很有可能會分配p+1
個位元組之類的)聚集有效載荷(aggregate payload)
:請求Rk
完成之後,Pk
表示當前已分配塊的有效載荷
之後。又叫做聚集有效載荷
。Hk
表示堆的當前的大小(單調非遞減的)。
- 峰值利用率為
Uk
- 假定n個分配和釋放請求的某種序列
吞吐率
和存儲器利用率
是相互牽制的,分配器
設計的一個有趣的挑戰就是在兩者之間找到一個平衡。
9.9.4 碎片
造成堆利用率很低的主要原因是一種稱為碎片(fragmentation)
的現象。
碎片
:雖然有未使用的存儲器但不能滿足分配要求時的現象。- 1.
內部碎片
:已分配塊比有效載荷(實際所需要的)大時發生。- 比如:上文中只要5個字(有效載荷),卻給了6個字(已分配塊),那一個多的就是
碎片
. - 任何時刻,
內部碎片
的數量取決於以前請求
的模式和分配器的實現方式。- 可計算的,可量化的。
- 比如:上文中只要5個字(有效載荷),卻給了6個字(已分配塊),那一個多的就是
- 2.
外部碎片
:當空閑存儲器合計
起來足夠滿足一個分配請求,但是沒有一個單獨
的空閑塊足夠大可以處理這個請求發生的。外部碎片
的量化十分困難。- 不僅取決於以前
請求
的模式和分配器的實現方式,還要知道將來請求
的模式。
- 不僅取決於以前
- 優化: 需要
啟髮式策略
來用少量的大空閑塊替換大量的小空閑塊。
- 1.
9.9.5 實現問題
一個實際的分配器要在吞吐率
和利用率
把我平衡,必須考慮一下幾個問題。
- 空閑塊組織: 如何記錄空閑塊? (對應
9.9.6
) - 放置: 如何選擇一個合適的空閑快來放置一個新分配的塊? (對應
9.9.7
) - 分割: 將一個新分配的塊放入某個空閑塊後,如何處理這個空閑快中的剩餘部分?(對應
9.9.8
) - 合併: 我們如何處理一個剛剛被釋放的塊
9.9.6 隱式空閑鏈表(老本行了)
堆塊
(十分巧妙的利用了本該永遠為0的低三位):
- 一個
塊
由一個字的頭部
,有效載荷
,以及可能的填充
組成。頭部
:編碼了這個塊
的大小(包括頭部和填充),以及這個塊
是否分配。- 假設是
8位元組
的對齊約束條件- 那麼頭部低三位一定是
0
。 - 所以釋放低三位來表示一些其他信息。
- 即塊大小還是能表示
0~2^32
(只是必須是8的倍數),非0~2^29
。 - 低三位就能表示
是否分配
之類的信息。
- 即塊大小還是能表示
- 那麼頭部低三位一定是
- 假設是
將堆
組織為一個連續的已分配塊
和空閑塊
的序列。
這種結構就叫做隱式空閑鏈表
隱式
:- 為什麼叫
隱式鏈表
。- 因為不是通過指針(
next
)來鏈接起來。 - 而是通過
頭部
的長度隱含地鏈接起來。
- 因為不是通過指針(
終止頭部
(類似與普通鏈表的NULL
)已分配
,大小為零
的塊
- 為什麼叫
優缺點
:- 優點:簡單
- 缺點1:任何操作的
開銷
都與已分配塊和空閑塊的總數呈線性關係O(N)
.- 放置分配的塊。
- 對空閑鏈表的搜索。
- 缺點2: 即使申請一個
位元組
,也會分配2
個字
的塊。空間浪費。
9.9.7 放置已分配的塊
當應用請求k
位元組的塊,分配器搜索空閑鏈表,查找一個足夠大可以放置請求的空閑塊。
有一下幾種搜索放置策略
首次適配
從頭開始
搜索空閑鏈表,選擇第一個合適的空閑塊。
下一次適配
- 和
首次適配
很類似,但不是從頭開始,而是從上一次查詢
的地方開始。
- 和
最佳適配
- 檢查每個空閑塊,找一個滿足條件的最小的空閑塊(貪心)。
優缺點
首次適配
- 優點
- 往往將大的空閑塊保留在鏈表後面。
- 缺點
- 小的空閑塊往往在前面,增大了對
較大快
的搜索時間。
- 小的空閑塊往往在前面,增大了對
- 優點
下一次適配
- 優點
- 速度塊。
- 缺點
存儲器利用率
低
- 優點
最佳適配
- 優點
- 利用率高
- 缺點
- 要完整搜索鏈表,速度慢。
- 後面有更加精細複雜的
分離式空閑鏈表
。
- 優點
9.9.8 分割空閑塊
兩種策略
- 占用
所有
空閑塊- 缺點:產生更多的
內部碎片
(但是如果內部碎片很少,可以接受) - 優點:能使得 空閑塊+已分配塊的數量減少
- 能加快
搜索速度
。 - 有的
外部碎片
(幾個位元組,很有可能是外部碎片)可能根本放置不了東西,但是卻占用了搜索時間,還不如當內部碎片算了
- 能加快
- 放置策略趨向於產生好的匹配中使用。
- 即占用所有
空閑塊
,內部碎片也很少。
- 即占用所有
- 缺點:產生更多的
- 分割空閑塊
- 缺點:更多的空閑塊和已分配塊,搜索速度降低。
- 優點:空間利用率更高。
9.9.9 獲取額外的堆存儲器
如果分配器
不能為請求塊找到合適的空閑塊
將發生什麼?
合併
相鄰的空閑塊(下一節描述)。sbrk
函數- 在最大化合併還不行的情況。
- 向內核請求額外的堆存儲器。
- 並將其轉為
大的空閑塊
- 將塊插入鏈表。
- 並將其轉為
9.9.10 合併空閑塊
假碎片
: 因為釋放
,使得某些時候會出現相鄰的空閑塊。
- 單獨的放不下請求(
碎片
),合併卻可以(假性
),所以叫假碎片
。
何時合併?
重要的決策決定,何時執行合併?
立即合併
- 定義:
塊
被釋放時,合併所有相鄰的塊。 - 缺點:對於某些請求模式,會產生
抖動
。
- 定義:
推遲合併
- 定義: 一個稍晚的時候,再合併。
- 比如:上文中的找不到合適空閑塊的時候。
- 定義: 一個稍晚的時候,再合併。
在對分配器的討論中,我們假設使用立即合併
。
但要知道,快速的
分配器通常會選擇某種形式的推遲合併
。
9.9.11 帶邊界標記的合併
Q
:釋放當前塊
後,如果要合併下一個
塊是十分簡單,但是合併上一塊
複雜度卻很高。
A
:Knuth
提出邊界標記
。
- 就是是
頭部
的副本。 其實就是
雙向鏈表
啦。- 缺點:每個塊保持一個頭部和腳部,浪費空間。
- 在應用程
- 在應用程