天天看點

深入了解Linux高端記憶體

一、Linux核心位址映射模型

x86 CPU采用了段頁式位址映射模型。程序代碼中的位址為邏輯位址,經過段頁式位址映射後,才真正通路實體記憶體。

段頁式機制如下圖:

深入了解Linux高端記憶體

圖檔發自簡書App

Linux簡化了分段機制,使得虛拟位址與線性位址總是一緻,是以,Linux的虛拟位址空間也為0~4G。Linux核心将這4G位元組的空間分為兩部分。将最高的1G位元組(從虛拟位址0xC0000000到0xFFFFFFFF),供核心使用,稱為“核心空間”。而将較低的3G位元組(從虛拟位址0x00000000到0xBFFFFFFF),供各個程序使用,稱為“使用者空間)。因為每個程序可以通過系統調用進入核心,是以,Linux核心由系統内的所有程序共享。于是,從具體程序的角度來看,每個程序可以擁有4G位元組的虛拟空間。

Linux使用兩級保護機制:0級供核心使用,3級供使用者程式使用。從圖中可以看出(這裡無法表示圖),每個程序有各自的私有使用者空間(0~3G),這個空間對系統中的其他程序是不可見的。最高的1GB位元組虛拟核心空間則為所有程序以及核心所共享。

1.虛拟核心空間到實體空間的映射

核心空間中存放的是核心代碼和資料,而程序的使用者空間中存放的是使用者程式的代碼和資料。不管是核心空間還是使用者空間,它們都處于虛拟空間中。讀者會問,系統啟動時,核心的代碼和資料不是被裝入到實體記憶體嗎?它們為什麼也處于虛拟記憶體中呢?這和編譯程式有關,後面我們通過具體讨論就會明白這一點。

雖然核心空間占據了每個虛拟空間中的最高1GB位元組,但映射到實體記憶體卻總是從最低位址(0x00000000)開始。對核心空間來說,其位址映射是很簡單的線性映射,0xC0000000就是實體位址與線性位址之間的位移量,在Linux代碼中就叫做PAGE_OFFSET。

我們來看一下在include/asm/i386/page.h中對核心空間中位址映射的說明及定義:

深入了解Linux高端記憶體

圖檔發自簡書App

源代碼的注釋中說明,如果你的實體記憶體大于950MB,那麼在編譯核心時就需要加CONFIG_HIGHMEM4G和CONFIG_HIGHMEM64G選項,這種情況我們暫不考慮。如果實體記憶體小于950MB,則對于核心空間而言,給定一個虛位址x,其實體位址為“x-PAGE_OFFSET”,給定一個實體位址x,其虛位址為“x+PAGE_OFFSET”。

這裡再次說明,宏__pa()僅僅把一個核心空間的虛位址映射到實體位址,而決不适用于使用者空間,使用者空間的位址映射要複雜得多。

2.核心映像

在下面的描述中,我們把核心的代碼和資料就叫核心映像(kernel image)。當系統啟動時,Linux核心映像被安裝在實體位址0x00100000開始的地方,即1MB開始的區間(第1M留作它用)。然而,在正常運作時,整個核心映像應該在虛拟核心空間中,是以,連接配接程式在連接配接核心映像時,在所有的符号位址上加一個偏移量PAGE_OFFSET,這樣,核心映像在核心空間的起始位址就為0xC0100000。

例如,程序的頁目錄PGD(屬于核心資料結構)就處于核心空間中。在程序切換時,要将寄存器CR3設定成指向新程序的頁目錄PGD,而該目錄的起始位址在核心空間中是虛位址,但CR3所需要的是實體位址,這時候就要用__pa()進行位址轉換。在mm_context.h中就有這麼一行語句:

asm volatile("movl %0,%%cr3": :"r"(__pa(next_pgd;pgd)));      

這是一行嵌入式彙編代碼,其含義是将下一個程序的頁目錄起始位址next_pgd,通過__pa()轉換成實體位址,存放在某個寄存器中,然後用mov指令将其寫入CR3寄存器中。經過這行語句的處理,CR3就指向新程序next的頁目錄表PGD了。

二、Linux核心位址空間劃分

通常32位Linux核心位址空間劃分0 ~ 3G為使用者空間,3 ~ 4G為核心空間。注意這裡是32位核心位址空間劃分,64位核心位址空間劃分是不同的。

深入了解Linux高端記憶體

圖檔發自簡書App

