天天看點

深入了解 Linux 記憶體管理1. 記憶體位址2. 記憶體管理之分段機制3. 記憶體管理之分頁機制4. 實體位址擴充5. 64位作業系統的分頁機制6. 程序的建立和執行

1. 記憶體位址

以Intel的中央處理器為例,Linux 32位的系統中,實體記憶體的基本機關是位元組(Byte),1個位元組有8個二進制位。每個記憶體位址指向一個位元組,記憶體位址加1後得到下一個位元組的位址。這裡用以表示實體記憶體實際位置的位址,就是通常所說的實體位址(Physical Address)。CPU正在執行的程序代碼、程序資料和棧區資料等,都臨時儲存在實體記憶體中。

線性位址(Linear Address,亦即虛拟位址 Virtual Address)是出于以下考慮,而在實體位址和程式之間增加的中間層。

(1) 隔離不同程序使用的記憶體位址空間;

(2) 提高記憶體的使用率;

(3) 确定程式運作時的位址;

(4) 擴充記憶體,即運作所需内層大于實體記憶體的程式

虛拟位址範圍對應CPU的尋址能力,32位的CPU的虛拟位址範圍為 0x00000000 ~ 0xFFFFFFFF,即最大虛拟記憶體為2^32 Bytes = 4GB;相應的64位CPU最大虛拟記憶體為 2^64 Bytes,然而實際上目前大部分作業系統和應用程式都不需要這樣大的虛拟位址空間,并且64位長的位址會增加系統的複雜性和位址轉換成本,是以目前的x86-64架構隻使用虛拟位址低位48位(0 ~ 47)作為虛拟位址,并用第47位的值填充48 ~ 63高位,是以64位CPU的最大虛拟記憶體為2^48 = 256TB。一般地,實體位址空間隻是虛拟位址空間的一個子集。

為了提高記憶體管理效率,發揮虛拟空間的作用,可設定CPU的 CR0 寄存器的最高位(PG,分頁标志位),啟用分頁機制将虛拟空間等分成若幹頁,然後按頁幀管理和使用虛拟空間。實體記憶體規定的頁大小有4096 Bytes,8192 Bytes,2MB, 4MB等,因為虛拟空間頁中存儲的内容實際上是要放到實體記憶體上的,是以虛拟空間也采用上述大小進行分頁。普通的分頁大小采用4KB的标準。

現代計算機系統中,一般不需要程式員直接操作實體位址,而是由作業系統按頁幀為程序配置設定執行用的虛拟位址。每個頁幀可以被映射到任何可用的實體記憶體頁。CPU 在執行程式程序時,CPU發出對相應的虛拟位址進行讀或寫操作,硬體裝置(MMU,記憶體管理單元,一般都內建在CPU晶片上)分析虛拟位址後查詢頁表并計算,将該虛拟位址映射為實體位址,然後通過北橋晶片(北橋晶片主要功能就是負責CPU和實體記憶體之間的通信)連接配接記憶體總線,進而CPU能夠通路到實體記憶體中程序代碼和資料。

邏輯位址(Linear Address)指的是程式内部的位址偏移量。該位址以作業系統為程式配置設定的程式入口位址為基準,指定程式中操作數或指令的位址。邏輯位址是程式員直接操縱的位址,例如在 C 語言程式設計中,定義一個 int  變量,然後使用取位址運算符(&var)得到的位址就是邏輯位址。

邏輯位址由兩部分構成,分别是段選擇符(Segment Selector)和段内偏移量(Offset),段選擇符是一個16-bit (2位元組)無符号數,段偏移量則是一個 32-bit 的無符号數。段選擇符的内容如下圖所示。

深入了解 Linux 記憶體管理1. 記憶體位址2. 記憶體管理之分段機制3. 記憶體管理之分頁機制4. 實體位址擴充5. 64位作業系統的分頁機制6. 程式的建立和執行

Figure 1 Segment Selector Fields

