Linux從頭學15:【頁目錄和頁表】-理論 + 例項 + 圖文的最完全、最接地氣詳解

IOT物聯網小鎮發表於2021-10-11

作 者:道哥,10+年嵌入式開發老兵,專注於:C/C++、嵌入式、Linux

關注下方公眾號,回覆【書籍】,獲取 Linux、嵌入式領域經典書籍;回覆【PDF】,獲取所有原創文章( PDF 格式)。

【IOT物聯網小鎮】

目錄

x86系統中,為了能夠更加充分、靈活的使用實體記憶體,把實體記憶體按照4KB的單位進行分頁。

然後通過中間的對映表,把連續的虛擬記憶體空間,對映到離散的實體記憶體空間。

對映表中的每一個表項,都指向一個物理頁的開始地址。

但是這樣的對映表有一個明顯的缺點:對映表自身也是需儲存在物理記憶體中的。

在 32 位系統中,它使用了多達4MB的實體記憶體空間(每個表項4個位元組,一共有4G/4K個表項)。

為了解決這個問題,x86處理器使用了兩級轉換:頁目錄和頁表

這篇文章,我們就從最基礎的底層計算過程入手,把這個最重要的記憶體管理機制搞定,以後再學習更深入的知識點時,就會更容易理解了。

頁表的拆分過程

在一個32位的系統中,物理記憶體的最大可表示空間就是 0xFFFF_FFFF,也就是 4GB

雖然實際安裝的實體記憶體可能遠遠沒有這麼大,但是在設計記憶體管理機制的時候,還是需要按照最大的可定址範圍來進行設計的。

按照一個物理頁4KB的單位來劃分,4GB 空間可以分割為1024 * 1024個物理頁:

在上一篇文章中,使用單一的對映表來指向這些物理頁,導致了對映表自身佔據了太多的實體記憶體空間。

一個使用者程式中定義的幾個段,可能實際上只使用了很小的空間,完全用不到 4 GB。

但是仍然需要為它分配多達 4GB 的實體記憶體空間來儲存這個對映表,很浪費。

為了解決這個問題,可以把這個單一對映表拆分1024個體積更小的對映表:

  1. 每一個對映表中,只有 1024 個表項,每一個表項仍然指向一個物理頁的起始地址;

  2. 一共使用 1024 個這樣的對映表;

這樣一來,1024(每個表中的表項個數) * 1024(表的個數),仍然可以覆蓋4GB的實體記憶體空間。

這裡的每一個表,就稱作頁表,所以一共有1024個頁表。

一個頁表中一共有1024個表項,每一個頁表項佔用4個位元組,所以一個頁表就佔用4KB的實體記憶體空間,正好是一個物理頁的大小。

也許有的小夥伴就開始算賬了:一個頁表自身佔用 4KB,那麼1024個頁表一共就佔用了4MB的實體記憶體空間,仍然是很多啊?

是的,從總數上看是這樣,但是:一個應用程式是不可能完全使用全部的 4GB 空間的,也許只要幾十個頁表就可以了。

例如:一個使用者程式的程式碼段、資料段、棧段,一共就需要10 MB的空間,那麼使用3個頁表就足夠了,加上頁目錄,一共需要 16 KB的空間。

計算過程:

每一個頁表項指向一個 4KB 的物理頁,那麼一個頁表中 1024 個頁表項,一共能覆蓋 4MB 的實體記憶體;

那麼 10MB 的程式,向上對齊取整之後(4MB 的倍數,就是 12 MB),就需要 3 個頁表就可以了。

記住上圖中的一句話:一個頁表,可以覆蓋 4MB 的實體記憶體空間(1024 * 4 KB)。

頁表中,每一個表項的格式如下:

注意下面的這幾個屬性:

P(Present): 存在位。1 - 物理頁存在; 0 - 物理頁不存在;

RW(Read/Write): 讀/寫位。1 - 這個物理頁可讀可寫; 0 - 這個物理頁只可讀;

