1. 程式人生 > >Linux的記憶體定址——淺談分段和分頁機制

Linux的記憶體定址——淺談分段和分頁機制

本文會以80x86架構,linux2.6為例,簡單介紹記憶體的分段和分頁機制。

1. 三種記憶體地址

關於記憶體地址,首先要了解它有三種,分別是

邏輯地址、線性地址和實體地址。

把邏輯地址轉換為線性地址是由一個叫做分段單元的硬體電路完成的。

同樣地,還有一個叫做分頁單元的硬體電路負責把線性地址轉換為實體地址。

那現在很明確地,當我們討論分段的時候,就是討論邏輯地址是如何轉換成線性地址的。當我們討論分頁的時候,就是在討論線性地址如何轉換成了實體地址

下面分開討論分段和分頁功能是如何實現。

2. 邏輯地址

介紹分段之前,先明確邏輯地址的構成分兩部分:一個16位的段選擇符,一個32位的偏移量。

其中段選擇符由高到低是:13位index,1位TI,2位RPL。它們的作用在下文介紹。

3. 段選擇符、段暫存器、段描述符

現在我們知道,段選擇符是邏輯地址的一部分,為了方便找到段選擇符,處理器提供了6個段暫存器,段暫存器的唯一目的就是存放段選擇符。

段描述符有8個位元組,負責描述段的特徵,包括段的首位元組的線性地址、段的最大長度、段的特權級、區分段是資料段還是程式碼段的標誌位……一共9個欄位。

段選擇符和段描述符具有對應關係,那應該如何通過段選擇符得到段描述符呢?答案是查詢全域性描述符表(GDT)或者區域性描述符表(LDT)。

4. GDT\LDT

在linux中,每個CPU都有一個自己的GDT,用於存放本處理器定址要用到的段描述符。linux有一個全域性的cpu_gdt_table陣列,放著每個cpu當前的GDT。每個GDT有32個表項,其中有14個空置,剩下的都是段描述符,包括了使用者態程式碼段、使用者態資料段、核心態資料段、核心態程式碼段、任務狀態段TSS、3個區域性執行緒儲存段TLS……共18個段描述符。。

所謂LDT就是某些程序要有一些自己用的段,需要建立一個自己專用的描述符表,這就是LDT。

從硬體上看,處理器有兩個暫存器專門存放GDT和LDT的基地址(是一個線性地址),分別叫做gdtr和ldtr。

5.分段單元的工作過程

Input:邏輯地址(段選擇符和偏移量)

段選擇符的TI欄位決定了這個段的段描述符是在GDT還是LDT中,通過對TI欄位的檢查,分段單元可以知道它應該去gdtr還是ldtr去讀表的基地址。

接下來看段選擇符的index欄位,這個欄位說明了段描述符在表的第幾項。因為每個段描述符是8位元組,那麼表的基地址+index*8就是段描述符的地址。

得到段描述符後,也就知道了段的基地址了。此時用段的基地址+段的偏移量,就得到最終的線性地址了。

Output:該邏輯地址對應的線性地址。

分段就介紹完了。而事實上,從2.6核心開始,Linux就只在80x80結構下才需要使用分段了。為什麼linux偏愛分頁呢?首先是因為忽略掉分段後,記憶體的管理簡單了,相當於所有的段的基地址都相同,大家都是同一個段,linux只負責處理線性地址就好了;另外,不同的計算機體系結構對分段的支援不一,使用分段不利於linux的跨處理器平臺。

所以在linux裡,使用者程式碼段、資料段,核心程式碼段資料段,它們的段描述符裡,段的起始線性地址都是0x00000000。這意味著一個邏輯地址,它的偏移量就是它的線性地址。

好,接下來討論分頁。

6. 頁,頁框,頁表

頁是為了提高效率,把線性地址分成了固定長度的組,所以頁就是一組連續的線性地址。在轉換成實體地址的時候一次轉換一頁。

既然這樣,那實體地址也要分組。分頁單元把所有的RAM也分成了固定長度的組,叫做頁框(所以頁框就是一組連續的實體地址),頁框和頁長度一致,一個頁框可以放一頁。