2. 記憶體管理之分段機制

段(Segmentation)是在段式記憶體管理的概念下形成的術語。段式管理的基本思想是把程式内容或過程關系分成段,例如代碼段、資料段等,作業系統按段為程序配置設定虛拟位址空間,這樣不同程序段空間不同,實作了程序隔離和記憶體保護。每個段都有自己的描述符(Segment Descriptor, 8-byte long),這些描述符被儲存在全局描述符表(Global Descriptor Table, GDT)或局部描述符表(Local Descriptor Table, LDT)中。段描述符的内容如圖2所示。

深入了解 Linux 記憶體管理1. 記憶體位址2. 記憶體管理之分段機制3. 記憶體管理之分頁機制4. 實體位址擴充5. 64位作業系統的分頁機制6. 程式的建立和執行

Figure 2 Segment Descriptor

Base Address一共 32 bits,它指向目前段第一個位元組的線性位址。Limit部分一共 20 bits,它指明本段虛拟空間最後一個位元組相對第一個位元組的偏移量,是以它也能表示段的長度。與頁不同(長度固定為4KB等),段的長度根據程式相應内容變化。另外,如果标志位 G設定為0,那麼偏移量每增加 1,位址值增加 1 byte,那麼這時段的最大長度為 1 byte * 2^20 = 1MB;如果标志位 G設定為1,那麼偏移量加1,位址值增加 4 KB,相應的這時段的最大長度為 4KB * 2^20 = 4GB。

在程序的執行過程中,當遇到需要通路記憶體的指令時,首先根據邏輯位址得到相應的線性位址,然後再根據線性位址得到實體位址。根據邏輯位址得到線性位址的過程如圖3所示。

深入了解 Linux 記憶體管理1. 記憶體位址2. 記憶體管理之分段機制3. 記憶體管理之分頁機制4. 實體位址擴充5. 64位作業系統的分頁機制6. 程式的建立和執行

Figure 3 Translating a Logical Address

CPU提供CS 寄存器臨時儲存正在執行的程序代碼段的段選擇符,DS 寄存器臨時儲存程序資料段的段選擇符,以及SS寄存器臨時儲存棧區分段的段選擇符。這樣,在轉換邏輯位址的時候,CPU根據目前儲存在CS中的段選擇器(參見圖1),其中 TI 标志确定段描述符位于GDT還是LDT,Index部分确定段描述符在表(GDT或LDT)中的位置,進而可以找到邏輯位址對應的段描述符;根據段描述符中的Base Address 找到段的起始線性位址,使用起始位址加上指令邏輯位址中的偏移量,就能得到指令所指向的實際線性位址。

由于分段機制和Intel處理器相關聯,在其它的硬體系統上,可能并不支援分段式記憶體管理,是以在 Linux 中,作業系統傾向與使用分頁的方式管理記憶體。在使用者模式(User Mode)下,所有的程序共用使用者代碼段和使用者資料段。使用者模式下,所有程序使用代碼段的段描述符的Base Address部分都指向線性位址0x00000000,同時資料段的段描述符的 Base Address部分也指向線性位址0x00000000;在核心模式(Kernel Mode)下,所有的程序共用核心代碼段和核心資料段。核心所有程序使用代碼段的段描述符的Base Address部分都指向線性位址0x00000000,同時資料段的段描述符的 Base Address部分也指向線性位址0x00000000。上述的段描述符的G位都設定為1,段對應的虛拟空間從0到2^32,對應整個32位CPU的最大虛拟空間。

上述辦法解決了其它硬體平台不支援段式管理的情況,大大簡化了位址轉換操作,但是由于理論上每個程序的可用線性空間範圍都是4G,即程序共用段表,使用段界限隔離程序記憶體的目的就不能實作了。是以,在Linux中,為每個程序配置設定獨立的頁表,純粹依靠分頁機制提供記憶體保護和程序隔離。接下來,針對分頁機制進行詳細的說明。

