前言
最近看了《深入理解計算機系統》,重溫了許多操作系統和組原的知識。本篇博客主要介紹虛擬內存,讓我們先從ELF-->進程引入。
ELF文件
對於每個程序,其在經歷預處理、編譯、彙編之後,都要經過鏈接器將其鏈接成一個單一的可執行文件。在現在Unix和x86-64 Linux系統上,其使用的可執行格式爲ELF,如下:
可以看到ELF涵蓋了程序中的各種信息,加載器就是通過讀取ELF文件中的數據和代碼,將其從磁盤複製到內存中,生成相應的進程並跳轉到第一條指令或入口點來運行該程序。
進程
進程地址空間如下圖所示:
可以看到,只讀代碼和數據對應於elf文件的(.init,.text,.rodata),讀寫段對應於elf文件的(.data,.bss)。
問題來了,如果我們有很多個程序要運行,所需內存已經超過了我們物理內存的容量,此時該怎麼處理呢?
其實,當程序運行時,如果內存空間足夠大,操作系統會按分頁機制,將程序調入內存中。否則,操作系統會分批將程序的部分內容調入內存,再通過磁盤上的虛擬內存來實現內存置換,達到按需加載的目的。
到這裏對虛擬內存有一定的概念了,似乎其作用就是“虛擬地擴充我們的內存”。
虛擬內存
由以上我們引入虛擬內存。
什麼是虛擬內存
爲了更加有效地管理內存,操作系統對主存提出了一種抽象的概念:虛擬內存。其通過硬件+軟件的支持,爲進程提供了更大的、一致的和私有的地址空間。虛擬內存主要提供一下三個能力:
-
將主存看成是一個存儲在磁盤上的地址空間的高速緩存,在主存中只保存活動區域,並根據需要在磁盤和主存之間來回傳送數據,通過這種方式,它高效地使用了主存;
-
它爲每個進程提供了一致的地址空間,從而簡化了內存管理;
-
它保護了每個進程的地址空間不被其他進程破壞。
物理和虛擬尋址
物理尋址
首先輸入一條加載指令,當CPU執行這條加載指令時,會生成一個有效物理地址,通過內存總線,把它傳遞給主存。主存取出從物理地址4開始的字節,並將它返回給CPU,CPU會將其放到一個寄存器中。
早期的PC就是使用物理尋址。
虛擬尋址
現代PC多使用虛擬尋址的方式。
使用虛擬尋址,CPU通過生成一個虛擬地址來訪問主存,這個虛擬地址在被送到內存之前先轉換成適當的物理地址。將一個地址從虛擬地址轉換成物理地址的過程稱爲地址翻譯。CPU芯片上存在MMU(Memory Management Unit)內存管理單元的專用硬件,其通過內存中的進程頁表來動態翻譯地址。
-
頁表是什麼?
每個進程都有各自的頁表,用來實現虛擬頁到物理頁的映射。
-
頁表存儲在哪裏?
單級頁表存儲在內存中,多級內表最高級頁表於內存,其他需加載。
-
頁表從哪裏來?
在每個程序被加載時,其相應的頁表就產生了(內存映射,下文會提及)。
頁表
地址翻譯過程由操作系統軟件、MMU中的地址翻譯硬件和頁表實現。
頁表:將虛擬頁映射到物理頁。
頁表就是一個頁表條目(Page Table Entity:PTE)的數組。每個PTE由一個有效位和一個N位地址組成。有效位表明該虛擬頁是否被緩存在物理內存中。如果設置了有效位,那麼地址字段就表示物理頁的起始位置。如果沒有設置有效位,如果地址位非空則這個地址指向虛擬頁在磁盤上的起始位置,空就表明此虛擬也還未被分配。
設想一下幾種情景
頁命中
當CPU要讀取VP1地址上的字節時,地址翻譯硬件將虛擬地址定位到VP1,發現其爲cached狀態,此時可以直接從內存中讀取到PP1。
缺頁
當CPU引用VP3時,發現其爲uncached狀態,此時觸發缺頁異常,缺頁異常由內核處理,內核中的缺頁異常處理程序會從物理內存中選擇一個犧牲頁,將VP3存儲在此犧牲頁的內存空間上,並修改頁表,如果此犧牲頁被修改過,需將其複製寫回磁盤。當異常程序返回時,它會從重新啓動導致缺頁的指令,此時VP3已在主存中了,那麼頁將命中,如情景1。
malloc
當程序調用malloc時,需要分配一個新的虛擬頁面,此時我們在磁盤上創建空間VP5並更新PTE5,讓其指向磁盤上的VP5。
注意:多個虛擬頁面可以映射到同一個共享物理頁面上。
虛擬內存:內存管理工具
虛擬內存是一個有用的機制,因爲其大大地簡化了內存管理。
-
簡化鏈接。獨立的地址空間允許每個進程的內存映像使用相同的基本格式,而不管代碼和數據實際存放在物理內存的何處。
-
簡化加載。虛擬內存使得容易向內存中加載可執行文件和共享對象文件。
-
簡化共享。獨立地址空間爲操作系統提供了一個管理用戶進程和操作系統自身之間共享的一致機制。
-
簡化內存分配。虛擬內存向用戶進程提供一個簡單的分配額外內存的機制。當一個用戶程序要求額外的堆空間時候,操作系統分配 k 個適當的連續的虛擬內存頁面,並且將他們映射到物理內存的中的 k 個任意頁面,操作系統沒有必要分配 k 個連續的物理內存頁面。
虛擬內存:內存保護工具
在頁表中,可以該頁表項添加控制位,來實現權限的控制。例如,每個 PTE 添加SUP、WRITE、EXRC控制位, SUP 位表示進程是否必須運行在超級用也就是內核模式下才能訪問該頁,WRITE 位控制頁面的寫訪問, EXRC 位控制頁面的執行。如果有指令違反了這些控制條件,那麼 CPU 會觸發一個一般保護故障,將控制傳遞給內核中的異常處理程序。
虛擬內存地址空間
在進程地址空間的圖解中,最高位的地址爲留給內核使用的虛擬內存,其中包含了每個進程都一樣的內核代碼和數據+物理內存,以及對每個進程都不相同的進程相關結構(與頁表、task和mm結構,內核棧等)。
linux虛擬內存區域
linux內核爲每個進程維護一個單獨的任務結構(task_struct),task_struct是linux下的PCB(進程控制塊),其中包含進程運行時所需的各種信息(pid、指針、elf的名字等)。在其中有一個條目指向mm結構體。
mm_struct,其描述了虛擬內存的當前狀態。其中具有兩個重要的指針:pgd和mmap。其中pgd指向第一級頁表的基址,而mmap指向vm_area_structs的鏈表,其中鏈表每個節點表示當前虛擬內存的一個區域。
節點T包含以下字段:
-
vm_start:區域起始處
-
vm_end:區域結束處
-
vm_port:區域內所有頁的權限
-
vm_flags:區域內頁面是否共享
-
vm_next:下一個區域
linux缺頁異常處理
當MMU發現某虛擬地址並未在物理內存中時,觸發缺頁中斷。
此時缺頁異常處理程序會經過以下步驟:
-
檢查虛擬地址是否合法。通過當前區域,與鏈表節點中的vm_start+vm_end比較(在實際中,linux在鏈表中構造了一顆紅黑樹,並在這棵樹實現快速的查找,避免O(n)時間複雜度的遍歷)。
-
檢查內存訪問是否合法。即進程是否有此區域的訪問權限。
-
當檢查通過後,內核將選擇一個犧牲頁面(判斷其是否被修改過,修改過則寫回磁盤),然後換入新的頁面並更新頁表。當程序返回時,再次調用此指令,下次將不會存生缺頁中斷。
內存映射
回到之前頁表從哪裏來的問題?
linux將一個虛擬內存區域與磁盤上對象關聯的過程,稱爲內存映射。
其包括:文件映射、匿名映射。
文件映射
一個區域可以映射到磁盤文件上的連續部分,文件區被劃分成頁大小的片,每一片包含一個虛擬頁面的初始內容。因爲按需進行頁面調度,所以這些虛擬頁面沒有實際交換進入物理內存,指導CPU第一次引用到頁面。如果區域比文件區大,則用零來填充區域剩下的部分。
用戶可以在用戶空間通過open\read\write等函數來操作文件內容。
匿名映射
匿名文件是內核創建的。包含的全是二進制零。因爲其不屬於任何文件,CPU第一次引用此區域的虛擬頁面時,內核就在物理內存中找合適的犧牲頁面,將其存儲到物理內存上。
即用戶空間對malloc分配的內存,如果不進行實際的訪問,內核只分配虛擬地址,而不將虛擬地址通過映射與物理地址一一對應,只有CPU實際訪問了這些區域,才真正發生匿名映射,也才發生缺頁處理。
-
CPU第一次引用匿名區域時?
由於此時匿名區域只有虛擬地址,沒有和物理內存相關聯,內核就產生一個缺頁錯誤,缺頁異常處理程序從swap區獲得一塊物理內存與頁映射,這裏虛擬內存就與物理內存掛鉤,產生相應的頁表,頁表相關信息存儲在TLB或CACHE中以實現虛擬地址到物理地址的轉換。
-
SWAP?
swap分區就是linux中的虛擬內存,是linux中一種內存策略。
swap涉及的換頁,就是將不常使用的匿名頁置換到磁盤(虛擬內存)上。
注意:文件映射與虛擬文件系統相關,其有自己的緩衝和緩存,當需要這些程序的內存置換時,直接回放文件(不與swap打交道)所有文件映射的物理頁幀不來自於swap區域。對於malloc生產的對象數據,由於缺乏存儲文件空間,它們需要swap空間。
簡而言之,swap區域就是匿名映射的交換空間,這個區域由內核創建和管理。
總結
以上就是近期看黑皮書的一些總結,計算機系統很多底層的知識自己還是瞭解地比較淺顯,趁着畢業前這段空檔期還是需要及時補補。
由於本人才疏學淺,如有不足之處,還望看官們指出!
最後,還是推薦大家自己去看看《深入理解計算機系統》這本書,包羅計算機中的萬象,本人也是受益良多。
參考
《深入理解計算機系統》——布賴恩特(Bryant,R.E.)
https://bbs.csdn.net/topics/391860037——大神Buddy.Zhang的解答