三、Linux核心高端記憶體的由來

當核心子產品代碼或線程通路記憶體時,代碼中的記憶體位址都為邏輯位址,而對應到真正的實體記憶體位址,需要位址一對一的映射,如邏輯位址0xc0000003對應的實體位址為0×3,0xc0000004對應的實體位址為0×4,… …,邏輯位址與實體位址對應的關系為:

實體位址 = 邏輯位址 – 0xC0000000

假設按照上述簡單的位址映射關系,那麼核心邏輯位址空間通路為0xc0000000 ~ 0xffffffff,那麼對應的實體記憶體範圍就為0×0 ~ 0×40000000,即隻能通路1G實體記憶體。若機器中安裝8G實體記憶體,那麼核心就隻能通路前1G實體記憶體,後面7G實體記憶體将會無法通路,因為核心 的位址空間已經全部映射到實體記憶體位址範圍0×0 ~ 0×40000000。即使安裝了8G實體記憶體,那麼實體位址為0×40000001的記憶體,核心該怎麼去通路呢?代碼中必須要有記憶體邏輯位址 的,0xc0000000 ~ 0xffffffff的位址空間已經被用完了,是以無法通路實體位址0×40000000以後的記憶體。

顯然不能将核心位址空間0xc0000000 ~ 0xfffffff全部用來簡單的位址映射。是以x86架構中将核心位址空間劃分三部分:ZONE_DMA, ZONE_NORMAL和 ZONE_HIGHMEM。ZONE_HIGHMEM即為高端記憶體,這就是記憶體高端記憶體概念的由來。

在x86結構中,三種類型的區域如下:

名稱 範圍
ZONE_DMA 記憶體開始的16MB
ZONE_NORMAL 16MB~896MB
ZONE_HIGHMEM 896MB ~ 結束
深入了解Linux高端記憶體

圖檔發自簡書App

四、Linux核心高端記憶體的了解

高端記憶體HIGH_MEM位址空間範圍為0xF8000000~0xFFFFFFFF(896MB~1024MB)。那麼如核心是如何借助128MB高端記憶體位址空間是如何實作通路可以所有實體記憶體?

Linux将核心位址空間劃分為三部分:

  • ZONE_DMA、
  • ZONE_NORMAL
  • ZONE_HIGHMEM

前面我們解釋了高端記憶體的由來。當核心想通路高于896MB實體位址記憶體時,從0xF8000000 ~ 0xFFFFFFFF位址空間範圍内找一段相應大小空閑的邏輯位址空間,借用一會。借用這段邏輯位址空間,建立映射到想通路的那段實體記憶體(即填充核心PTE頁面表),臨時用一會,用完後歸還。這樣别人也可以借用這段位址空間通路其他實體記憶體,實作了使用有限的位址空間,通路所有所有實體記憶體。如下圖。

深入了解Linux高端記憶體

圖檔發自簡書App

例如核心想通路2G開始的一段大小為1MB的實體記憶體,即實體位址範圍為0×80000000 ~ 0x800FFFFF。通路之前先找到一段1MB大小的空閑位址空間,假設找到的空閑位址空間為0xF8700000 ~ 0xF87FFFFF,用這1MB的邏輯位址空間映射到實體位址空間0×80000000 ~ 0x800FFFFF的記憶體。映射關系如下:

邏輯位址 實體位址
0xF8700000 0x80000000
0xF8700001 0x80000001
0xF87FFFFF 0x800FFFFF
當核心通路完0×80000000 ~ 0x800FFFFF實體記憶體後,就将0xF8700000 ~ 0xF87FFFFF核心線性空間釋放。這樣其他程序或代碼也可以使用0xF8700000 ~ 0xF87FFFFF這段位址通路其他實體記憶體。

從上面的描述,我們可以知道高端記憶體的最基本思想:借一段位址空間,建立臨時位址映射,用完後釋放,達到這段位址空間可以循環使用,通路所有實體記憶體。

看到這裡,不禁有人會問:萬一有核心程序或子產品一直占用某段邏輯位址空間不釋放,怎麼辦?若真的出現的這種情況,則核心的高端記憶體位址空間越來越緊張,若都被占用不釋放,則沒有建立映射到實體記憶體都無法通路了。

五、Linux核心高端記憶體的劃分

核心将高端記憶體劃分為3部分:

  • VMALLOC_START ~ VMALLOC_END
  • KMAP_BASE ~ FIXADDR_START
  • FIXADDR_START ~ 0xFFFFFFFF