3. 記憶體管理之分頁機制

分頁機制将整個線性位址空間及整個實體記憶體看成由許多大小相同的存儲塊組成的,并把這些塊作為頁(虛拟空間分頁後每個機關稱為頁)或頁幀(實體記憶體分頁後每個機關稱為頁幀)進行管理。不考慮記憶體通路權限時,線性位址空間的任何一頁,理論上可以映射為實體位址空間中的任何一個頁幀。最常見的分頁方式是以 4KB 機關劃分頁,并且保證頁位址邊界對齊,即每一頁的起始位址都應被4K整除。在4KB的頁機關下,32位機的整個虛拟空間就被劃分成了 2^20 個頁。因為虛拟位址是按頁全部被映射到相同大小的頁幀,并且頁面邊界對齊,是以虛拟位址的後12位可以直接作為實體位址的低12位使用。

為了節省儲存頁表所需的記憶體空間(2^20 * 4B = 4M),32位作業系統常使用兩級頁表結構記載虛拟位址空間分頁現狀。是以每個虛拟位址就由三部分組成,高10位是頁目錄(Page Directory)中内容的索引,中間10位是頁表索引,低12位則作為對應實體位址在頁幀中的偏移量。

深入了解 Linux 記憶體管理1. 記憶體位址2. 記憶體管理之分段機制3. 記憶體管理之分頁機制4. 實體位址擴充5. 64位作業系統的分頁機制6. 程式的建立和執行

Figure 4 Paging Mechanism

頁目錄儲存在CR3寄存器中,可以直接通路。通路時以線性位址高10位作為索引,直接檢索并得到對應索引的 32 位頁目錄項。32位頁目錄項的結構如圖4中Page Directory部分所示,目錄項的高20位用以給出該目錄項對應的頁表在記憶體中的實體位址的高 20 位,1024個目錄項剛好能給出1024個頁表的入口位址。目錄項的低12位是一些标志位,其中P标志指明目前目錄項對應的頁表是否在記憶體中;U标志指明目前目錄項對應的頁的通路權限;S标志指明頁的大小是4KB或4MB,等等。另外,由于每個頁目錄項的長度為32位,即4個位元組,頁目錄中共有1024個頁目錄項,是以頁目錄的總大小為 4KB。

頁表儲存在記憶體中。頁表項的長度是32位,每個頁表中有1024個頁表項,可得出每個頁表的大小是 4 KB。頁表在記憶體中存放時,與實體分頁的大小(4KB)對齊,是以每個頁表所在的實體記憶體的起始實體位址的後12位都是0。而該實體位址的高20位又由頁表對應的頁目錄項中的高20位指定,這樣就可以得到找到實體記憶體中的頁表了。找到頁表後,以線性位址的中間10位為索引,檢索到該索引對應的32位頁表項。和頁目錄項類似,頁表項的高20用以給出其對應頁幀的起始實體位址的高20位。頁表項的低12位是關于頁的标志位。

頁幀對應實體記憶體。根據前面的兩步找得到頁幀的起始實體位址的高20位後,由于實體記憶體按4KB大小劃分成頁幀,是以頁幀的起始實體位址的低12位都是0。這樣高20位加低12位,得到頁幀的起始實體位址。找到頁幀後,使用線性位址的低10位作為偏移量,加上頁幀的起始實體位址後能找到線性位址對應的實體位址了。需要注意的是,頁幀和頁表項的對應關系并不是确定的,頁表項指向的頁首先是虛拟頁,然後該虛拟頁的内容被儲存在任何合适的頁幀中。

