1. 程式人生 > >Linux記憶體和地址空間管理

Linux記憶體和地址空間管理

本文以Linux 2.6版本核心為例,介紹了核心線性地址空間的佈局,並描述了80386架構處理器下的3種記憶體地址的概念及在分段、分頁機制下的相互轉換。

通過記憶體地址訪問,我們可以得到存在記憶體單元裡的內容,這很容易理解。但在不同的環境下,會涉及到幾種不同的記憶體地址的概念,初學者很容易混淆。為了方便後面的學習,我們以80X86架構處理器為例,把涉及的3種記憶體地址的概念分別做一解釋。

邏輯地址(Logical Address):彙編程式中,我們經常會看到段基址加偏移量來表示記憶體地址的方式,如0100:20,1000:40,其實這就是邏輯地址。目前的 MSDOS或Windows程式,利用了80X86架構處理器的分段機制,程式地址空間被分隔成若干個不同的段,如程式碼段,資料段等。自然地,記憶體地址就 用段地址+段偏移來表示。和Windows不同,Linux只是有限地使用分段機制。Linux啟動程式碼在真實模式階段,會顯式地用邏輯地址;當進入保護模 式後,核心和程序的程式碼資料段都被設定成相同的段基址0x00000000,這時,段偏移量就等於線性地址,因而用線性地址表示記憶體地址更加方便。

線性地址(Linear Address):線性地址也被稱作虛擬地址,在32位CPU架構下,可以表示4G的地址空間,用16進製表示就是0x00000000到 0xffffffff。用線性地址定址記憶體比邏輯地址更加直觀,因此在Linux 中,無論核心地址空間還是程序地址空間,都是用線性地址來表示的。當然,訪問記憶體最終還要利用實體地址的,所以線性地址在定址時需要轉換成實體地址。這種 轉換是由硬體自動完成的,我們會在後面的分頁技術部份詳細討論。

實體地址 (Physical Address):實體地址顧名思義就是用來訪問實體記憶體的地址表示方式。無論邏輯地址,還是線性地址,最終都要轉換成實體地址來完成訪存的過程。從電氣層來講,實體地址實際上是CPU地址引腳發往記憶體匯流排的電訊號。

在80386處理器中,典型的邏輯地址,線性地址和實體地址的轉換過程是這樣的。內 存管理單元(MMU)通過硬體分段機制把邏輯地址轉換成線性地址,然後再通過硬體分頁機制把線性地址轉換為實體地址。這裡,讀者可能會感到有些困惑,一次 訪存操作要涉及到兩次地址轉換,效率會不會很低。其實,這種轉換都是硬體自動完成的,無需軟體參與,再加上TLB Cache等技術,實際訪存效率還是比較高的。當然,轉換的規則是需要軟體提前初始化好的。

記憶體定址

1.分段機制

分段機制是用分段的方法把程式的程式碼,資料以及堆疊分隔開來,從而保證在同一線性地址空間內,多個程式之間互不干擾的執行。

Intel從80286開始引入保護模式,因此邏輯地址到實體地址的轉換方式和之前 的真實模式有了很大不同。真實模式下的分段機制很簡單,地址轉換公式是:實體地址=段地址*16+段偏移,這裡不贅述了。在保護模式下,邏輯地址是由段選擇子 和段偏移量兩部分構成。其中,段選擇子是16位長度,段偏移是32位長度。

段選擇子標識一個段,通常被載入到段暫存器CS,DS,ES,SS,FS和GS來 表示,如圖1所示。INDEX表示所標識的段描述符在段描述符表(GDT or LDT)中的索引。TI表示所標識的段描述符是在全域性還是本地段描述符表中。TI=0表示在全域性描述符表(GDT)中,TI=1表示在本地描述符表 (LDT)中。RPL表示請求特權級別,分0-3級,0級別最高。在Linux核心中,0代表核心態,3代表使用者態。

80386的段描述符表分全域性段描述符表(GDT)和區域性描述符表(LDT)兩類。其中,全域性段描述符表最重要,它其實是一個存放線上性地址空間的結構陣列,每個結構單元代表一個段描述符。邏輯地址通過其段選擇子中的索引值,就可以定位到對應的段描述符上。