D(Dirty): 髒位。表示這個物理頁中的資料是否被寫過;

頁目錄結構

現在,每一個物理頁,都被一個頁表中的一個表項來指向了,那麼這1024個頁表的地址,應該怎麼來管理呢?

答案是:頁目錄表!

顧名思義:在頁目錄中,每一個表項指向一個頁表的開始地址(實體地址)

作業系統在載入使用者程式的時候,不僅僅需要分配實體記憶體,來存放程式的內容;

而且還需要分配實體記憶體,用來儲存程式的頁目錄和頁表。

再來算算賬:

剛才說過:每一個頁表覆蓋4MB的記憶體空間,那麼頁目錄中一共有1024個表項,指向1024個頁表的實體地址。

那麼頁目錄能覆蓋的記憶體空間就是1024 * 4MB,也就是 4GB,正好是32位地址的最大定址範圍。

頁目錄中,每一個表項的格式如下:

其中的屬性欄位,與頁表中的屬性類似,只不過它的描述物件是頁表

還有一點:每一個使用者程式都有自己的頁目錄和頁表!下文有詳細說明。

幾個相關的暫存器

現在,所有頁表的實體地址被頁目錄表項指向了,那麼頁目錄的實體地址,處理器是怎麼知道的呢

答案就是:CR3 暫存器,也叫做: PDBR(Page Table Base Register)

這個暫存器中,儲存了當前正在執行的那個任務的頁目錄地址。

每個任務(程式)都有自己的頁目錄和頁表,頁目錄表的地址被記錄在任務的TSS段中。

當作業系統排程任務的時候,處理器就會找到即將執行的新任務TSS段資訊,然後把新任務的頁目錄開始地址更新到CR3暫存器中。

當新任務的指令開始被執行時,處理器在獲取指令、運算元據時,操作的是線性地址

頁處理單元就會從 CR3 暫存器中儲存的頁目錄表開始,把這個線性地址最終轉換成實體地址

當然,處理器中還有一個快表,用來加快從線性地址到實體地址的轉換過程。

CR3 暫存器的格式如下:

順便把官網上的其他幾個控制暫存器都貼出來:

其中,CR0 暫存器的最高位PG,就是開啟頁處理單元的開關

也即是說:

當系統上電之後,剛開始的地址定址方式一直是 [段:偏移地址] 的方式。

當啟動程式碼準備好頁目錄和頁表之後,就可以設定 CR0.PG = 1

此時,處理器中的頁處理單元就開始工作了:面對任何一個線性地址,都要經過頁處理單元之後,才得到一個實體地址。

載入使用者程式時: 頁目錄、頁表的分配和填充過程

在之前的文章中,介紹過一個使用者程式被作業系統載入的全過程,簡述如下:

  1. 讀取程式 header 資訊,解析出程式的總長度,從任務自己的虛擬記憶體中分配一塊足夠的連續空間;

  2. 分配一個空閒物理頁,用作程式的頁目錄,頁目錄的地址會記錄在稍後建立的 TSS 段中;

  3. 使用虛擬記憶體中的線性地址,分配一個物理頁(4 KB),登記到頁目錄和頁表中;

  4. 從硬碟上讀取 8 個扇區的資料(每個扇區 512 位元組),存放到剛才分配的物理頁中;

  5. 檢查程式內容是否讀取完畢:是-進入第 6 步;否-返回到第 3 步;

  6. 為使用者程式建立一些必要的核心資料結構,比如:TSS、TCB/PCB 等等;

  7. 為使用者程式建立 LDT,並且在其中建立每一個段描述符;

  8. 把作業系統的頁目錄中高階地址部分的表項,複製給使用者程式的頁目錄表。

這樣的話,所有使用者程式的頁目錄中,高階地址的表項都指向相同的頁表地址,就達到了共享“作業系統空間”的目的。