作業系統按頁為每個程序配置設定虛拟位址範圍,理論上根據程式需要最大可使用4G的虛拟記憶體。但由于作業系統需要保護核心程序記憶體,是以将核心程序虛拟記憶體和使用者程序虛拟記憶體分離,前者可用空間為1G虛拟記憶體,後者為3G虛拟記憶體。程序執行時,作業系統為其配置設定的頁的頁目錄會被加載到CR3寄存器,頁表會被加載到實體記憶體。分頁單元将線性位址轉換為實體位址的過程中,會檢查目前程序是否有通路該分頁的權限,以及線性位址對應的頁資料是否在實體記憶體中,如果上述檢查條件未被通過,分頁單元将會生成頁錯誤異常,進而中止程序或将相應分頁資料加載到實體記憶體。

4. 實體位址擴充

實體位址擴充(Physical Address Extension)是Intel 32位CPU上獨有的一種虛拟位址分頁方式。理論上,32位CPU有32條記憶體尋址線,最多能通路4G的實體記憶體;實際上在Linux系統中,使用者模式程式需要線性位址空間,是以核心最多隻能直接通路的實體記憶體為1G。但是,随着計算機軟體的發展,一台32位計算機上可能同時運作許多程序,而這些同時運作的程序所需記憶體量會大于4G,是以Intel為其32位CPU增加了4條記憶體尋址線,共36條,這樣CPU支援的實體記憶體增大到2^36,即64GB。擴充實體記憶體的同時,保持虛拟位址空間範圍為4G不變。進而使32位的應用程式繼續使用32位的位址,每個程序可使用的最大虛拟記憶體仍是4GB。

64GB的實體記憶體在4KB分頁下,被分成2^24個頁幀,每個頁幀的起始實體位址後12位仍然為0,但前24位則需要頁表提供。而我們知道,正常分頁中,頁表項為32位,其中隻能提供20位作為其指向頁幀的高20位實體位址,不能滿足36位系統的尋址需要。可以同通過增加頁表項的總長度來解決這個問題,為了保證以4KB的邊界對齊,我們将頁表項的長度增加為64位,8位元組(而不能是剛好滿足需要的36位),頁表大小保持4KB,那麼一個頁表中隻有512個頁表項(2^12 / 8)。

相應地,頁目錄也要适應36位的實體記憶體尋址能力,每條頁目錄項長度也變成64位,頁目錄大小保持4KB,一個頁目錄中隻有512個頁目錄項。這樣一個頁目錄總共可檢索 512 × 512=2^18個頁,而虛拟位址空間共有 2^20個頁,是以總共需要4個頁目錄。

一個新的分層被加入到CR3控制器和頁目錄之間,這個新的分層是頁目錄指針表(Page Directory Pointer Table)。頁目錄指針表中有四個長度為64位的指針,分别指向前述的4個頁目錄。頁目錄指針表被加載到64GB記憶體的第一個4GB上(實體位址0x00000000 ~ 0xFFFFFFFF),CR3中則儲存的是該頁目錄指針表的起始實體位址。

開啟實體擴充尋址方式後,将線性位址轉換為實體位址的方式和之前有較大不同,具體過程如圖5所示。

深入了解 Linux 記憶體管理1. 記憶體位址2. 記憶體管理之分段機制3. 記憶體管理之分頁機制4. 實體位址擴充5. 64位作業系統的分頁機制6. 程式的建立和執行

Figure 5 Linear Address Translation with PAE

首先由CR3得到頁面指針表的實體位址,然後以線性位址的30 ~ 31位作為索引得到頁目錄。接下來的21 ~39位(共9 bits,恰好提供全部512個頁目錄項的索引)可以幫助找到線性位址對應的頁表,12 ~  20(共9 bits,恰好提供全部512個頁表項的實體位址)可以幫助找到線性位址對應的頁幀的實體位址。

5. 64位作業系統的分頁機制

64位機的尋址能力為 2^64 Bytes,但實際中用不到這麼多的虛拟記憶體,使用64位尋址方式還會造成尋址時間增加、記憶體空間浪費等不利因素,是以在實際應用中,對64位機使用48位的尋址方式(最大支援256TB實體記憶體)。同樣的,将實體記憶體分為4KB大小的頁幀,那麼就需要 48-12=36位實體位址高位來确定頁幀位置。為了減小儲存頁表所需的實體記憶體,實作記憶體權限通路,可以通過增加兩個頁目錄層來分散頁表。在Linux中,采用4層分頁的方式來實作該目的。