段描述符顧名思義,描述了線性地址空間的一個段。因為其格式比較複雜,這裡就不一一羅列了,其中最重要的欄位是段基地址(BASE)和大小(LIMIT),它們會參與到邏輯地址的轉換過程。

事實上,分段機制和後面要講到的分頁機制都可以起到隔離不同程序的地址空間,因此功能上有些重複,再加上考慮和RISC架構的相容性,因此,Linux只是很有限地使用了段機制。

在Linux中,4個最主要的段描述符-使用者態程式碼和資料段,核心態程式碼和資料段都 被設定成BASE=0x0000000, LIMIT=0xffffffff,因此,所有的使用者程序包括核心都使用同樣的線性地址空間,而且邏輯地址中的段偏移就等於線性地址。這樣,除了使用者態和 核心態之間的切換外,在其它絕大多數情況下,段暫存器都不需要改變。所以說,段機制在Linux中的使用被弱化了。

2.分頁機制

分頁機制是把線性地址空間轉換到實體地址空間,從而達到多工的隔離目的。從這個意思上講,分頁機制和分段機制有相似的地方,但分頁機制在功能上要強大很多。比如,分頁機制下所有的程序擁有相同的地址空間,管理更方便。

分頁機制的硬體把實體記憶體空間看作有一系列的大小相同的頁幀組成,通常情況下,頁的 大小是4K。當然,在較新的80X86架構CPU中,也可以通過設定使頁大小達到4M或者2M。以4K頁表為例,一個32位的線性地址可以被分成頁目錄項 索引(10位),頁表項索引(10位)以及偏移量(12位)。

分頁機制下,線性地址的轉換需要用到轉換表。轉換表在80X86中有兩級,第一級 叫做頁目錄表,第二級叫頁表。這兩種表其實都是存放在物理空間中的結構陣列,每個結構單元佔32位。我們把這種結構單元稱作頁目錄項或者頁表項,其格式主 要包括兩部份,分別為指向頁表或頁幀的實體地址(對頁目錄表來說,指向頁表的實體地址;對頁表來說,指向頁幀的實體地址)和控制位(含保留位)。

線性地址到實體地址的轉換過程是這樣的。首先根據線性地址中頁目錄索引在頁目錄表中找到對應的頁目錄項,該項中包含有指向下級頁表的實體地址;再根據頁表索引在頁表中找到對應項,取出頁幀的實體地址。頁幀實體地址加上線性地址中的偏移量就得到最終轉換後的實體地址。

線性地址到實體地址的轉換是硬體自動完成的,但考慮到效率的因素,CPU還使用TLB來加速轉換速度。

為了達到更廣泛的相容性, Linux核心採用4級頁表模型來使用硬體分頁機制 (2.6.11版本之前使用3級頁表),分別是Page Global Directory(pgd_t), Page Upper Directory(pud_t), Page Middle Directory(pmd_t) 和Page Table(pte_t)。當硬體分頁機制實際是兩級頁表時,Linux核心在程式碼上雖然還是4級頁表(相容更多級別頁表的CPU架構),但巧妙地把 Page Upper Directory 和Page Middle Direcotry跳過了。

Linux線性地址空間佈局

在Linux中,每一個程序可以訪問4GB的線性地址空間。其中,0到3GB的線性 地址是使用者空間,使用者態程序可以直接訪問。3G之上的空間為核心空間,只有當CPU處於核心態才能訪問,使用者態程序不能直接訪問。當用戶態的程序需要申請 核心服務時,可以通過中斷或者系統呼叫來觸發CPU特權級的轉換(3到0),從而訪問到核心空間中的程式碼或資料。

所有程序從3G到4G的線性空間都是相同的,有著同樣的頁目錄項和頁表,從而對應同樣的實體記憶體空間。因此,可以說3G到4G的線性空間被所有程序共享,程序之間的差異體現在0到3G空間所對應的頁目錄項和頁表。

