在正式講述虛擬記憶體之前需要提及存儲器的層級結構以及進程在記憶體中的結構。 存儲器的層級結構速度從快到慢排列如下 寄存器——L1高速緩存——L2高速緩存——L3高速緩存——主存——磁碟——分散式文件系統 而成本也是從高到低,空間是從低到高。 兩個相鄰的存儲設備,前者往往是充當後者的高速緩存,後者往往存儲 ...
在正式講述虛擬記憶體之前需要提及存儲器的層級結構以及進程在記憶體中的結構。
存儲器的層級結構速度從快到慢排列如下
寄存器——L1高速緩存——L2高速緩存——L3高速緩存——主存——磁碟——分散式文件系統
而成本也是從高到低,空間是從低到高。
兩個相鄰的存儲設備,前者往往是充當後者的高速緩存,後者往往存儲比前者更完整的數據。
後面的內容會涉及到高速緩存和主存。
為了簡單的理解可以假設主存是一個線性的數組。每個元素可以是一個位元組。而主存與硬碟或者與高速緩存間做數據傳輸時,每次可以傳輸若幹個位元組,我們可以把這若幹個位元組定為一個新的單位,叫"塊"。而高速緩存中每個存儲的元素則是一個塊。高速緩存的存儲結構
如上圖所示,高速緩存被分成S個組,每個組裡面有E行,每行都有一個有效位,t個標記為和B個位元組,這B個位元組就組成了之前提到的塊。並且和記憶體地址有這樣的一個關係m=t+s+b。即有t個標記位,s個位作為組索引,b個位作為塊偏移,並且是t,s,b是從高位到低位排列。這個關係決定了給定一個地址,它該存放在高速緩存中的哪個位置。
另外整個高速緩存的總容量是C=S*B*E;按照S和B的變換可以把高速緩存分成下麵三種類型
1.直接映射
2.組相聯
3.全相聯
直接映射和全相聯是兩種極端情況。
直接映射是緩存中每個組只有1行,即E=1。對於這種情況,當某個當某個地址在緩存中發生衝突時則直接替換。
全相聯是緩存中只有一個組,所有行都在這個唯一的組裡面,相當於S=1。這種情況所有緩存都放在一個組裡面,然後輪詢找出哪一行是未使用的,如果全部組都已經使用了,那就按照一定的演算法找出一個塊踢出,再放上新的塊。
組相連則是介於上面兩種情況,每個組最少有兩行。這種情況跟全相聯一樣,不命中的時候在組裡面找不到未使用的塊,也是需要有策略選出一個犧牲的塊,替換上新的。
下麵則用直接映射來模擬一下讀取高速緩存的過程
現在有一個直接映射的高速緩存如下
(S,E,B,m)=(4,1,2,4)
由此可以得出s=2,b=1,t=m-b-s=4-1-2=1,整個地址構成如下
t=1 |
s=2 |
b=1 |
所有地址的標記位,組索引和塊偏移如下表所示,另外給記憶體中的塊編上一個序號
高速緩存如下
假設現在讀地址0的字,按照上面的表,組塊索引是0,標記位是0,緩存不命中,需要從記憶體中讀取塊0寫入高速緩存,因此m[0]和m[1]會被寫入組0,標記位是0,有效位置1,寫後高速緩存如下
接著讀地址1的字,記憶體地址是0001,組索引為0,標記位是0,查找高速緩存,組號和標記位對上,有效位是1,取偏移是1的值m[1]
接下來讀地址8的字,記憶體地址是1001,組索引是0,標記位是1,查找高速緩存不命中,而且該組已經被用上了,犧牲了原本的0組的兩個值m[0]和m[1],讀取新的值m[8]和m[9],塊偏移是1,取值m[8]
假設再讀地址0的字時,又會發現不命中,重覆類似第一次讀地址0的操作。
接下來補充鏈接的少部分的內容,這些內容會在記憶體映射和記憶體分配的時候會涉及到
上面的這幅圖在眾多C語言的書都有出現,其他語言的都有類似的流程圖。一個C文件經過預處理和編譯,得出了一份彙編語言的源碼文件。這個文件經過彙編器彙編之後,就產生了人類無法直接閱讀的可重定向目標文件。這個文件具有一定的結構,
它會把原本程式員寫的源代碼拆分成若幹部分:指令放到.text節;全局靜態變數放到.data節;內部定義的函數名、引用外部函數名這些符號信息放到.symtab節……但是外部引用的函數地址(如我們最常用的printf)在這個階段還是沒有的,
這些需要引用地址的信息都放到一個叫"可重定向條目"的結構裡面,此時需要往下走到鏈接這個過程。
最後鏈接器鏈接時會把這些外部引用的函數地址給填上去,
最終生成了像上圖結構的可執行文件,常見的是windows的exe文件。
可執行文件結構如此,一個程式運行後,進程在記憶體中的結構是如下這樣子。
這幅圖跟可執行文件的結構較為相似,底部是低地址,地址往上遞增。低地址處是代碼區.text已初始化數據區.data未初始化數據區.bss。這幾個區是在程式運行的時候通過記憶體映射附加上去的。在高地址處有個區域是用戶棧,這個棧棧底在高地址處,棧頂在低地址處。棧頂和棧底由兩個指針記錄他們的地址,指針的值存放在CPU寄存器中,分別是%rbp和%rsp。用戶棧的作用有兩個,其一是存放函數調用的棧幀,其二是存放函數調用期間所用到的臨時變數。另外一個區域是位於整個空間中部的運行時堆,這個運行時堆主要是用於給程式申請記憶體空間的時候用的,即調用malloc的時候。動態記憶體分配也屬於虛擬記憶體範疇的內容,但本篇暫不作介紹。
下麵則進入虛擬記憶體範疇的內容。從物理記憶體講起,因為物理記憶體大家較為熟悉,一個記憶體上分配了若幹的存儲空間,每個存儲空間都有一個唯一能定位到它的地址稱為物理地址。把一個物理地址傳給記憶體控制器就能往指定地址的記憶體空間上讀/寫數據。
而虛擬記憶體則是現代操作系統對記憶體做了一層抽象。與物理地址對應,虛擬記憶體上的每個存儲空間的地址稱為虛擬地址。儘管是虛擬地址,最終提供或者存放數據的地方還是物理地址,在這個過程中虛擬地址會同通過一個在CPU上的名為記憶體管理單元(簡稱MMU)的硬體翻譯成物理地址,這個過程就叫做地址翻譯。在之前可執行文件的彙編語言截圖中的地址是虛擬地址,進程的記憶體結構中的幾個地址都是虛擬地址。
虛擬記憶體實際是存放在磁碟中的,在該空間中劃分了一段一段的連續的空間,這個空間叫作一個"頁",與之前高速緩存中的"塊"概念類似。同樣地,在記憶體中也有一樣的分頁機制。這個頁就用於主存跟磁碟間相互傳遞數據的最小單元。一個虛擬頁被存放到物理記憶體中,這個行為也叫作緩存。磁碟空間比物理記憶體空間要大得多,同樣地虛擬地址空間也會比用作虛擬頁的空間要大。通常一個虛擬頁的狀態有三個
1.未分配:實際上該頁是未存在的,在磁碟上未開闢空間;
2.已分配:與未分配對應,已經在磁碟上開闢了空間;
3.已緩存:該頁已經被分配了,並已經被存放到物理記憶體中。
還有一個比較重要的結構叫做頁表,頁表中每個元素叫作頁表條目(簡稱PTE),PTE的數量與虛擬頁數量相同。頁表是存放在物理記憶體中。每個PTE都有一個有效位,當有效位為0時,表明該頁未分配;當有效位為1是,PTE中存放的要麼是虛擬頁的頁號,要麼是已經緩存到物理記憶體的物理地址。
CPU平時只能往物理記憶體讀寫數據,當剛好取到的頁屬於已分配狀態,就會引發缺頁異常,這時候系統會從物理記憶體中選出一頁作為犧牲頁,把PTE指回虛擬記憶體的頁號,把新的頁從磁碟載入到物理記憶體,PTE指向物理記憶體的地址中。
之前也有提及從虛擬地址轉換得出物理地址的過程叫地址翻譯,由CPU裡面的MMU執行。下麵則來講述這個過程。
如上圖所示,一個虛擬地址可以劃分成兩部分,低的p位作為虛擬頁的偏移量,而剩下部分是作為虛擬頁的頁號,實質上就是PTE在頁表上的索引而已。同樣物理地址也是分了兩部分,物理頁偏移量和物理頁號,有個額外的規則是物理頁的偏移量=虛擬頁的偏移量。這裡P=2p,P是頁的大小,位元組為單位。各種量的簡稱如上圖所示。地址翻譯過程的簡述如下:
1.給定一個虛擬地址,劃分出虛擬頁偏移量和虛擬頁號
2.通過頁表基址寄存器找出頁表,找出對應頁號的PTE
3.看從PTE得出頁是否已緩存,不緩存則要找出犧牲頁替換,得出物理頁號
4.把物理頁號和虛擬頁偏移量組合成物理地址
在上述過程中的第三部查找頁表搜出PTE在整個過程中較為耗時,因為涉及到訪存。假設這部分操作利用上緩存則會節省一部分的時間。TLB正是解決了這個問題,TLB叫翻譯後備緩存器(Translation Lookaside Buffer),存放在MMU中。有了這種機制一個虛擬地址的結構將再次被劃分,這次劃分的區域是虛擬頁號
下麵通過一個例子,結合主存,高速緩存,PTE,TLB,頁表來模擬這個地址翻譯的過程
TLB,頁表,高速緩存入下麵所示
現在要求出虛擬地址0x03d4的值。
按照前面的條件得出
p=6,所以VPN=14-6=8;
TLB是4路組相聯,所以TLB索引占兩位,剩餘的標記位是TLBT=VPN-TLBI=8-2=6
虛擬地址可以拆成
TLB行數是3,標記位是03的命中,PPN是0D,PPN是12-6=6位,因為PPO=VPO,所以重新組織後物理地址是0x354,MMU的地址翻譯就到此結束了,。
高速緩存是直接映射,行大小是4位元組,一共有16個組
所以得出以下信息
B=4=2^2,b=2;
S=16=2^4,s=4;
t=m-s-b=12-4-2=6
高速緩存索引號是5,標記位0D命中,偏移是0。最終結果虛擬地址0x03d4的值是0x36。
假設上面TLB不命中,虛擬頁號是0x0F的,查頁表也得出有效位是1,PPN是0x0D。
記憶體映射
記憶體映射與虛擬記憶體極為相似,也利用了虛擬記憶體。這兩者也是利用了頁表。記憶體映射是與磁碟上的文件載入有關的。比如我們的可執行文件載入到記憶體,或者一些像libc.so共用庫,也或者是普通一個txt。
當一個文件要載入到記憶體中時,實際上在虛擬記憶體中開闢了一開虛擬記憶體空間,這個區域的地址是指向了一個頁表,這個頁表類似於虛擬記憶體那樣與磁碟的內容進行關聯。比如現在要讀取某共用庫的一個地址的內容,該地址是位於進程的虛擬記憶體的共用庫的區域內,而這個地址指向的是一個記憶體映射的頁表PTE。如同虛擬定址那樣,如果該PTE是無效的或者是指向文件中的某個地址,則會發生缺頁異常,然後會就在物理記憶體中選中一個犧牲的物理頁替換成這個待讀取的頁。載入到物理頁完畢後,就可以在物理頁中讀取想要的數據。
記憶體映射有利於節省記憶體空間,現在如libc.so這樣的文件載入到記憶體中,如果每個進程都載入一份到記憶體中那回造成浪費,現在利用記憶體映射,每個進程都可以把他們共同需要的libc.so映射到同一片的記憶體區域。
。另外一種場景,假設兩個進程A和B都打開了某個文件,現在有一個進程B需要寫文件的內容,但是另外一個A進程完全不能受到這次寫的影響,那進程B則會在物理記憶體中拷貝一份跟原有頁一樣的內容,把虛擬記憶體指向到這個新的頁。
用戶態的記憶體映射實際上利用了一個叫mmap的函數。函數的定義如下
void* mmap(void* start ,size_t length , int prot , int flags , int fd , off_t offset);
Start是待映射的虛擬記憶體的起始地址
length是映射的長度
fd是文件描述符,指定了磁碟文件中的起始地址
offset是最後一個參數,他指定了開始映射的地址距離文件起始地址的偏移量。
prot是映射後虛擬記憶體的訪問許可權,這個許可權是可讀可寫之類的
flag指定了映射的對象類型,這些類型是匿名對象,共用對象,私有對象之類的。
本篇屬於個人學習完的總結。文中有摘抄了網上或書籍中的圖片。如有錯誤的請及時指出,以便本人及時更正。謝謝