深入了解 Linux 記憶體管理1. 記憶體位址2. 記憶體管理之分段機制3. 記憶體管理之分頁機制4. 實體位址擴充5. 64位作業系統的分頁機制6. 程式的建立和執行

Figure 6 Paging in 64-bit Linux

從64位線性位址(隻有48位作為位址用)轉換位實體位址的過程類似32位線性位址的轉換。

為了避免在多級頁表解析過程中多次查表而導緻性能下降的問題,Intel x86 處理器緩存了位址轉譯資訊,即從虛拟位址到實體位址的映射關系。這樣,當處理器重複通路同一個位址時無須再進行轉譯。此緩存是一種位于處理器内部的關聯存儲單元陣列,也被稱為位址轉譯快查緩沖區(TLB,TranslationLook-aside Buffer)。TLB 中包含了最近使用過的頁面的記憶體映射資訊,處理器提供了專門的電路來并發地讀取并比較TLB中的頁面映射項。是以,對于頻繁使用的虛拟位址,它們很可能在TLB中有對應的映射項,因而處理器可以絕對快速地将虛拟位址轉譯成實體位址;反之,如果一個虛拟位址沒有出現在TLB中,那麼處理器必須采用以上介紹的兩次查表過程(意味着要兩次通路記憶體)才能完成位址轉譯。在這種情況下,這一次記憶體通路會慢一些,但是,經過這次通路以後,此虛拟頁面與對應實體頁面之間的映射關系将被記錄到TLB中,是以,下次再通路此虛拟頁面時,處理器就可以從TLB 中實作快速轉譯,除非此映射項已經被 TLB 移除了。研究表明,由于計算機程式的記憶體通路有一定的局部性,是以,即使處理器隻維護一個相對較小的TLB,程式的運作也能獲得較顯著的性能提升。

6. 程序的建立和執行

執行程式時,作業系統會建立一個執行該程式的程序,然後裝載程式或程式片段等,然後開始順序執行代碼段。在這個過程中,作業系統總的來說做三件事情:

(1) 為程序建立一個獨立的虛拟位址空間(範圍)

例如在32位系統正常分頁狀态下,作業系統發現待執行程式的指令和資料總和為32KB,那麼作業系統會為程序配置設定8個頁的虛拟記憶體空間,并配置設定頁目錄和頁表,把頁目錄裝入CR3,把程序用到的頁表加載到記憶體。但并不把指令和資料加載到記憶體。

(2) 讀取程式可執行檔案檔案頭,并且建立虛拟空間與可執行檔案中的代碼段、資料段的邏輯位址的映射關系

這一步将程式指令和資料映射到虛拟記憶體空間中。

(3) 将 CPU 的指令寄存器設定成可執行檔案的入口位址,啟動運作

執行程式過程時,如果目前指令或資料之在虛拟位址空間中,而實際上并不在實體記憶體中(前兩步都沒有将指令或資料加載到實體記憶體),将發生頁錯誤,這時作業系統再從實體記憶體配置設定一個空閑的實體頁幀,并将虛拟位址頁對應的資料從磁盤拷貝加載到實體頁幀中,并建立頁表項和頁幀的映射關系。随着程序的執行,頁錯誤也會不斷産生,作業系統也會響應每個頁錯誤并為程序配置設定實體記憶體頁幀。但實體記憶體是有限的,為一個程序可配置設定的實體記憶體也有限。全部可用實體記憶體都配置設定給程序後,如果程序繼續抛出頁錯誤請求更多實體記憶體,這時候作業系統根據自身的頁置換操作算法,在保證程序正常運作的前提下,将先前為程序配置設定的實體記憶體頁幀收回,重新分給該程序。

繼續閱讀