作 者:道哥,10+年嵌入式開發老兵,專注於:C/C++、嵌入式、Linux。
關注下方公眾號,回復【書籍】,獲取 Linux、嵌入式領域經典書籍;回復【PDF】,獲取所有原創文章( PDF 格式)。
【IOT物聯網小鎮】
目錄
終於開始介紹分頁機制了,作為一名 Linuxer
,大名鼎鼎的分頁機制必須要徹底搞懂!
我就盡自己的最大努力,正確把我理解的分頁機制,用圖文形式徹底分解,希望對您有所幫助!
一共分 3 篇文章:
這篇文章主要介紹單映射表;
下一篇介紹兩級映射(頁目錄和頁表);
最后一篇介紹對映射表自身的操作。
分段存儲的壞處
在之前的文章中,我們多次描寫了一個段描述符的結構,其中就包括段的開始地址、界限和各種段的屬性。
經過分段處理單元的權限檢查和計算,這個開始地址加上偏移量,就是一個線性地址,如下圖所示:
在 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 位
),使用兩個表來轉換,這個問題下一篇文章會詳細聊。
本文描述了:通過一個映射表,把連續的虛擬內存,映射到離散的物理頁,極大的利用了物理內存。
當操作系統需要分配一大塊、連續的內存空間給用戶程序時,映射表中的表項可以指向多個不連續的物理頁,反正用戶程序接觸不到這一層(用戶程序只與虛擬內存打交道)。
這樣利用物理內存的效率就極大的提高了。
再加上換出和換入機制(把硬盤當做物理內存來用),讓用戶程序以為有用不完的物理內存。
同時,我們也討論了這個單一映射表的壞處,那就是映射表本身也占用了4MB
的物理內存空間。
為了解決這個問題,偉大的先驅者們又引入了多級映射表,那就是頁目錄表和頁表,我們下一篇文章再見!
如果這篇文章對您有小小的幫助,請轉發給身邊的小伙伴,讓我們一起進步!
推薦閱讀
【1】C語言指針-從底層原理到花式技巧,用圖文和代碼幫你講解透徹
【2】一步步分析-如何用C實現面向對象編程
【3】原來gdb的底層調試原理這么簡單
【4】內聯匯編很可怕嗎?看完這篇文章,終結它!
其他系列專輯:精選文章、C語言、Linux操作系統、應用程序設計、物聯網