摘要:Linux 操做系統和驅動程序運行在內核空間,應用程序運行在用戶空間,二者不能簡單地使用指針傳遞數據,由於Linux使用的虛擬內存機制,用戶空間的數據可能被換出,當內核空間使用用戶空間指針時,對應的數據可能不在內存中。用戶空間的內存映射採用段頁式,而內核空間有本身的規則;本文旨在探討內核空間的地址映射。
Linux內核地址空間劃分
node
一般32位Linux內核虛擬地址空間劃分0~3G爲用戶空間,3~4G爲內核空間(注意,內核可使用的線性地址只有1G)。注意這裏是32位內核地址空間劃分,64位內核地址空間劃分是不一樣的。
網絡
![](http://static.javashuo.com/static/loading.gif)
1)線性地址空間:是指Linux系統中從0x00000000到0xFFFFFFFF整個4GB虛擬存儲空間。數據結構
2)內核空間:內核空間表示運行在處理器最高級別的超級用戶模式(supervisor mode)下的代碼或數據,內核空間佔用從0xC0000000到0xFFFFFFFF的1GB線性地址空間,內核線性地址空間由全部進程共享,但只有運行在內核態的進程才能訪問,用戶進程能夠經過系統調用切換到內核態訪問內核空間,進程運行在內核態時所產生的地址都屬於內核空間。架構
3)用戶空間:用戶空間佔用從0x00000000到0xBFFFFFFF共3GB的線性地址空間,每一個進程都有一個獨立的3GB用戶空間,因此用戶空間由每一個進程獨有,可是內核線程沒有用戶空間,由於它不產生用戶空間地址。另外子進程共享(繼承)父進程的用戶空間只是使用與父進程相同的用戶線性地址到物理內存地址的映射關係,而不是共享父進程用戶空間。運行在用戶態和內核態的進程均可以訪問用戶空間。app
Linux內核高端內存的由來
函數
當內核模塊代碼或線程訪問內存時,代碼中的內存地址都爲邏輯地址,而對應到真正的物理內存地址,須要地址一對一的映射,如邏輯地址0xc0000003對應的物理地址爲0×3,0xc0000004對應的物理地址爲0×4,… …,邏輯地址與物理地址對應的關係爲this
物理地址 = 邏輯地址 – 0xC0000000:這是內核地址空間的地址轉換關係,注意內核的虛擬地址在「高端」,可是ta映射的物理內存地址在低端。atom
邏輯地址 |
物理內存地址 |
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之後的內存。spa
顯 然不能將內核地址空間0xc0000000 ~ 0xfffffff所有用來簡單的地址映射。所以x86架構中將內核地址空間劃分三部分:ZONE_DMA、ZONE_NORMAL和 ZONE_HIGHMEM。ZONE_HIGHMEM即爲高端內存,這就是內存高端內存概念的由來。操作系統
在x86結構中,三種類型的區域(從3G開始計算)以下:
ZONE_DMA 內存開始的16MB
ZONE_NORMAL 16MB~896MB
ZONE_HIGHMEM 896MB ~ 結束(1G)
![](http://static.javashuo.com/static/loading.gif)
Linux內核高端內存的理解
前 面咱們解釋了高端內存的由來。 Linux將內核地址空間劃分爲三部分ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM,高端內存HIGH_MEM地址空間範圍爲 0xF8000000 ~ 0xFFFFFFFF(896MB~1024MB)。那麼如內核是如何藉助128MB高端內存地址空間是如何實現訪問能夠全部物理內存?
當內核想訪問高於896MB物理地址內存時,從0xF8000000 ~ 0xFFFFFFFF地址空間範圍內找一段相應大小空閒的邏輯地址空間,借用一會。借用這段邏輯地址空間,創建映射到想訪問的那段物理內存(即填充內核PTE頁面表),臨時用一會,用完後歸還。這樣別人也能夠借用這段地址空間訪問其餘物理內存,實現了使用有限的地址空間,訪問全部全部物理內存。以下圖。
![](http://static.javashuo.com/static/loading.gif)
例 如內核想訪問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內核高端內存的劃分
內核將高端內存劃分爲3部分:VMALLOC_START~VMALLOC_END、KMAP_BASE~FIXADDR_START和FIXADDR_START~4G。
![](http://static.javashuo.com/static/loading.gif)
對 於高端內存,能夠經過 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() 可實現臨時映射。
常見問題:
一、用戶空間(進程)是否有高端內存概念?
用戶進程沒有高端內存概念。只有在內核空間才存在高端內存。用戶進程最多隻能夠訪問3G物理內存,而內核進程能夠訪問全部物理內存。
二、64位內核中有高端內存嗎?
目前現實中,64位Linux內核不存在高端內存,由於64位內核能夠支持超過512GB內存。若機器安裝的物理內存超過內核地址空間範圍,就會存在高端內存。
三、用戶進程能訪問多少物理內存?內核代碼能訪問多少物理內存?
32位系統用戶進程最大能夠訪問3GB,內核代碼能夠訪問全部物理內存。
64位系統用戶進程最大能夠訪問超過512GB,內核代碼能夠訪問全部物理內存。
四、高端內存和物理地址、邏輯地址、線性地址的關係?
高端內存只和邏輯地址有關係,和邏輯地址、物理地址沒有直接關係。
五、爲何不把全部的地址空間都分配給內核?
若把全部地址空間都給內存,那麼用戶進程怎麼使用內存?怎麼保證內核使用內存和用戶進程不起衝突?
(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了。