這裡主要聊一下第3步,假設使用者程式檔案在硬碟上的長度是 20 MB,電腦中實際安裝的實體記憶體是 1 GB

可以先計算一下:頁目錄中,每一個表項覆蓋的空間是 4 MB,那麼 20 MB的資料,需要 5 個表項就可以了。

在初始狀態,頁目錄中的所有表項都是空的,其中的P位都是為0,表示頁表不存在

作業系統首先從虛擬記憶體中,分配一塊20 MB的空間,假設從 1 GB(0x4000_0000)的地址處開始吧,這個地址是線性地址

也就是說把應用程式的檔案讀取到記憶體中,從地址0x4000_0000開始存放,向高地址方向增長。

注意:在“平坦”型分段模型下,線性地址等於虛擬地址。

0x4000_0000 = 0100_0000_0000_0000___0000_0000_0000_0000

10位表示該線性地址在頁目錄中的索引,中間10位表示頁表中的索引,最後12位表示物理頁中的偏移地址

因此,前10位就是 0100_0000_00,表示這個線性地址位於頁目錄中的第256個表項:

作業系統發現這個表項中為空,沒有指向任何一個頁表

於是就從實體記憶體中,找一個空閒的物理頁,用作頁目錄中第256個表項指向的頁表

注意:這個物理頁是用作頁表,而不是用作儲存使用者程式檔案。

假設在實體記憶體上 128 MB (0x0800_0000)的地址處,找到一個空閒的物理頁,用作這個頁表。

把頁表中的1024個表項全部清空,並且把頁表的實體地址 0x0800_0000,登記在頁目錄中的第256個表項中:0x08000(上圖黃色部分)。

為什麼不是 0x0800_0000

因為一個物理頁的地址一定是4KB對齊的(最後的12位全部為 0),所以頁目錄的表項中只需要記錄頁表地址的高 20 位即可。

現在,頁表也有了,下面就是分配一個物理頁來儲存程式的內容

假設在剛才那個物理頁(用作頁表的那個)的上面,又找到一個空閒的物理頁,地址是:0x0800_1000

此時,這個用於存放程式內容的物理頁的地址,就需要記錄在頁表的一個表項中了。

那麼應該記錄在頁表中的什麼位置呢?也就是應該登記在哪一個表項中呢?

需要根據線性地址的中間 10 位來確定:

0x4000_0000 = 0100_0000_0000_0000___0000_0000_0000_0000

中間10位的全部是 0,說明索引值就是0,也就是說頁表中的第0個表項,儲存這個物理頁的地址,如下圖所示:

一個物理頁的地址一定是4KB對齊的(最後的12位全部為 0),所以只需要記錄物理頁地址的高 20 位即可。

用於儲存程式檔案內容的物理頁分配好了,下面就開始從硬碟中讀取程式檔案的內容了。

一個物理頁的大小是 4 KB,硬碟上一個扇區的大小是 512 B,那麼從硬碟上連續讀取8個扇區的資料就可以把一個物理頁寫滿。

剛才已經假設:使用者程式檔案在硬碟上的長度是 20 MB

當讀取了一個物理頁的內容後,通過計算發現使用者程式內容還沒有讀取完,於是繼續重複以上流程。

  1. 線性地址增加 4KB:0x4000_1000 = 0100_0000_0000_0000___0001_0000_0000_0000;

  2. 前 10 位沒有變,仍然是頁目錄中的第 256 個表項,發現這個表項指向的頁表已經存在了,於是就不用再分配物理頁用作頁表了;

  3. 分配一個空閒物理頁,用於存放程式內容,假設在 0x0100_4000處找到一個,把這個地址登記在頁表中;

此時,線性地址的中間 10 位的索引值是 1,所以登記在頁表中的第 1 個表項。

  1. 從硬碟上讀取 8 個扇區的資料,寫入這個物理頁;

因為頁目錄中一個表項所覆蓋的範圍是 4 MB(也就是一個頁表中1024個表項所指向的物理頁空間的總和)。

