Linux使用者空間與核心空間(理解高階記憶體)
Linux 作業系統和驅動程式執行在核心空間,應用程式執行在使用者空間,兩者不能簡單地使用指標傳遞資料,因為Linux使用的虛擬記憶體機制,使用者空間的資料可能被換出,當核心空間使用使用者空間指標時,對應的資料可能不在記憶體中。
Linux核心地址對映模型
x86 CPU採用了段頁式地址對映模型。程序程式碼中的地址為邏輯地址,經過段頁式地址對映後,才真正訪問實體記憶體。
段頁式機制如下圖。
Linux核心地址空間劃分
通常32位Linux核心地址空間劃分0~3G為使用者空間,3~4G為核心空間。注意這裡是32位核心地址空間劃分,64位核心地址空間劃分是不同的。
Linux核心高階記憶體的由來
當核心模組程式碼或執行緒訪問記憶體時,程式碼中的記憶體地址都為邏輯地址,而對應到真正的實體記憶體地址,需要地址一對一的對映,如邏輯地址0xc0000003對應的實體地址為0×3,0xc0000004對應的實體地址為0×4,… …,邏輯地址與實體地址對應的關係為
實體地址 = 邏輯地址 – 0xC0000000
邏輯地址 | 實體記憶體地址 |
0xc0000000 | 0×0 |
0xc0000001 | 0×1 |
0xc0000002 | 0×2 |
0xc0000003 | 0×3 |
… | … |
0xe0000000 | 0×20000000 |
… | … |
0xffffffff | 0×40000000 ?? |
假 設按照上述簡單的地址對映關係,那麼核心邏輯地址空間訪問為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核心高階記憶體的理解
前 面我們解釋了高階記憶體的由來。 Linux將核心地址空間劃分為三部分ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM,高階記憶體HIGH_MEM地址空間範圍為 0xF8000000 ~ 0xFFFFFFFF(896MB~1024MB)。那麼如核心是如何藉助128MB高階記憶體地址空間是如何實現訪問可以所有實體記憶體?
當核心想訪問高於896MB實體地址記憶體時,從0xF8000000 ~ 0xFFFFFFFF地址空間範圍內找一段相應大小空閒的邏輯地址空間,借用一會。借用這段邏輯地址空間,建立對映到想訪問的那段實體記憶體(即填充核心PTE頁面表),臨時用一會,用完後歸還。這樣別人也可以借用這段地址空間訪問其他實體記憶體,實現了使用有限的地址空間,訪問所有所有實體記憶體。如下圖。
例 如核心想訪問2G開始的一段大小為1MB的實體記憶體,即實體地址範圍為0×80000000 ~ 0x800FFFFF。訪問之前先找到一段1MB大小的空閒地址空間,假設找到的空閒地址空間為0xF8700000 ~ 0xF87FFFFF,用這1MB的邏輯地址空間對映到實體地址空間0×80000000 ~ 0x800FFFFF的記憶體。對映關係如下:
邏輯地址 | 實體記憶體地址 |
0xF8700000 | 0×80000000 |
0xF8700001 | 0×80000001 |
0xF8700002 | 0×80000002 |
… | … |
0xF87FFFFF | 0x800FFFFF |
當核心訪問完0×80000000 ~ 0x800FFFFF實體記憶體後,就將0xF8700000 ~ 0xF87FFFFF核心線性空間釋放。這樣其他程序或程式碼也可以使用0xF8700000 ~ 0xF87FFFFF這段地址訪問其他實體記憶體。
從上面的描述,我們可以知道高階記憶體的最基本思想:借一段地址空間,建立臨時地址對映,用完後釋放,達到這段地址空間可以迴圈使用,訪問所有實體記憶體。
看到這裡,不禁有人會問:萬一有核心程序或模組一直佔用某段邏輯地址空間不釋放,怎麼辦?若真的出現的這種情況,則核心的高階記憶體地址空間越來越緊張,若都被佔用不釋放,則沒有建立對映到實體記憶體都無法訪問了。
在 香港尖沙咀有些寫字樓,洗手間很少且有門鎖的。客戶要去洗手間的話,可以向前臺拿鑰匙,方便完後,把鑰匙歸還到前臺。這樣雖然只有一個洗 手間,但可以滿足所有客戶去洗手間的需求。要是某個客戶一直佔用洗手間、鑰匙不歸還,那麼其他客戶都無法上洗手間了。Linux核心高階記憶體管理的思想類 似。
Linux核心高階記憶體的劃分 核心將高階記憶體劃分為3部分:VMALLOC_START~VMALLOC_END、KMAP_BASE~FIXADDR_START和FIXADDR_START~4G。
對 於高階記憶體,可以通過 alloc_page() 或者其它函式獲得對應的 page,但是要想訪問實際實體記憶體,還得把 page 轉為線性地址才行(為什麼?想想 MMU 是如何訪問實體記憶體的),也就是說,我們需要為高階記憶體對應的 page 找一個線性空間,這個過程稱為高階記憶體對映。
對應高階記憶體的3部分,高階記憶體對映有三種方式: 對映到”核心動態對映空間”(noncontiguous memory allocation) 這種方式很簡單,因為通過 vmalloc() ,在”核心動態對映空間”申請記憶體的時候,就可能從高階記憶體獲得頁面(參看 vmalloc 的實現),因此說高階記憶體有可能對映到”核心動態對映空間”中。
持久核心對映(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 對應的線性地址從這個空間釋放出來。
臨時對映(temporary kernel mapping) 核心在 FIXADDR_START 到 FIXADDR_TOP 之間保留了一些線性空間用於特殊需求。這個空間稱為”固定對映空間”在這個空間中,有一部分用於高階記憶體的臨時對映。
這塊空間具有如下特點: (1)每個 CPU 佔用一塊空間 (2)在每個 CPU 佔用的那塊空間中,又分為多個小空間,每個小空間大小是 1 個 page,每個小空間用於一個目的,這些目的定義在 kmap_types.h 中的 km_type 中。
當要進行一次臨時對映的時候,需要指定對映的目的,根據對映目的,可以找到對應的小空間,然後把這個空間的地址作為對映地址。這意味著一次臨時對映會導致以前的對映被覆蓋。通過 kmap_atomic() 可實現臨時對映。
常見問題:
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(). (6)小結: *程序定址空間0~4G *程序在使用者態只能訪問0~3G,只有進入核心態才能訪問3G~4G *程序通過系統呼叫進入核心態 *每個程序虛擬空間的3G~4G部分是相同的 *程序從使用者態進入核心態不會引起CR3的改變但會引起堆疊的改變 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中對核心空間中地址對映的說明及定義: /* * This handles the memory map.. We could make this a config * option, but too many people screw it up, and too few need * it. * * A __PAGE_OFFSET of 0xC0000000 means that the kernel has * a virtual address space of one gigabyte, which limits the * amount of physical memory you can use to about 950MB. * * If you want more physical memory than this then see the CONFIG_HIGHMEM4G * and CONFIG_HIGHMEM64G options in the kernel configuration. */ #define __PAGE_OFFSET (0xC0000000) …… #define PAGE_OFFSET ((unsigned long)__PAGE_OFFSET) #define __pa(x) ((unsigned long)(x)-PAGE_OFFSET) #define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET)) 源 程式碼的註釋中說明,如果你的實體記憶體大於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)); 這是一行嵌入式彙編程式碼,其含義是將下一個程序的頁目錄起始地址next_pgd,通過__pa()轉換成實體地址,存放在某個暫存器中,然後用mov指令將其寫入CR3暫存器中。經過這行語句的處理,CR3就指向新程序next的頁目錄表PGD了。