Linux維護了一份主核心空間頁目錄表及頁表(Master Kernel Page Global Directory),在核心程式碼中用swapper_pg_dir來表示。Swapper_pg_dir 在核心初始化時被設定,雖然它不會被任何程序直接使用,但所有程序在3G以上線性空間對應的頁目錄項都會引用它。

接下來,我們重點關注一下Linux核心線性地址空間(3G-4G)是如何使用分佈的,參見下圖

1.直接記憶體對映區(Direct Memory Region)

Linux直接對映從實體地址0開始,最大896M大小的物理空間到3G開始的線性 地址區間,該區間我們稱作直接記憶體對映區,其線性地址和實體地址的轉換公式為 線性地址=3G + 實體地址。例如,實體地址區間0x100000-0x200000對映到線性空間就是3G+0x100000-3G+0x200000。直接記憶體對映區是 在Linux初始化時靜態對映好的。

2.動態記憶體對映區(VMalloc Region)

低端記憶體對映區上面有8M大小的隔離帶,再往上就是動態記憶體對映區,該區間主要為核心函式Vmalloc服務的,其特點是線性空間連續,但對應的物理空間不連續。在不連續記憶體管理一章中會詳細討論vmalloc的工作原理。

3.PKMap區(PKMap Region)

動態記憶體分配區上面有8K的隔離帶,再往上就是PKMap區,大約有4M空間。我們 都知道,3G到4G的線性空間只有1G,但實體記憶體可能遠遠不止1G,Linux不可能把所有的物理頁面都直接對映到該線性空間。事實上,正如直接對映區 所定義的,Linux最大隻能直接對映896M物理空間。那如何訪問896M以上的物理空間呢?這就是PKMap區產生的由來。該區間是作為一個動態窗 口,用來對映896M以上的高階物理頁面,通過該視窗,Linux就可以訪問到高階記憶體了。

4.固定對映區(Fixing Mapping Region)

PKMap區上面,有大約4M的線性空間,被稱作固定對映區,它和4G頂端只有4K 的隔離帶。固定對映區顧名思義,是用來對映物理頁到該區中固定的線性地址項,固定對映區中每個地址項都服務於特定的用途,如ACPI_BASE等。因此, 核心不需要使用變數指標,直接用地址項就可以訪問被對映的物理頁,因而效率更高。

記憶體管理

1.頁幀管理——夥伴(Buddy)系統

Linux管理實體記憶體是以物理頁為單位進行的。對應於每個物理頁,Linux在記憶體中都有一個page型別的物件例項來表示它,稱作頁描述符。所有的頁描述符在Linux初始化階段被靜態設定好,放在以mem_map為頭的陣列內。

在一個節點上的實體記憶體可以分成幾個區(Zone)來管理,它們分別是:

◆ZONE_DMA區,包含實體地址在16M以下的頁幀;

◆ZONE_Normal區,包含實體地址在16M到896M之間的頁幀;

◆ZONE_HIGHMEM區,包含實體地址在896M以上的頁幀。

在每個區內,Linux使用夥伴系統(Buddy)來組織空閒頁的管理。夥伴演算法的 思想是,把空閒頁組按連續數目20 , 21, 22,…不同放在不同的連結串列中,連結串列上每個節點代表一個空閒頁組。比如,2個頁連續的空閒頁組會作為一個節點放到21連結串列中,同樣的,4個頁連續的空閒頁 組會作為一個節點放到22連結串列中。當核心需要申請一組連續的空閒頁面時,它會首先在大小最接近的夥伴連結串列中查詢並返回目標節點;如果該連結串列為空,再搜尋大 小更高一級的夥伴連結串列,依此往復,直至找到。找到的節點包含的連續頁面可能會比請求的大,因此要先拆分,然後返回請求大小的頁面組,同時把剩下的連續空閒 頁面插到相應大小的連結串列中。比如,核心請求2個連續空閒頁面,因為21連結串列為空,最後在22連結串列中找到一個節點。夥伴系統把該節點的4個頁面拆分成兩個連 續頁面為2的頁組,一個返回給核心請求,另一個插到21連結串列中去。同樣道理,當釋放頁面時,夥伴系統會自動合併相鄰的小頁面組變成較大的頁面組,然後再插 入相應大小的連結串列中。

