看了這篇文章還沒徹底搞懂Linux分頁機制?我自罰三杯!
目錄
- 分段存儲的壞處
- 物理內存的管理
- 映射表
- 一個線性地址的尋址過程
終于開始介紹分頁機制了,作為一名 Linuxer,大名鼎鼎的分頁機制必須要徹底搞懂!
我就盡自己的最大努力,正確把我理解的分頁機制,用圖文形式徹底分解,希望對您有所幫助!
一共分三篇文章:
- 這篇文章主要介紹單映射表;
- 下一篇介紹兩級映射(頁目錄和頁表);
- 最后一篇介紹對映射表自身的操作。
分段存儲的壞處
在之前的文章中,我們多次描寫了一個段描述符的結構,其中就包括段的開始地址、界限和各種段的屬性。
經過分段處理單元的權限檢查和計算,這個開始地址加上偏移量,就是一個線性地址,如下圖所示:
在 x86 系統中,分段機制是固有的,必須經過這個環節才能得到一個線性地址。
所以 Linux 系統中,為了“不使用”分段機制,但是又無法繞過,只好定義了“平坦”的分段模型。
在沒有開啟分頁機制的情況下,分段單元輸出的線性地址就等于物理地址。
這里就存在著一個重要的問題:從段的開始地址,一直到段空間的最后地址,這是一塊連續的空間!
在這樣的情況下,每一個用戶程序中,包含的所有段,在物理內存上所對應的空間也必須是連續的,如下圖:
因為每一個程序的代碼、數據長度都是不確定、不一樣的,按照這樣的映射方式,物理內存將會被分割成各種離散的、大小不同的塊。
經過一段運行時間之后,有些程序會退出,那么它們占據的物理內存空間可以被回收,但是,這些物理內存都是以很多碎片的形式存在。
如果這個時候操作系統想分配一塊稍微大一些的連續空間,雖然空閑的物理內存空間總數是足夠的,但是不連續啊,這就給物理內存帶來極大的浪費!
怎么辦?
現在的需求是:操作系統提供給用戶的段空間必須是連續的,但是物理內存最好不要連續。
軟件領域有一句經典名言:沒有什么是不能通過增加一個抽象層解決的!
在內存管理上,新加的這一層就是虛擬內存:把物理內存按照一個固定的單位(4 KB,稱作一個物理頁)進行分割,然后把連續的虛擬內存,映射到若干個不連續的物理內存頁。
圖中綠色的的映射表,就是用來把虛擬內存,映射到物理內存。
物理內存的管理
關于映射表的細節,下一個主題再聊,先來看一下操作系統對物理內存的狀態管理。
在如今的一臺 PC 機上,內存動輒就是是 8G/16G/32G 的配置,好像很充裕、隨便用。
但是在 N 年以前,買一個 U 盤都是按照 MB 為單位的,更別說內存了。
因此在那個時代,面對 MB 級別的物理內存,操作系統還能夠把它虛擬成 4GB 的內存空間給用戶程序使用,也是挺厲害的!
言歸正傳,在這篇文章中,我們就奢侈一點,假設可用的物理內存有 1GB 的空間。
當系統上電之后,BIOS 會檢查系統的各種硬件資源,并告訴操作系統,其中就包括這 1GB 的物理內存。
按照一個物理頁的大小 4KB 進行劃分,1 GB 的空間就是 262144 (1GB / 4K)個物理頁。
操作系統需要對這些頁進行管理,也就是維護它們的狀態:哪些頁正在被使用,哪些頁空閑。
最簡單、直觀的方法,就是用一塊連續的內存空間來描述每一個物理頁的狀態,每一個bit位對應一個物理頁:
bit = 1: 表示該物理頁被使用;
bit = 0:表示該物理頁空閑;
262144 個頁需要262144個bit位,也就是32768個字節。
那么對于1 GB大小的物理內存來說,如下圖所示:
利用map結構,操作系統就知道當前: 哪些物理頁正在被使用,哪些物理頁是空閑的。
每一個物理頁是 4KB,所以地址中最后 12 個 bit 都是 0;
map 結構本身也需要存儲在物理內存中的,因此 32768 個字節,一共需要 8 個物理頁來存儲(32768 / 4 * 1024 = 8)。
映射表
在32位系統中,虛擬內存的最大空間是 4GB,這是每一個用戶程序都擁有的虛擬內存空間。
實際上,操作系統都會把虛擬內存的高地址部分,用作操作系統,低地址部分留給用戶程序使用;
Linux 系統中,高地址的 1GB 空間是操作系統使用;Windows 系統中,高地址的 2GB 的空間被操作系統使用,但是可以調整;
但是,實際的物理內存只有1GB(假設值),那么操作系統就要使用自己的騰挪大法,讓用戶程序認為4GB的內存空間全部可用。
就好比變戲法一樣:十個碗,九個蓋,誰能玩的溜、不露餡,誰就是高手!
計算一下映射表本身所占據的空間大小:
映射表中的每一個表項,指向一個物理頁的開始地址。
在32位系統中,地址的長度是4個字節,那么映射表中的每一個表項就是占用4個字節。
既然需要讓4GB的虛擬內存全部可用,那么映射表中就需要能夠表示這所有的4GB空間,那么就一共需要1048576 (4GB / 4KB)個表項。
所以,映射表占據的總空間大小就是:1048576 * 4 = 4 MB 的大小。
也就是說,映射表自己本身,就要占用 1024 個物理頁(4MB / 4KB)。
正是因為使用一個映射表,需要占用這么大的物理內存空間,所以才有后面的多級分頁機制。
虛擬內存看上去被虛線“分割”成4KB的單元,其實并不是分割,虛擬內存仍然是連續的。
這個虛線的單元僅僅表示它與映射表中每一個表項的映射關系,并最終映射到相同大小的一個物理內存頁上。
例如:
虛擬內存的 0 ~ 4KB 空間,對應映射表第 0 個表項中,其中存儲的物理地址是 0x3FFF_F000(最后一個物理頁);
虛擬內存的 4KB ~ 8KB 空間,對應映射表第 1 個表項中,其中存儲的物理地址是 0x0000_0000(第 0 個物理頁);
虛擬內存的最后 4KB 空間,對應映射表最后一個表項中,其中存儲的物理地址是 0x0000_1000(第 1 個物理頁);
也就是說:
虛擬內存與映射表之間,是平行的一一對應關系;
映射表中的物理地址,與物理內存之間,是隨機的映射關系,哪里可用就指向哪里(物理頁)。
以上就是用一個映射表,把物理內存以4KB為一個頁進行分配,然后再與虛擬內存對應起來,包裝成連續的虛擬內存給用戶使用。
雖然最終使用的物理內存是離散的,但是與虛擬內存對應的線性地址是連續的。
處理器在訪問數據、獲取指令時,使用的都是線性地址,只要它是連續的就可以了,最終都能夠通過映射表找到實際的物理地址。
為了有一個更加感性的認識,我們再來看一個稍微具象一點的實例。
一個線性地址的尋址過程
我們假設用戶程序中有一個代碼段,那么在這個程序的 LDT(局部描述符表)中,段描述的結構如下:
假設條件如下:
虛擬內存(32位系統):4GB,實際的物理內存 1GB;
代碼段的開始地址位于 3 GB 的地方,也就是 0xC000_0000;
代碼段的長度是 1 MB;
我們的目標是:查找線性地址0xC000_2020所對應的物理地址。
根據描述符的結構,其中的段基地址是 0xC000_0000,界限是 0x00100,段描述符中,其它的字段暫時不用關心。
界限一共有 20 位,假設粒度是 4KB,那么 1 MB 的長度除以 4KB,結果就是 0x00100。
代碼段的開始地址(線性地址) 0xC000_0000,位于虛擬內存靠近高端四分之一的位置,那么映射表中對應的表項,也是位于高端的四分之一的位置。
映射表中每一個表項指向一個4KB大小的物理頁,那么長度為1MB的代碼段,就需要256個表項。
也就是說映射表中有 256 個表項,指向256個物理頁:
對于我們要查找的線性地址 0xC000_2020,首先把它拆解成兩部分:
高 20 位 0xC0002: 是映射表索引;
低 12 位 0x020: 是物理頁內的偏移地址;
索引值 0xC002,對應于下圖中從3GB開始的第2個表項:
在上面這個示意圖中,代碼段的開始地址 0xC000_0000,對應于映射表中索引為0xC0000這個表項,這個表項中記錄的物理內存頁開始地址是 0x1000_0000(距離開始地址 256 MB)。
代碼段的長度是 1 MB,一共需要256個表項,那么最后這個表項的索引就應該是 0xC00FF。
那么對于我們要尋找的線性地址 0xC000_2020,對應的表項索引號是 0xC0002,這個表項中記錄的物理內存頁的開始地址是 0x2000_0000(距離開始地址 512 MB)。
找到了物理內存的起始地址,再加上偏移量 0x020,那么最終的物理地址就是:0x2000_0020。
以上就是通過映射表,從線性地址到物理地址的頁轉換過程。
對于使用二級頁表的轉換機制來說,原理都是一樣的。無非是把高20位的索引拆開(10 位 + 10 位),使用兩個表來轉換,這個問題下一篇文章會詳細聊。
End
本文描述了:通過一個映射表,把連續的虛擬內存,映射到離散的物理頁,極大的利用了物理內存。
當操作系統需要分配一大塊、連續的內存空間給用戶程序時,映射表中的表項可以指向多個不連續的物理頁,反正用戶程序接觸不到這一層(用戶程序只與虛擬內存交互)。
這樣利用物理內存的效率就極大的提高了。
再加上換出和換入機制(把硬盤當做物理內存來用),讓用戶程序以為有用不完的物理內存。
同時,我們也討論了這個單一映射表的壞處,那就是映射表本身也占用了4MB的物理內存空間。
為了解決這個問題,偉大的先驅者們又引入了多級映射表(頁目錄表和頁表),我們下一篇文章再見!
本文轉載自微信公眾號「IOT物聯網小鎮」,可以通過以下二維碼關注。轉載本文請聯系IOT物聯網小鎮公眾號。






































