2017-2018-1 20155227 《信息安全系統設計基礎》第十三周學習總結
找出全書你認為最重要的一章,深入重新學習一下,要求(期末占10分):
完成這一章所有習題詳細總結本章要點給你的結對學習搭檔講解你的總結并獲取反饋
我選擇教材第九章的內容來深入學習,一方面,虛擬內存這一部分內容本身就十分重要,另一方面,我在操作系統課上對這一部分的知識理解的很淺,在十一周自學教材這一部分內容時也因為知識點太多而沒有學得很深入,正好借此機會來重新學習這一章的內容,加深理解。
教材學習內容總結
第九章 虛擬存儲器
為了更加有效地管理存儲器且少出錯,現代系統提供了對主存的抽象概念,叫做虛擬存儲器(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)
的專用硬件。利用存儲在主存中的查詢表來動態翻譯虛擬地址。查詢表由操作系統管理。
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 虛擬存儲器作為緩存的工具
虛擬存儲器(VM)
被組織為一個存放在磁盤上的N個連續字節大小
的單元組成的數組
。
每個字節都有一個唯一的虛擬地址,這個虛擬地址作為到數組的索引。
磁盤上數組的內容被緩存到
主存
中。同存儲器層次結構其他緩存一樣,磁盤上的數據被
分割成塊
。這些塊作為磁盤和主存之間的傳輸單元。虛擬頁(Virtual Page,VP)就是這個塊
- 物理存儲器被分割為
物理頁
,大小也為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
中讀取成功。
塊被稱為頁。
磁盤和DRAM之間傳送頁的活動叫做交換(swapping)或者頁面調度(paging)。
有不命中發生時,才換入頁面,這種策略叫做按需頁面調度(demand paging)。
9.3.5 分配頁面
比如某個頁面所指向地址為NULL
,將這個地址指向磁盤某處
,那么這就叫分配頁面
。
此時虛擬頁從未分配狀態
變為 未緩存
。
9.4 虛擬存儲器作為存儲器的管理工具
操作系統為每個進程提供一個獨立的頁表
。
因此,VM
簡化了鏈接和加載
,代碼和數據共享
,以及應用程序的存儲器分配
。
- 簡化鏈接
- 獨立的空間地址意味著每個進程的存儲器映像使用
相同的格式
。- 文本節總是從
0x08048000(32位)
處或0x400000(64位)
處開始。 - 然后是
數據,bss節,棧
。
- 文本節總是從
- 一致性極大簡化了鏈接器的
設計和實現
。
- 獨立的空間地址意味著每個進程的存儲器映像使用
- 簡化加載
- 加載器可以
從不實際拷貝任何數據從磁盤到存儲器
。 - 基本都是
虛擬存儲系統
完成。
- 加載器可以
簡化共享
獨立地址空間為操作系統提供了一個管理用戶進程和操作系統自身之間的
一致共享機制
- 操作相同的
操作系統內核代碼
- C標準庫的
printf
.
- 操作相同的
- 因此操作系統需要將不同進程的適當的虛擬頁映射到相同的物理頁面。
- 多個進程共享這部分代碼的一個拷貝。
- 而不是每個進程都要加載單獨的內核和C標準庫的拷貝。
簡化存儲器分配
即虛擬頁連續
(虛擬頁還是單獨的),物理頁可以不連續
。使得分配更加容易。
9.5 虛擬存儲器作為存儲器保護的工具
任何現代操作系統必須為操作系統提供手段來控制對存儲器系統的訪問。
- 不應該允許用戶進程修改它的只讀文本段。
- 不允許它讀或修改任何內核的代碼和數據結構
- 不允許讀寫其他進程的私有存儲器。
- 不允許修改共享的虛擬頁,除非所有共享者顯示允許這么做(通過調用明確的進程間通信)
- 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)
- 一個
頁面條目 (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 結合高速緩存和虛擬存儲器
在任何使用虛擬存儲器又使用SRAM高速緩存的系統中,都存在應該使用虛擬地址 還是 使用 物理地址 來訪問SRAM高速緩存的問題。
大多數系統是選擇物理尋址。
- 使用物理尋址,多個進程同時在高速緩存中有
存儲塊
和共享來自相同虛擬頁面
的塊成為簡單的事。- 而且還無需處理保護問題,因為 訪問權限的檢查在地址翻譯中
(PTE)
的一部分。
- 而且還無需處理保護問題,因為 訪問權限的檢查在地址翻譯中
- 以下是一個例子(將PTE進行高速緩存)。
9.6.2 利用TLB加速地址翻譯
每次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
- 第一步:CPU產生
- 當
TLB
不命中的時候,MMU
必須從L1緩存或內存中取出相應的PTE
,并進行類似缺頁處理過程。
9.6.3 多級頁表
如果我們有一個32位
地址空間,4KB
大小的頁面(p=2^12
)和一個4B的PTE
,即使應用所引用的只是虛擬地址空間中很小的一部分,也總是需要一個4MB
的頁表駐留在存儲器中。
所以多級頁表
的誕生用于解決在很少使用時有一個很大的頁表常駐于內存。
用來壓縮頁表的常用方式是使用層次結構的頁表。
以下用上圖的兩層作為例子。
- 總共有
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)
. - 臟位告訴內核在拷貝替換頁前是否要寫回。
- 內核通過調用一條特殊的內核模式指令來清除引用位或臟位。
- 每次對一個頁進行了寫 就會設置D位,又稱
四級頁表如何將VPN
翻譯成物理地址
- 每個
VPN
被用作頁表的偏移量
。 CR3
寄存器包含L1頁的物理地址
9.7.2 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時存在的
虛擬存儲器
相同。 - 當兩個進程中任一個需要被寫時,觸發寫時拷貝機制。
- 新進程現在的虛擬存儲器剛好和調用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標記位
:被映射對象是一個共享對象。
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
是mallo
c一個包裝函數。
- 如果想要初始化,可以用
- 想要改變已分配塊大小。
- 用
realloch
函數
- 用
- 不一定是
- 如果
malloc
遇到問題。- 返回
NULL
, 并設置errno
。
- 返回
動態存儲分配器
,可以通過使用mmap
和munmap
函數,顯示分配和釋放堆存儲器。- 或者可以使用
sbrk
函數。
- 或者可以使用
#include<unistd.h>void *sbrk(intptr_t incr);返回:若成功則為舊的brk指針,若出錯則為-1,并設置errno為ENOMEML.
free
程序通過調用free函數來釋放已分配的堆塊。
#include<stdlib.h>void free(void *ptr);返回:無
ptr
參數必須指向一個從malloc
,calloc
,realloc
獲得的已分配塊的起始位置。- 如果不是,那么
free
行為未定義。 - 更糟糕的是,
free
沒有返回值,不知道是否錯了。
- 如果不是,那么
9.9.2 為什么要使用動態存儲器分配
程序使用動態存儲器分配的最重要原因是:
- 經常直到程序實際運行時,它們才知道某些數據結構的大小。
9.9.3 分配器的要求和目標
顯式分配器有如下約束條件
- 處理任意請求序列。
- 立即響應請求。
- 不允許為提高性能重新排列或緩沖請求。
- 只使用
堆
。 對齊塊
。- 上文的8字節。
不修改
已分配的塊。
目標
吞吐率最大化和存儲器
使用率最大化。這兩個性能要求通常是相互沖突的
。
- 目標1:最大化吞吐率
- 假定
n
個分配和釋放請求的某種序列R1,R2,R3.....Rn
吞吐率
:每個單位時間
完成的請求數。
- 通過使分配和釋放請求的平均時間
最小化
來最大化吞吐率
- 假定
- 目標2:最大化存儲器利用率
- 設計
優秀的
分配算法。 - 需要增加分配和釋放請求的時間。
- 評估使用堆的效率,最有效的標準是
峰值利用率(peak utilization)
- 設計
吞吐率
和存儲器
利用率是相互牽制
的,分配器設計的一個有趣的挑戰就是在兩者之間找到一個平衡
。
9.9.4 碎片
造成堆利用率很低的主要原因是一種稱為碎片(fragmentation)的現象。
碎片:雖然有未使用的存儲器但不能滿足分配要求時的現象。
- 內部碎片:已分配塊比有效載荷(實際所需要的)大時發生。
- 比如:上文中只要5個字(有效載荷),卻給了6個字(已分配塊),那一個多的就是碎片.
- 任何時刻,
內部碎片
的數量取決于以前請求的模式和分配器的實現方式。可計算的
,可量化的
。
- 外部碎片:當空閑存儲器合計起來足夠滿足一個分配請求,但是沒有一個單獨的空閑塊足夠大可以處理這個請求發生的。
外部碎片
的量化十分困難。- 不僅取決于以前請求的模式和分配器的實現方式,還要知道
將來
請求的模式。
- 不僅取決于以前請求的模式和分配器的實現方式,還要知道
- 內部碎片:已分配塊比有效載荷(實際所需要的)大時發生。
9.9.5 實現問題
一個實際的分配器要在吞吐率
和利用率
把握平衡,必須考慮一下幾個問題。
- 空閑塊組織: 如何記錄空閑塊? ( 9.9.6)
- 放置: 如何選擇一個合適的空閑快來放置一個新分配的塊? (9.9.7)
- 分割: 將一個新分配的塊放入某個空閑塊后,如何處理這個空閑快中的剩余部分?(9.9.8)
9.9.6 隱式空閑鏈表
將堆
組織為一個連續的
已分配塊和空閑塊
的序列。
這種結構就叫做隱式空閑鏈表
隱式
:- 為什么叫
隱式鏈表
。- 因為不是通過
指針(next)
來鏈接起來。 - 而是通過
頭部的長度
隱含地鏈接起來。
- 因為不是通過
終止頭部(類似與普通鏈表的NULL)
- 已分配,大小為零的塊
- 為什么叫
- 優缺點:
優點
:簡單缺點1
:任何操作的開銷都與已分配塊和空閑塊的總數呈線性關系O(N)
.- 放置分配的塊。
- 對空閑鏈表的搜索。
缺點2
: 即使申請一個字節,也會分配2個字的塊。空間浪費。
9.9.7 放置已分配的塊
有以下幾種搜索放置策略
:
首次適配
- 從頭開始搜索空閑鏈表,選擇第一個合適的
空閑塊
。
- 從頭開始搜索空閑鏈表,選擇第一個合適的
下一次適配
- 和首次適配很類似,但不是從頭開始,而是從上一次查詢的地方開始。
最佳適配
- 檢查每個
空閑塊
,找一個滿足條件的最小的空閑塊(貪心)
。
- 檢查每個
優缺點
首次適配
優點
- 往往將大的空閑塊保留在鏈表后面。
缺點
- 小的空閑塊往往在前面,增大了對較大快的搜索時間。
下一次適配
優點
- 速度塊。
缺點
- 存儲器利用率低
最佳適配
優點
- 利用率高
缺點
- 要完整搜索鏈表,速度慢。
9.9.8 分割空閑塊
兩種策略:
- 占用所有空閑塊
缺點:
產生更多的內部碎片
(但是如果內部碎片很少,可以接受)優點:
能使得空閑塊+已分配塊
的數量減少- 能加快搜索速度。
- 有的外部碎片(幾個字節,很有可能是外部碎片)可能根本放置不了東西,但是卻占用了搜索時間,還不如當內部碎片算了
放置策略
趨向于產生好的匹配中使用。- 即占用所有
空閑塊
,內部碎片
也很少。
- 即占用所有
- 分割空閑塊
缺點:
更多的空閑塊和已分配塊,搜索速度降低。優點:
空間利用率更高。
9.9.9 獲取額外的堆存儲器
如果分配器
不能為請求塊找到合適的空閑塊
將發生什么?
合并相鄰的空閑塊
。sbrk函數
- 在
最大化合并
還不行的情況。 - 向內核請求額外的堆存儲器。
- 并將其轉為大的
空閑塊
- 將
塊
插入鏈表
。
- 并將其轉為大的
- 在
9.9.10 合并空閑塊
假碎片:
因為釋放,使得某些時候會出現相鄰的空閑塊
。
- 單獨的放不下請求
(碎片)
,合并卻可以(假性),所以叫假碎片
。
何時合并?
立即合并
定義:
塊被釋放時,合并所有相鄰的塊。缺點:
對于某些請求模式,會產生抖動
。
推遲合并
定義:
一個稍晚的時候,再合并。- 比如:上文中的找不到合適空閑塊的時候。
9.10 GC_垃圾收集
垃圾收集器(garbage collector)
是一種動態存儲分配器
。
垃圾:
它自動釋放不再需要的已分配塊,這些塊稱為垃圾(garbage)
.垃圾收集(garbage collection)
:自動回收堆存儲
的過程叫做垃圾收集
。應用顯式分配堆塊
,但從不顯式釋放堆塊。垃圾收集器
定期識別垃圾快,并調用相應地free
,將這些快放回空閑鏈表。
垃圾收集可以追溯到John McCarthy
在20世紀60年代
早期在MIT
開發的Lisp系統
。
- 它是
Java,ML,Perl和Mathematic等
現代語言系統的一個重要部分。 - 有關文獻描述了大量的垃圾收集方法,數量令人吃驚。
- 我們討論局限于
McCarthy
自創的Mark&Sweep(標記&清除)算法
。- 它可以建立已存在的
malloc包
的基礎上,為C和C++
提供垃圾收集。
- 它可以建立已存在的
9.11 C程序中常見的與存儲器有關的錯誤
9.11.1 間接引用壞指正
scanf("%d",&val);
scanf("%d",val);
最好的情況 :
以異常中止。- 有可能
覆蓋
某個合法的讀/寫區域,造成奇怪的困惑的結果。
9.11.2 讀未初始化的存儲器
堆存儲器并不會初始化。
正確做法
- 使用
calloc
. - 顯示
y[i]=0
;
- 使用
9.11.3 允許棧緩沖區溢出
程序不檢查輸入串的大小就寫入棧中的目標緩沖區
- 那么就有
緩沖區溢出錯誤(buffer overflow bug)
。 gets()
容易引起這樣的錯誤
- 用fgets()
限制大小。
9.11.4 假設指針和它們所指向對象是相同大小
有的系統里,int
和 int *
都是四字節,有的則不同。
9.11.5 越界
9.11.6 引用指針,而不是它所指向的對象
對指針的優先級用錯。
9.11.7 誤解指針的運算
忘記了指針的算術操作是以它們指向的對象的大小為單位來進行的,這種大小不一定是字節
。
9.11.8 引用不存在的變量
返回一個指針
,指向棧
里面一個變量的地址。但是這個變量在返回的時候已經從棧里被彈出。
- 地址是正確的,指向了
棧
。 - 但是卻沒有指向想指向的變量。
9.11.9 引用空閑堆塊的數據
引用了某個已經free
掉的塊。在C++
多態中經常容易犯這個錯誤。
9.11.10 引起存儲器泄露
- 即是沒有
回收垃圾
。導致內存中垃圾越來越多。- 只有
重啟程序
,才能釋放。
- 只有
- 對于守護進程和服務器這樣的程序,存儲器泄露是十分嚴重的事。
- 因為一般情況,不能隨便重啟。
9.12 小結
虛擬存儲器是對主存的一個抽象。
- 使用一種叫
虛擬尋址
的間接形式來引用主存。- 處理器產生
虛擬地址
,通過一種地址翻譯硬件來轉換為物理地址
。- 通過使用頁表來完成翻譯。
- 又涉及到各級緩存的應用。
頁表
的內容由操作系統提供
- 通過使用頁表來完成翻譯。
- 處理器產生
虛擬存儲器提供三個功能
- 它在主存中自動緩存最近使用的存放在磁盤上的
虛擬地址空間內容
。- 虛擬存儲器緩存中的塊叫做
頁
- 虛擬存儲器緩存中的塊叫做
- 簡化了
存儲器管理
,- 進而
簡化了鏈接
- 進程間
共享數據
。 - 進程的存儲器分配以及程序加載。
- 進而
- 每條
頁表
條目里添加保護位,從而簡化了存儲器保護
。
地址翻譯的過程必須和系統中所有的硬件緩存的操作集合。
- 大多數條目位于
L1
高速緩存中。- 但是又通過一個
TLB
的頁表條目的片上高速緩存L1
。
- 但是又通過一個
現代系統通過將虛擬存儲器片和磁盤上的文件片關聯起來,以初始化虛擬存儲器片
,這個過程叫做存儲器映射
。
存儲器映射
為共享數據,創建新的進程 以及加載數據提供一種高效的機制。- 可以用
mmap
手工維護虛擬地址空間區域
。- 大多數程序依賴于動態存儲器分配,例:
malloc
- 管理虛擬地址空間一個稱為堆的區域
- 分配器
兩種類型
。顯示分配器
C,C++
隱式分配器
JAVA
- 大多數程序依賴于動態存儲器分配,例:
GC是通過不斷遞歸訪問指針來標記已分配塊,在需要的時刻進行Sweep
。
C,C++
無法辨認指針導致無法實現完全的GC
。- 只有保守的
GC
。 - 需要配合平衡樹進行查找
p
所指向的塊
- 只有保守的
第九章課后家庭作業
書上課后練習題都有答案,就不在博客里詳細寫了。
9.11
A.虛擬地址0x027c
13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 0 |
B.地址翻譯
參數 | 值 |
---|---|
VPN | 0x09 |
TLB索引 | 0x01 |
TLB標記 | 0x02 |
TLB命中 | No |
缺頁 | No |
PPN | 0x17 |
C.物理地址格式
11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 |
D.物理地址引用
參數 | 值 |
---|---|
字節偏移 | 0x0 |
緩存索引 | 0xF |
緩存標記 | 0x17 |
緩存命中 | No |
返回緩存字節 | - |
9.12
A.虛擬地址0x03a9
13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 1 | 0 | 1 | 0 | 0 | 1 |
B.地址翻譯
參數 | 值 |
---|---|
VPN | 0x0E |
TLB索引 | 0x02 |
TLB標記 | 0x03 |
TLB命中 | No |
缺頁 | No |
PPN | 0x11 |
C.物理地址格式
11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 0 | 1 |
D.物理地址引用
參數 | 值 |
---|---|
字節偏移 | 0x1 |
緩存索引 | 0xA |
緩存標記 | 0x11 |
緩存命中 | No |
返回緩存字節 | - |
9.13
A.虛擬地址0x0040
13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
B.地址翻譯
參數 | 值 |
---|---|
VPN | 0x01 |
TLB索引 | 0x01 |
TLB標記 | 0x00 |
TLB命中 | No |
缺頁 | YES |
PPN | - |
9.14
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
int main()
{int fd;char *start;fd = open("hello.txt", O_RDWR, 0); //打開文件start = mmap(NULL, 1, PROT_WRITE, MAP_SHARED, fd, 0);close(fd); if(start == MAP_FAILED) return -1;//判斷是否映射成功(*start) = 'J';munmap(start, 1);return 0;
}
9.15
請求 | 塊大小 | 塊頭部 |
---|---|---|
malloc(3) | 8 | 0x9 |
malloc(11) | 16 | 0x11 |
malloc(20) | 24 | 0x19 |
malloc(21) | 32 | 0x21 |
9.16
對齊要求 | 已分配塊|空閑塊|最小塊大小|
---|---|---|---|
單字 | 頭部和腳部|頭部和腳部|16字節|
單字 | 頭部,但是沒有腳部|頭部和腳部|16字節|
雙字 | 頭部和腳部|頭部和腳部|16字節|
雙字 | 頭部,但是沒有腳部|頭部和腳部|16字節|
空閑塊至少是16
字節。已分配塊不需要pred
和succ
,所以這八個字節可以裝數據,加上頭和腳,就是16字節
,而且滿足單字雙字對齊。
9.17
我們需要修改find_fit
函數(之前的版本在practise
習題中寫過,書后有答案)。
代碼可以參考9.18
。
為此需要先定義一個全局的cur_point指針
,表示上次搜索之后指向哪個塊(有效載荷首地址)。
static void *find_fit(size_t asize)
{void *bp = cur_point;do{if( !GET_ALLOC(HDRP(bp)) && (asize <= GET_SIZE(HDRP(bp))) ) return cur_point = bp;bp = (GET_SIZE(HDRP(bp)) == 0) ? heap_listp : NEXT_BLKP(bp); }while(bp != cur_point);return NULL;
}
另外,需要釋放cur_point
指向的指針時,它可能和前面的空閑塊合并,我們應該將cur_point
指向前一個空閑塊首地址。在coalesce()
函數中需要做如下修改:
else if (!prev_alloc && next_alloc) { /* Case 3 */size += GET_SIZE(HDRP(PREV_BLKP(bp)));PUT(FTRP(bp), PACK(size, 0));PUT(HDRP(PREV_BLKP(bp)), PACK(size, 0));if(cur_point == bp) cur_point = PREV_BLKP(bp);bp = PREV_BLKP(bp);
}
else { /* Case 4 */size += GET_SIZE(HDRP(PREV_BLKP(bp))) +GET_SIZE(FTRP(NEXT_BLKP(bp)));PUT(HDRP(PREV_BLKP(bp)), PACK(size, 0));PUT(FTRP(NEXT_BLKP(bp)), PACK(size, 0));if(cur_point == bp) cur_point = PREV_BLKP(bp);bp = PREV_BLKP(bp);
}
9.18
需要考慮四個問題
:
初始化
的時候,序言塊
和結尾塊
是怎么樣的。
序言塊八字節8|0x11。
結尾塊四字節0|0x11.
- 為
堆
申請更多空間的時候(sbrk),如何更改結尾塊
記錄最后一個塊的
alloc
。結尾塊向后延伸申請的空間,并將剛多出的空間作為一個空閑塊。設置為
size|(alloc<<1)
。再合并該空閑塊。這里如何合并呢?需要判斷最后一塊是否已分配,可通過epilogue
來判斷。- 為
- 某個
空閑塊
匹配的時候,如何設置頭和下一塊的頭。
我們基于以下假設:某個空閑塊匹配,上一個和下一個一定不是空閑塊(否則可以合并)。
所以頭部就設置為(
asize|0x011
)。如果要分割,則下一塊的頭部設置為(
size-asize|0x010
),不用合并,因為再下一塊肯定是已經分配。- 某個
- 釋放某個塊時,如何合并。
檢查頭部,
alloc_prev
= 上一塊是否分配檢查下一個塊的頭部,
alloc_next
= 下一個塊是否分配。再根據那四種情況分別設置。
最后,如果下一塊已分配,則需要將下一塊頭部設置為(原頭部&(~0x010))。
另外,在mm_malloc
中,分配多大的塊也要發生相應的變化,因為現在最小塊大小可以是DSIZE
,而不是2*DSIZE
。
- 代碼已上傳至碼云
9.19
1)
a
; 對于伙伴系統,如果要申請大小為33
的空間,那么需要分配64
個空間。如果申請大小為65
的空間,那么塊大小就需要128
,所以最多可能有約50%
的空間被浪費。b
中,最佳適配要搜索所有空間,所以肯定比首次適配要慢一些。c
,邊界標記主要功能是釋放一個塊時,能立即和前后空閑塊合并。如果空閑塊不按順序排列的話,其實也能夠和前一個或者后一個空閑塊進行合并,但如果要和前后一起合并,可能會有些困難,那需要搜索前后塊在空閑鏈表中的位置,并且刪除一個再進行合并。可以參考P576,LIFO
方法。d
,其實任何分配器都可能有外部碎片,只要剩余的空閑塊大小和足夠但是單個都不夠,就會產生外部碎片
。2)
d
; 塊大小遞增,那么最佳適配法找到的塊和首次適配找到的塊是同一個,因為最佳適配總是想找一個剛好大于請求塊大小的空閑塊。a
,塊大小遞減,首次適配很容易找到,所以分配性能會很高。b
,最佳適配方法無論怎樣,都要搜索所有的鏈表(除非維護成塊大小遞增的鏈表)。c
,是匹配的最小的。3)
c
; 保守的意思就是所有可能被引用的堆都會被標記,int
像指針,所以可能認為它表示的地址是正在被引用的(實際上它只是個int
)。
9.20
不會……
教材學習中的問題及解決
在深入學習之前我和同伴是帶著這些疑問的,學習之后通過討論,對這些問題有了一點理解。
- 問題1:
Linux
虛擬地址空間如何分布? 問題1解決:
Linux
使用虛擬地址空間,大大增加了進程的尋址空間,由低地址到高地址
分別為:- 1、只讀段:該部分空間只能讀,不可寫;(包括:代碼段、
rodata 段(C常量字符串和
#define```定義的常量) ) - 2、
數據段
:保存全局變量、靜態變量的空間; - 3、
堆
:就是平時所說的動態內存,malloc/new
大部分都來源于此。其中堆頂的位置可通過函數brk 和 sbrk
進行動態調整。 - 4、
文件映射區域
:如動態庫、共享內存等映射物理空間的內存,一般是mmap
函數所分配的虛擬地址空間。 - 5、
棧
:用于維護函數調用的上下文空間,一般為8M
,可通過ulimit –s
查看。 - 6、
內核虛擬空間
:用戶代碼不可見的內存區域,由內核管理(頁表
就存放在內核虛擬空間)。
- 1、只讀段:該部分空間只能讀,不可寫;(包括:代碼段、
- 問題2:
64
位系統擁有2^64
的地址空間嗎? 問題2解決:
事實上,64
位系統的虛擬地址空間劃分發生了改變:- 1、地址空間大小不是
2^32
,也不是2^64
,而一般是2^48
。因為并不需要2^64
這么大的尋址空間,過大空間只會導致資源的浪費。64
位Linux
一般使用48
位來表示虛擬地址空間,40
位表示物理地址,這可通過/proc/cpuinfo
來查看 - 2、其中,
0x0000000000000000~0x00007fffffffffff
表示用戶空間,0xFFFF800000000000~ 0xFFFFFFFFFFFFFFFF
表示內核空間,共提供256TB(2^48)
的尋址空間。這兩個區間的特點是,第47
位與48~63
位相同,若這些位為0
表示用戶空間,否則表示內核空間。 - 3、用戶空間
由低地址到高地址
仍然是只讀段
、數據段
、堆
、文件映射區域
和棧
;
- 1、地址空間大小不是
- 問題3:如何查看進程發生缺頁中斷的次數?
問題3解決:
用
ps -o majflt,minflt -C program
命令查看。majflt
代表major fault
,中文名叫大錯誤,minflt
代表minor fault
,中文名叫小錯誤。這兩個數值表示一個進程自啟動以來所發生的缺頁中斷的次數。
- 問題4:發成缺頁中斷后,執行了那些操作?
問題4解決:
當一個進程發生缺頁中斷的時候,進程會陷入內核態,執行以下操作:- 1、檢查要訪問的
虛擬地址
是否合法 - 2、
查找/分配
一個物理頁 - 3、
填充
物理頁內容(讀取磁盤,或者直接置0,或者啥也不干) - 4、建立
映射關系
(虛擬地址到物理地址)
重新執行發生缺頁中斷的那條指令
- 1、檢查要訪問的
- 問題5:
堆內碎片
不能直接釋放,導致疑似“內存泄露”問題,為什么malloc
不全部使用mmap
來實現呢(mmap
分配的內存可以會通過munmap
進行free
,實現真正釋放)?而是僅僅對于大于128k
的大塊內存才使用mmap
? 問題5解決:
進程向
OS
申請和釋放地址空間的接口sbrk/mmap/munmap
都是系統調用,頻繁調用系統調用都比較消耗系統資源的。并且,mmap
申請的內存被munmap
后,重新申請會產生更多的缺頁中斷。例如使用mmap
分配1M
空間,第一次調用產生了大量缺頁中斷 (1M/4K 次
) ,當munmap
后再次分配1M
空間,會再次產生大量缺頁中斷。缺頁中斷是內核行為,會導致內核態CPU
消耗較大。另外,如果使用mmap
分配小內存,會導致地址空間的分片更多,內核的管理負擔更大
。同時堆是一個連續空間,并且堆內碎片由于沒有歸還
OS
,如果可重用碎片,再次訪問該內存很可能不需產生任何系統調用和缺頁中斷,這將大大降低CPU
的消耗。 因此,glibc
的malloc
實現中,充分考慮了sbrk
和mmap
行為上的差異及優缺點,默認分配大塊內存 (128k
) 才使用mmap
獲得地址空間,也可通過mallopt(M_MMAP_THRESHOLD, <SIZE>)
來修改這個臨界值。上周考試錯題總結
無
結對及互評
點評模板:
- 博客中值得學習的或問題:
- xxx
- xxx
- ...
- 代碼中值得學習的或問題:
- xxx
- xxx
- ...
- 其他
本周結對學習情況
-[20155318](http://www.cnblogs.com/lxy1997/)
- 結對照片
- 結對學習內容- 教材第九章內容- 跟著同伴回顧了第十二章的內容- ...
其他(感悟、思考等,可選)
通過這一章的學習,進一步加深了對虛擬存儲器的了解。
代碼托管
(statistics.sh腳本的運行結果截圖)
學習進度條
代碼行數(新增/累積) | 博客量(新增/累積) | 學習時間(新增/累積) | 重要成長 | |
---|---|---|---|---|
目標 | 5000行 | 30篇 | 400小時 | |
第一周 | 133/133 | 1/1 | 8/8 | |
第三周 | 159/292 | 1/3 | 10/18 | |
第五周 | 121/413 | 1/5 | 10/28 | |
第七周 | 835/3005 | 2/7 | 10/38 | |
第八周 | 1702/4777 | 1/8 | 10/48 | |
第九周 | 1664/6441 | 3/11 | 10/58 | |
第十一周 | 300/6741 | 3/14 | 10/68 | |
第十三周 | 743/7484 | 2/16 | 10/78 |
嘗試一下記錄「計劃學習時間」和「實際學習時間」,到期末看看能不能改進自己的計劃能力。這個工作學習中很重要,也很有用。
耗時估計的公式
:Y=X+X/N ,Y=X-X/N,訓練次數多了,X、Y就接近了。
參考:軟件工程軟件的估計為什么這么難,軟件工程 估計方法
計劃學習時間:15小時
實際學習時間:10小時
改進情況:
(有空多看看現代軟件工程 課件
軟件工程師能力自我評價表)
參考資料
- 《深入理解計算機系統V3》學習指導
- ...