2.記憶體物件管理——Slab系統

夥伴系統適合分配以頁為單位的較大的記憶體空間,但對任意大小的資料物件的記憶體分配, 尤其較小的物件,如16個位元組,就無能為力了。而資料物件的記憶體分配在Linux核心中需要被大量地使用,如程序描述符,檔案描述符等,所以Linux引 入Slab系統機制來解決物件的分配和釋放問題。

Slab系統由若干Cache組成,每個Cache管理一個特定類的物件。同時,每個Cache由若干個slab塊組成,每個slab塊實際上是由若干個頁面組成的頁組,被管理的物件就被放在頁組內。

Cache有兩類,稱為通用cache和專用cache。

通用Cache也分兩種。一個是給Cache自己的資料結構物件用的,稱作 cache_cache。另一種叫kmalloc Cache,包含一系列Cache,每個Cache對應物件的大小是2的冪,如2,4,8,16…;當Linux通過kmalloc請求記憶體時,直接從大 小最接近的那個kmalloc Cache中分配。

通用Cache都是在系統初始化階段靜態生成好的。

要使用專用Cache,首先需要顯示地為目標類物件建立一個Cache。事實上,可 以把這個Cache看作一個特定類的物件池,以後物件的申請和釋放都在池中發生。剛開始時,Cache是空的,沒有任何物件,也就沒有任何slab塊。當 Linux第一次向該Cache申請目標類物件時,Slab系統會自動為Cache生成一個包含若干頁面的slab塊,請求的物件就從該slab塊中分 配。如果該物件被申請的很多導致slab塊耗盡,Slab系統會自動為Cache增加新的slab塊。相反,如果物件大量釋放致使Cache空閒率提 高,Slab系統會在適當的時機,通過釋放slab塊來為Cache瘦身。

3.不連續記憶體管理——vmalloc

在前面的線性地址空間佈局中,我們提到過動態記憶體對映區,這段區域就是vmalloc區。在前面的夥伴系統以及Slab系統中,其所管理的實體記憶體空間都是連續的。但在vmalloc區,線性地址空間所對應的物理空間地址是不連續的。

通過vmalloc產生的實體地址和線性地址的對映是動態生成的,按需產生頁目錄項 和頁表。例如,如果使用vmalloc分配1M大小的線性空間,其過程是核心將從動態記憶體分配區找出1M空閒的連續線性地址空間,然後再從頁幀管理系統中 逐頁找出1M實體地址空間(頁之間不一定相鄰),最後通過產生對應的頁目錄項及頁表把它們對映起來。所以,通過Vmalloc得到的線性空間,其對應的物 理空間地址不一定連續,這是和直接對映區內的線性空間最大的不同。

圖1 段選擇子格式

圖2 邏輯地址到線性地址轉換圖

圖3 頁目錄項及頁表項格式圖

圖4 線性地址到實體地址轉換圖

圖5 Linux核心線性空間佈局圖

鏈 接

Baseline kernel

Baseline kernel是指在http://kernel.org上釋出的Linux核心,在kernel.org上能找到穩定版(版本號的第二個數字是偶數,例如 2.6.21)和開發版(版本號的第二個數字是偶數,例如 2.5.1)。目前大多數發行版是基於2.6.9和2.6.18。穩定版並不意味著開發工作停止了,相反,值得注意的是即使是穩定版,在不同的版本之間, 使用者還是能夠看到大量的新程式碼,甚至有時可能會包含萬行的程式碼修改。

鏈 接

LKML

Linux核心郵件列表,是所有的Linux核心開發者開展資訊交流的主要平臺,有 超過5000名註冊成員,其中包括了Linus Torvalds和Andrew Morton 等最著名的人物。如果有意在上面發表自己的觀點,首先需要閱讀一下FAQ http://www.tux.org/lkml,並且確信自己理解了那裡提到的事項。值得一提的是,在你加入LKML之後,每天郵箱裡會有數百份新郵 件,當然還可以通過一些網站(例如 http://lkml.org)訪問LKML。