所以當讀取了4 MB的程式內容之後,這個頁表中的所有表項就被填滿了。

此時,讀取的程式內容所佔用的【線性地址】空間是:0x4000_0000 ~ 0x403F_FFFF

下面再繼續讀取新內容時,就從 0x4040_0000 這個線性地址處開始存放,讀取過程與上面都是一樣的:

  1. 確定頁目錄表項:

0x4040_0000 = 0100_0000_0100_0000___0000_0000_0000_0000,前 10 位的索引值是 257;

  1. 發現 257 這個表項為空,於是分配一個空閒的物理頁,用作頁表;

  2. 分配一個物理頁,用作儲存程式檔案的內容,並把這個物理頁的地址記錄在頁表中;

線性地址 0x4040_0000 的中間 10 位的索引值是 0,所以登記在頁表的第一個表項中;

後面的過程就不再嘮叨了,一樣一樣的~~

最終的頁目錄和頁表的佈局,類似下面這張圖:

線性地址到實體地址的查詢、計算例項

如果理解了上一個主題的內容,那麼部分應該就可以不用再看了,因為它倆是相反的過程,而且查詢過程更簡單一些。

仍然繼續我們的假設:

  1. 使用者程式的長度是 20 MB,存放在虛擬記憶體 0x4000_0000 ~ 0x4140_0000 (線性地址)這段空間內;

  2. 程式碼段的長度是 8 MB,從虛擬記憶體的 0x40C0_0000 處開始存放;

也就是如下圖所示:

現在,使用者程式的內容已經全部讀取到記憶體中了,頁目錄、頁表全部都安排妥當了。

在頁目錄表中,一共有 5 個表項,正好表示這20MB的地址空間。

其中,8 MB 的程式碼所儲存的物理頁地址,登記在頁目錄表中的 259 和 260 這兩個表項中(上圖右側的綠色表項)。

目標:處理器在執行程式碼時,遇到一個線性地址0x4100_8800,頁處理單元需要把它轉換得到實體地址

0x4100_8800 = 0100_0001_0000_0000___1000_1000_0000_0000

首先,根據線性地址的前 10 位(0100_0001_00),得到它在頁目錄中的索引值為 260

這個表項中記錄的頁表地址為 0x08040,因為頁表地址的低12位一定是bit0,因此這個頁表的地址就是 0x0804_0000

頁目錄表的開始地址,肯定是從 CR3 暫存器獲取的;

然後,根據線性地址的中間 10 位(00_0000___1000),得到頁表中的索引值為 8

這個表項中記錄的物理頁地址為 0x02004,補上低位的12bit0,就得到物理頁的開始地址是 0x0200_4000

最後,根據線性地址的最後 12 位(1000_0000_0000),得到它在物理頁的偏移量 2048

也就是說:從物理頁的開始地址(0x0200_4000),偏移2048個位元組的地方,就是這個線性地址(0x4100_8080)對應的實體地址(0x0200_4800)。

大功告成!


------ End ------

關於虛擬地址到實體地址的轉換、頁目錄和頁表的查詢過程,基本就討論結束了。

不知道客官您是否已經酒足飯飽?

下週再寫一篇對頁目錄和頁表自身的“元操作”,這個系列文章就基本結束了。

如果還滿意的話,請您鼓勵一下,點個贊,轉發給朋友圈中的技術小夥伴,這也是對我最大的鼓勵,非常感謝!

推薦閱讀

【1】C語言指標-從底層原理到花式技巧,用圖文和程式碼幫你講解透徹
【2】一步步分析-如何用C實現物件導向程式設計
【3】原來gdb的底層除錯原理這麼簡單
【4】內聯彙編很可怕嗎?看完這篇文章,終結它!

其他系列專輯精選文章C語言Linux作業系統應用程式設計物聯網

星標公眾號,能更快找到我!

相關文章