對于高端記憶體,可以通過 alloc_page() 或者其它函數獲得對應的 page,但是要想通路實際實體記憶體,還得把 page 轉為線性位址才行(為什麼?想想 MMU 是如何通路實體記憶體的),也就是說,我們需要為高端記憶體對應的 page 找一個線性空間,這個過程稱為高端記憶體映射。

  1. 映射到”核心動态映射空間”(noncontiguous memory allocation)。

    這種方式很簡單,因為通過 vmalloc() ,在”核心動态映射空間”申請記憶體的時候,就可能從高端記憶體獲得頁面(參看 vmalloc 的實作),是以說高端記憶體有可能映射到”核心動态映射空間”中。

  2. 持久核心映射(permanent kernel mapping)。

    如果是通過 alloc_page() 獲得了高端記憶體對應的 page,如何給它找個線性空間?核心專門為此留出一塊線性空間,從 PKMAP_BASE 到 FIXADDR_START ,用于映射高端記憶體。在 2.6核心上,這個位址範圍是 4G-8M 到 4G-4M 之間。這個空間起叫”核心永久映射空間”或者”永久核心映射空間”。這個空間和其它空間使用同樣的頁目錄表,對于核心來說,就是 swapper_pg_dir,對普通程序來說,通過 CR3 寄存器指向。通常情況下,這個空間是 4M 大小,是以僅僅需要一個頁表即可,核心通過來 pkmap_page_table 尋找這個頁表。通過 kmap(),可以把一個 page 映射到這個空間來。由于這個空間是 4M 大小,最多能同時映射 1024 個 page。是以,對于不使用的的 page,及應該時從這個空間釋放掉(也就是解除映射關系),通過 kunmap() ,可以把一個 page 對應的線性位址從這個空間釋放出來。

  3. 臨時映射(temporary kernel mapping)

    核心在 FIXADDR_START 到 FIXADDR_TOP 之間保留了一些線性空間用于特殊需求。這個空間稱為”固定映射空間”在這個空間中,有一部分用于高端記憶體的臨時映射。