知道了頁和頁框以後,那麼我們怎麼知道該把哪一頁放進哪個頁框裡呢?這就要查頁表了。頁表放在主存裡,頁表可能分多級。

7.常規分頁,擴充套件分頁,64位系統的分頁

從這裡開始,我們預設分頁單元處理的一頁是4096位元組。

一個32位的線性地址被分成三部分,從高位到地位分別是:10位的Directory,10位的table,12位的offset。

首先為什麼要分多級呢?因為如果只有一級目錄的話,那麼表項會非常多。因為一頁是4096 = 2^12位元組,那麼offset是一定要佔12位的,剩下的20位將產生2^20個表項,一個表項4位元組,載入一個頁表就要佔4MB的RAM,這太浪費了記憶體了。而當使用這種二級模式時,可以通過目錄表先找到要讀的頁,這樣一次只讀2^10個表項,小了一半。

現在我們可以基本想象到分頁單元是怎麼把線性地址變成實體地址的了:假設有一個線性地址0x20180601,低12位offset是0x601,中間10位的table是0x180,高十位directory是0x080,directory表的實體地址在暫存器cr3中,讀到directory後從第0x80項找到table,從table的第0x180項找到本頁對應頁框的起始實體地址,最後這個地址+0x601得到最終的實體地址。

但是實際操作中還有些細節,比如說,1024項directory,1024項table,和12位的偏移,一共可以定址2^32個地址,涵蓋了整個記憶體,但事實上一個程序只會從有限的線性地址空間內分配空間,假設核心給了一個執行緒64頁,這已經是可以分配給程序的上限了,假設它的空間是0x2000 0000 到0x2003 ffff,目錄項只有0x080是有用的,其他都要設定成0,同理,table裡只有0到0x3f號的64個表項有用,其他也都設定成0。

如果這個程序想要訪問超過這64頁之外的空間,比如0x3000 0000,那必然是非法的,這個頁根本不會在記憶體中,這就要返回一個缺頁異常。我們需要一個標誌位來表明這個頁到底在不在主存裡。可見,directory表和table表,每個表項除了要有對應的頁表或者頁框地址之外,還要有一些標誌位。實際上,有一個present標誌負責記錄該頁是否在主存中。如果試圖訪問一個present=0的表項,就會產生缺頁異常。

除了這種常規分頁,從Pentium開始,還引入了擴充套件分頁PAE,允許一個頁框達到4MB項,引入擴充套件分頁後,offset有22位,高10位做目錄,不再有二級模式。

分頁到了64位系統中又有不同,因為在64位地址,4kb頁框的情況下,除去12位的offset,剩下了48位做directory或者table。那這樣顯然會造成每張表的表項非常高。

那應該怎麼解決呢?一個是增加分頁級別,從原來的二級升高到3級或4級;另外可以讓64位並不全用來定址。至於定址用多少位,分頁分幾級,會因處理器型別而異。像x64_64,用48位定址,按9+9+9+9+12劃分,一共有4級頁表。

8. Linux分頁

既然32位系統和64位系統分頁方法不同,不同硬體系統分頁的策略也不同,Linux是怎麼適應這諸多硬體模型的呢?

linux採用了4級分頁模型:分別是頁全域性目錄PGD,頁上級目錄PUD,頁中間目錄PMD,頁表PTE。如果是32位系統,而且沒有PAE,那麼PUD和PMD就不啟用,全部置0。

Linux內用pte_t、pmd_t、pud_t和pgd_t描述頁表、頁中間目錄、頁上級目錄和頁全域性目錄,提供了豐富的api,可以操作每一級頁表,進行讀、寫、清空等一系列操作。

OK,到此為止,本文簡單的介紹了幾個和分段分頁相關的概念,包括分段相關的邏輯地址、段描述符、段選擇符、段暫存器、GDT和LDT,以及分頁相關的頁、頁框、頁表、PAE、PTE、PMD、PUD和PGD等,並簡單描述了分段單元和分頁單元的工作過程。