六、常見問題:

  1. 使用者空間(程序)是否有高端記憶體概念?

    使用者程序沒有高端記憶體概念。隻有在核心空間才存在高端記憶體。使用者程序最多隻可以通路3G實體記憶體,而核心程序可以通路所有實體記憶體。

  2. 64位核心中有高端記憶體嗎?

    目前現實中,64位Linux核心不存在高端記憶體,因為64位核心可以支援超過512GB記憶體。若機器安裝的實體記憶體超過核心位址空間範圍,就會存在高端記憶體。

  3. 使用者程序能通路多少實體記憶體?核心代碼能通路多少實體記憶體?

    32位系統使用者程序最大可以通路3GB,核心代碼可以通路所有實體記憶體。

    64位系統使用者程序最大可以通路超過512GB,核心代碼可以通路所有實體記憶體。

  4. 高端記憶體和實體位址、邏輯位址、線性位址的關系?

    高端記憶體隻和邏輯位址有關系,和邏輯位址、實體位址沒有直接關系。

  5. 為什麼不把所有的位址空間都配置設定給核心?

    若把所有位址空間都給記憶體,那麼使用者程序怎麼使用記憶體?怎麼保證核心使用記憶體和使用者程序不起沖突?

    (1)讓我們忽略Linux對段式記憶體映射的支援。 在保護模式下,我們知道無論CPU運作于使用者态還是核心态,CPU執行程式所通路的位址都是虛拟位址,MMU 必須通過讀取控制寄存器CR3中的值作為目前頁面目錄的指針,進而根據分頁記憶體映射機制(參看相關文檔)将該虛拟位址轉換為真正的實體位址才能讓CPU真 正的通路到實體位址。

    (2)對于32位的Linux,其每一個程序都有4G的尋址空間,但當一個程序通路其虛拟記憶體空間中的某個位址時又是怎樣實作不與其它程序的虛拟空間混淆 的呢?每個程序都有其自身的頁面目錄PGD,Linux将該目錄的指針存放在與程序對應的記憶體結構task_struct.(struct mm_struct)mm->pgd中。每當一個程序被排程(schedule())即将進入運作态時,Linux核心都要用該程序的PGD指針設 置CR3(switch_mm())。

    (3)當建立一個新的程序時,都要為新程序建立一個新的頁面目錄PGD,并從核心的頁面目錄swapper_pg_dir中複制核心區間頁面目錄項至建立程序頁面目錄PGD的相應位置,具體過程如下:

    do_fork() --> copy_mm() --> mm_init() --> pgd_alloc() --> set_pgd_fast() --> get_pgd_slow() --> memcpy(&PGD + USER_PTRS_PER_PGD, swapper_pg_dir + USER_PTRS_PER_PGD, (PTRS_PER_PGD - USER_PTRS_PER_PGD) * sizeof(pgd_t))

    這樣一來,每個程序的頁面目錄就分成了兩部分,第一部分為“使用者空間”,用來映射其整個程序空間(0x0000 0000-0xBFFF FFFF)即3G位元組的虛拟位址;第二部分為“系統空間”,用來映射(0xC000 0000-0xFFFF FFFF)1G位元組的虛拟位址。可以看出Linux系統中每個程序的頁面目錄的第二部分是相同的,是以從程序的角度來看,每個程序有4G位元組的虛拟空間, 較低的3G位元組是自己的使用者空間,最高的1G位元組則為與所有程序以及核心共享的系統空間。

    (4)現在假設我們有如下一個情景:

    在程序A中通過系統調用​

    ​sethostname(const char *name,seze_t len)​

    ​設定計算機在網絡中的“主機名”。在該情景中我們勢必涉及到從使用者空間向核心空間傳遞資料的問題,name是使用者空間中的位址,它要通過系統調用設定到核心中的某個位址中。讓我們看看這個 過程中的一些細節問題:系統調用的具體實作是将系統調用的參數依次存入寄存器ebx,ecx,edx,esi,edi(最多5個參數,該情景有兩個 name和len),接着将系統調用号存入寄存器eax,然後通過中斷指令“int 80”使程序A進入系統空間。由于程序的CPU運作級别小于等于為系統調用設定的陷阱門的準入級别3,是以可以暢通無阻的進入系統空間去執行為int 80設定的函數指針system_call()。由于system_call()屬于核心空間,其運作級别DPL為0,CPU要将堆棧切換到核心堆棧,即 程序A的系統空間堆棧。我們知道核心為建立程序建立task_struct結構時,共配置設定了兩個連續的頁面,即8K的大小,并将底部約1k的大小用于 task_struct( 如​

    ​#define alloc_task_struct() ((struct task_struct *) __get_free_pages(GFP_KERNEL,1))​

    ​),而其餘部分記憶體用于系統空間的堆棧空間,即當從使用者空間轉入系統空間時,堆棧指針 esp變成了(​

    ​alloc_task_struct()+8192​

    ​),這也是為什麼系統空間通常用宏定義current(參看其實作)擷取目前程序的 task_struct位址的原因。每次在程序從使用者空間進入系統空間之初,系統堆棧就已經被依次壓入使用者堆棧SS、使用者堆棧指針ESP、EFLAGS、 使用者空間CS、EIP,接着system_call()将eax壓入,再接着調用SAVE_ALL依次壓入ES、DS、EAX、EBP、EDI、ESI、 EDX、ECX、EBX,然後調用sys_call_table+4*%EAX,本情景為sys_sethostname()。

    5)在sys_sethostname()中,經過一些保護考慮後,調用copy_from_user(to,from,n),其中to指向核心空間 system_utsname.nodename,譬如0xE625A000,from指向使用者空間譬如0x8010FE00。現在程序A進入了核心,在 系統空間中運作,MMU根據其PGD将虛拟位址完成到實體位址的映射,最終完成從使用者空間到系統空間資料的複制。準備複制之前核心先要确定使用者空間位址和 長度的合法性,至于從該使用者空間位址開始的某個長度的整個區間是否已經映射并不去檢查,如果區間内某個位址未映射或讀寫權限等問題出現時,則視為壞位址, 就産生一個頁面異常,讓頁面異常服務程式處理。過程如 下:copy_from_user()->generic_copy_from_user()->access_ok()+__copy_user_zeroing().

  • 程序尋址空間0~4G 。
  • 程序在使用者态隻能通路03G,隻有進入核心态才能通路3G4G 。
  • 程序通過系統調用進入核心态。
  • 每個程序虛拟空間的3G~4G部分是相同的。
  • 程序從使用者态進入核心态不會引起CR3的改變但會引起堆棧的改變。

繼續閱讀