Linux 操作系統(tǒng)和驅(qū)動程序運行在內(nèi)核空間,應(yīng)用程序運行在用戶空間,兩者不能簡單地使用指針傳遞數(shù)據(jù),因為Linux使用的虛擬內(nèi)存機制,用戶空間的數(shù)據(jù)可能被換出,當(dāng)內(nèi)核空間使用用戶空間指針時,對應(yīng)的數(shù)據(jù)可能不在內(nèi)存中。用戶空間的內(nèi)存映射采用段頁式,而內(nèi)核空間有自己的規(guī)則;本文旨在探討內(nèi)核空間的地址映射。
Linux內(nèi)核地址空間劃分
通常32位Linux內(nèi)核虛擬地址空間劃分0~3G為用戶空間,3~4G為內(nèi)核空間(注意,內(nèi)核可以使用的線性地址只有1G)。注意這里是32位內(nèi)核地址空間劃分,64位內(nèi)核地址空間劃分是不同的。
Linux內(nèi)核高端內(nèi)存的由來
當(dāng)內(nèi)核模塊代碼或線程訪問內(nèi)存時,代碼中的內(nèi)存地址都為邏輯地址,而對應(yīng)到真正的物理內(nèi)存地址,需要地址一對一的映射,如邏輯地址0xc0000003對應(yīng)的物理地址為0×3,0xc0000004對應(yīng)的物理地址為0×4,… …,邏輯地址與物理地址對應(yīng)的關(guān)系為
物理地址 = 邏輯地址 – 0xC0000000:這是內(nèi)核地址空間的地址轉(zhuǎn)換關(guān)系,注意內(nèi)核的虛擬地址在“高端”,但是ta映射的物理內(nèi)存地址在低端。
邏輯地址物理內(nèi)存地址0xc00000000×00xc00000010×10xc00000020×20xc00000030×3……0xe00000000×20000000……0xffffffff0×40000000 ??
假 設(shè)按照上述簡單的地址映射關(guān)系,那么內(nèi)核邏輯地址空間訪問為0xc0000000 ~ 0xffffffff,那么對應(yīng)的物理內(nèi)存范圍就為0×0 ~ 0×40000000,即只能訪問1G物理內(nèi)存。若機器中安裝8G物理內(nèi)存,那么內(nèi)核就只能訪問前1G物理內(nèi)存,后面7G物理內(nèi)存將會無法訪問,因為內(nèi)核 的地址空間已經(jīng)全部映射到物理內(nèi)存地址范圍0×0 ~ 0×40000000。即使安裝了8G物理內(nèi)存,那么物理地址為0×40000001的內(nèi)存,內(nèi)核該怎么去訪問呢?代碼中必須要有內(nèi)存邏輯地址 的,0xc0000000 ~ 0xffffffff的地址空間已經(jīng)被用完了,所以無法訪問物理地址0×40000000以后的內(nèi)存。
顯 然不能將內(nèi)核地址空間0xc0000000 ~ 0xfffffff全部用來簡單的地址映射。因此x86架構(gòu)中將內(nèi)核地址空間劃分三部分:ZONE_DMA、ZONE_NORMAL和 ZONE_HIGHMEM。ZONE_HIGHMEM即為高端內(nèi)存,這就是內(nèi)存高端內(nèi)存概念的由來。
在x86結(jié)構(gòu)中,三種類型的區(qū)域(從3G開始計算)如下:
ZONE_DMA??????? 內(nèi)存開始的16MB
ZONE_NORMAL ??? ? 16MB~896MB
ZONE_HIGHMEM?? ? ? 896MB ~ 結(jié)束(1G)
Linux內(nèi)核高端內(nèi)存的理解
前 面我們解釋了高端內(nèi)存的由來。 Linux將內(nèi)核地址空間劃分為三部分ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM,高端內(nèi)存HIGH_MEM地址空間范圍為 0xF8000000 ~ 0xFFFFFFFF(896MB~1024MB)。那么如內(nèi)核是如何借助128MB高端內(nèi)存地址空間是如何實現(xiàn)訪問可以所有物理內(nèi)存?
當(dāng)內(nèi)核想訪問高于896MB物理地址內(nèi)存時,從0xF8000000 ~ 0xFFFFFFFF地址空間范圍內(nèi)找一段相應(yīng)大小空閑的邏輯地址空間,借用一會。借用這段邏輯地址空間,建立映射到想訪問的那段物理內(nèi)存(即填充內(nèi)核PTE頁面表),臨時用一會,用完后歸還。這樣別人也可以借用這段地址空間訪問其他物理內(nèi)存,實現(xiàn)了使用有限的地址空間,訪問所有所有物理內(nèi)存。如下圖。
例 如內(nèi)核想訪問2G開始的一段大小為1MB的物理內(nèi)存,即物理地址范圍為0×80000000 ~ 0x800FFFFF。訪問之前先找到一段1MB大小的空閑地址空間,假設(shè)找到的空閑地址空間為0xF8700000 ~ 0xF87FFFFF,用這1MB的邏輯地址空間映射到物理地址空間0×80000000 ~ 0x800FFFFF的內(nèi)存。映射關(guān)系如下:
邏輯地址物理內(nèi)存地址0xF87000000×800000000xF87000010×800000010xF87000020×80000002……0xF87FFFFF0x800FFFFF
當(dāng)內(nèi)核訪問完0×80000000 ~ 0x800FFFFF物理內(nèi)存后,就將0xF8700000 ~ 0xF87FFFFF內(nèi)核線性空間釋放。這樣其他進程或代碼也可以使用0xF8700000 ~ 0xF87FFFFF這段地址訪問其他物理內(nèi)存。
從上面的描述,我們可以知道高端內(nèi)存的最基本思想:借一段地址空間,建立臨時地址映射,用完后釋放,達到這段地址空間可以循環(huán)使用,訪問所有物理內(nèi)存。
看到這里,不禁有人會問:萬一有內(nèi)核進程或模塊一直占用某段邏輯地址空間不釋放,怎么辦?若真的出現(xiàn)的這種情況,則內(nèi)核的高端內(nèi)存地址空間越來越緊張,若都被占用不釋放,則沒有建立映射到物理內(nèi)存都無法訪問了。
Linux內(nèi)核高端內(nèi)存的劃分
內(nèi)核將高端內(nèi)存劃分為3部分:VMALLOC_START~VMALLOC_END、KMAP_BASE~FIXADDR_START和FIXADDR_START~4G。
對 于高端內(nèi)存,可以通過 alloc_page() 或者其它函數(shù)獲得對應(yīng)的 page,但是要想訪問實際物理內(nèi)存,還得把 page 轉(zhuǎn)為線性地址才行(為什么?想想 MMU 是如何訪問物理內(nèi)存的),也就是說,我們需要為高端內(nèi)存對應(yīng)的 page 找一個線性空間,這個過程稱為高端內(nèi)存映射。
對應(yīng)高端內(nèi)存的3部分,高端內(nèi)存映射有三種方式:
映射到”內(nèi)核動態(tài)映射空間”(noncontiguous memory allocation)
這種方式很簡單,因為通過 vmalloc() ,在”內(nèi)核動態(tài)映射空間”申請內(nèi)存的時候,就可能從高端內(nèi)存獲得頁面(參看 vmalloc 的實現(xiàn)),因此說高端內(nèi)存有可能映射到”內(nèi)核動態(tài)映射空間”中。
持久內(nèi)核映射(permanent kernel mapping)
如果是通過 alloc_page() 獲得了高端內(nèi)存對應(yīng)的 page,如何給它找個線性空間?
內(nèi)核專門為此留出一塊線性空間,從 PKMAP_BASE 到 FIXADDR_START ,用于映射高端內(nèi)存。在 2.6內(nèi)核上,這個地址范圍是 4G-8M 到 4G-4M 之間。這個空間起叫”內(nèi)核永久映射空間”或者”永久內(nèi)核映射空間”。這個空間和其它空間使用同樣的頁目錄表,對于內(nèi)核來說,就是 swapper_pg_dir,對普通進程來說,通過 CR3 寄存器指向。通常情況下,這個空間是 4M 大小,因此僅僅需要一個頁表即可,內(nèi)核通過來 pkmap_page_table 尋找這個頁表。通過 kmap(),可以把一個 page 映射到這個空間來。由于這個空間是 4M 大小,最多能同時映射 1024 個 page。因此,對于不使用的的 page,及應(yīng)該時從這個空間釋放掉(也就是解除映射關(guān)系),通過 kunmap() ,可以把一個 page 對應(yīng)的線性地址從這個空間釋放出來。
臨時映射(temporary kernel mapping)
內(nèi)核在 FIXADDR_START 到 FIXADDR_TOP 之間保留了一些線性空間用于特殊需求。這個空間稱為”固定映射空間”在這個空間中,有一部分用于高端內(nèi)存的臨時映射。
這塊空間具有如下特點:
(1)每個 CPU 占用一塊空間
(2)在每個 CPU 占用的那塊空間中,又分為多個小空間,每個小空間大小是 1 個 page,每個小空間用于一個目的,這些目的定義在 kmap_types.h 中的 km_type 中。
當(dāng)要進行一次臨時映射的時候,需要指定映射的目的,根據(jù)映射目的,可以找到對應(yīng)的小空間,然后把這個空間的地址作為映射地址。這意味著一次臨時映射會導(dǎo)致以前的映射被覆蓋。通過 kmap_atomic() 可實現(xiàn)臨時映射。
常見問題:
1、用戶空間(進程)是否有高端內(nèi)存概念?
用戶進程沒有高端內(nèi)存概念。只有在內(nèi)核空間才存在高端內(nèi)存。用戶進程最多只可以訪問3G物理內(nèi)存,而內(nèi)核進程可以訪問所有物理內(nèi)存。
2、64位內(nèi)核中有高端內(nèi)存嗎?
目前現(xiàn)實中,64位Linux內(nèi)核不存在高端內(nèi)存,因為64位內(nèi)核可以支持超過512GB內(nèi)存。若機器安裝的物理內(nèi)存超過內(nèi)核地址空間范圍,就會存在高端內(nèi)存。
3、用戶進程能訪問多少物理內(nèi)存?內(nèi)核代碼能訪問多少物理內(nèi)存?
32位系統(tǒng)用戶進程最大可以訪問3GB,內(nèi)核代碼可以訪問所有物理內(nèi)存。
64位系統(tǒng)用戶進程最大可以訪問超過512GB,內(nèi)核代碼可以訪問所有物理內(nèi)存。
4、高端內(nèi)存和物理地址、邏輯地址、線性地址的關(guān)系?
高端內(nèi)存只和邏輯地址有關(guān)系,和邏輯地址、物理地址沒有直接關(guān)系。
5、為什么不把所有的地址空間都分配給內(nèi)核?
若把所有地址空間都給內(nèi)存,那么用戶進程怎么使用內(nèi)存?怎么保證內(nèi)核使用內(nèi)存和用戶進程不起沖突?
(1)讓我們忽略Linux對段式內(nèi)存映射的支持。 在保護模式下,我們知道無論CPU運行于用戶態(tài)還是核心態(tài),CPU執(zhí)行程序所訪問的地址都是虛擬地址,MMU 必須通過讀取控制寄存器CR3中的值作為當(dāng)前頁面目錄的指針,進而根據(jù)分頁內(nèi)存映射機制(參看相關(guān)文檔)將該虛擬地址轉(zhuǎn)換為真正的物理地址才能讓CPU真 正的訪問到物理地址。
(2)對于32位的Linux,其每一個進程都有4G的尋址空間,但當(dāng)一個進程訪問其虛擬內(nèi)存空間中的某個地址時又是怎樣實現(xiàn)不與其它進程的虛擬空間混淆 的呢?每個進程都有其自身的頁面目錄PGD,Linux將該目錄的指針存放在與進程對應(yīng)的內(nèi)存結(jié)構(gòu)task_struct.(struct mm_struct)mm->pgd中。每當(dāng)一個進程被調(diào)度(schedule())即將進入運行態(tài)時,Linux內(nèi)核都要用該進程的PGD指針設(shè) 置CR3(switch_mm())。
(3)當(dāng)創(chuàng)建一個新的進程時,都要為新進程創(chuàng)建一個新的頁面目錄PGD,并從內(nèi)核的頁面目錄swapper_pg_dir中復(fù)制內(nèi)核區(qū)間頁面目錄項至新建進程頁面目錄PGD的相應(yīng)位置,具體過程如下:
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字節(jié)的虛擬地址;第二部分為“系統(tǒng)空間”,用來映射(0xC000 0000-0xFFFF FFFF)1G字節(jié)的虛擬地址??梢钥闯鯨inux系統(tǒng)中每個進程的頁面目錄的第二部分是相同的,所以從進程的角度來看,每個進程有4G字節(jié)的虛擬空間, 較低的3G字節(jié)是自己的用戶空間,最高的1G字節(jié)則為與所有進程以及內(nèi)核共享的系統(tǒng)空間。
(4)現(xiàn)在假設(shè)我們有如下一個情景:
在進程A中通過系統(tǒng)調(diào)用sethostname(const char *name,seze_t len)設(shè)置計算機在網(wǎng)絡(luò)中的“主機名”.
在該情景中我們勢必涉及到從用戶空間向內(nèi)核空間傳遞數(shù)據(jù)的問題,name是用戶空間中的地址,它要通過系統(tǒng)調(diào)用設(shè)置到內(nèi)核中的某個地址中。讓我們看看這個 過程中的一些細節(jié)問題:系統(tǒng)調(diào)用的具體實現(xiàn)是將系統(tǒng)調(diào)用的參數(shù)依次存入寄存器ebx,ecx,edx,esi,edi(最多5個參數(shù),該情景有兩個 name和len),接著將系統(tǒng)調(diào)用號存入寄存器eax,然后通過中斷指令“int 80”使進程A進入系統(tǒng)空間。由于進程的CPU運行級別小于等于為系統(tǒng)調(diào)用設(shè)置的陷阱門的準(zhǔn)入級別3,所以可以暢通無阻的進入系統(tǒng)空間去執(zhí)行為int 80設(shè)置的函數(shù)指針system_call()。由于system_call()屬于內(nèi)核空間,其運行級別DPL為0,CPU要將堆棧切換到內(nèi)核堆棧,即 進程A的系統(tǒng)空間堆棧。我們知道內(nèi)核為新建進程創(chuàng)建task_struct結(jié)構(gòu)時,共分配了兩個連續(xù)的頁面,即8K的大小,并將底部約1k的大小用于 task_struct(如#define alloc_task_struct() ((struct task_struct *) __get_free_pages(GFP_KERNEL,1))),而其余部分內(nèi)存用于系統(tǒng)空間的堆??臻g,即當(dāng)從用戶空間轉(zhuǎn)入系統(tǒng)空間時,堆棧指針 esp變成了(alloc_task_struct()+8192),這也是為什么系統(tǒng)空間通常用宏定義current(參看其實現(xiàn))獲取當(dāng)前進程的 task_struct地址的原因。每次在進程從用戶空間進入系統(tǒng)空間之初,系統(tǒng)堆棧就已經(jīng)被依次壓入用戶堆棧SS、用戶堆棧指針ESP、EFLAGS、 用戶空間CS、EIP,接著system_call()將eax壓入,再接著調(diào)用SAVE_ALL依次壓入ES、DS、EAX、EBP、EDI、ESI、 EDX、ECX、EBX,然后調(diào)用sys_call_table+4*%EAX,本情景為sys_sethostname()。
(5)在sys_sethostname()中,經(jīng)過一些保護考慮后,調(diào)用copy_from_user(to,from,n),其中to指向內(nèi)核空間 system_utsname.nodename,譬如0xE625A000,from指向用戶空間譬如0x8010FE00?,F(xiàn)在進程A進入了內(nèi)核,在 系統(tǒng)空間中運行,MMU根據(jù)其PGD將虛擬地址完成到物理地址的映射,最終完成從用戶空間到系統(tǒng)空間數(shù)據(jù)的復(fù)制。準(zhǔn)備復(fù)制之前內(nèi)核先要確定用戶空間地址和 長度的合法性,至于從該用戶空間地址開始的某個長度的整個區(qū)間是否已經(jīng)映射并不去檢查,如果區(qū)間內(nèi)某個地址未映射或讀寫權(quán)限等問題出現(xiàn)時,則視為壞地址, 就產(chǎn)生一個頁面異常,讓頁面異常服務(wù)程序處理。過程如 下:copy_from_user()->generic_copy_from_user()->access_ok()+__copy_user_zeroing().
(6)小結(jié):
*進程尋址空間0~4G??
*進程在用戶態(tài)只能訪問0~3G,只有進入內(nèi)核態(tài)才能訪問3G~4G??
*進程通過系統(tǒng)調(diào)用進入內(nèi)核態(tài)
*每個進程虛擬空間的3G~4G部分是相同的??
*進程從用戶態(tài)進入內(nèi)核態(tài)不會引起CR3的改變但會引起堆棧的改變
Linux 簡化了分段機制,使得虛擬地址與線性地址總是一致,因此,Linux的虛擬地址空間也為0~4G。Linux內(nèi)核將這4G字節(jié)的空間分為兩部分。將最高的 1G字節(jié)(從虛擬地址0xC0000000到0xFFFFFFFF),供內(nèi)核使用,稱為“內(nèi)核空間”。而將較低的3G字節(jié)(從虛擬地址 0x00000000到0xBFFFFFFF),供各個進程使用,稱為“用戶空間)。因為每個進程可以通過系統(tǒng)調(diào)用進入內(nèi)核,因此,Linux內(nèi)核由系統(tǒng) 內(nèi)的所有進程共享。于是,從具體進程的角度來看,每個進程可以擁有4G字節(jié)的虛擬空間。
?? ?Linux使用兩級保護機制:0級供內(nèi)核使用,3級供用戶程序使用。從圖中可以看出(這里無法表示圖),每個進程有各自的私有用戶空間(0~3G),這個空間對系統(tǒng)中的其他進程是不可見的。最高的1GB字節(jié)虛擬內(nèi)核空間則為所有進程以及內(nèi)核所共享。
1.虛擬內(nèi)核空間到物理空間的映射
? 內(nèi)核空間中存放的是內(nèi)核代碼和數(shù)據(jù),而進程的用戶空間中存放的是用戶程序的代碼和數(shù)據(jù)。不管是內(nèi)核空間還是用戶空間,它們都處于虛擬空間中。讀者會問,系 統(tǒng)啟動時,內(nèi)核的代碼和數(shù)據(jù)不是被裝入到物理內(nèi)存嗎?它們?yōu)槭裁匆蔡幱谔摂M內(nèi)存中呢?這和編譯程序有關(guān),后面我們通過具體討論就會明白這一點。
雖 然內(nèi)核空間占據(jù)了每個虛擬空間中的最高1GB字節(jié),但映射到物理內(nèi)存卻總是從最低地址(0x00000000)開始。對內(nèi)核空間來說,其地址映射是很簡單 的線性映射,0xC0000000就是物理地址與線性地址之間的位移量,在Linux代碼中就叫做PAGE_OFFSET。
我們來看一下在include/asm/i386/page.h中對內(nèi)核空間中地址映射的說明及定義:
/*
* 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))
源 代碼的注釋中說明,如果你的物理內(nèi)存大于950MB,那么在編譯內(nèi)核時就需要加CONFIG_HIGHMEM4G和CONFIG_HIGHMEM64G選 項,這種情況我們暫不考慮。如果物理內(nèi)存小于950MB,則對于內(nèi)核空間而言,給定一個虛地址x,其物理地址為“x- PAGE_OFFSET”,給定一個物理地址x,其虛地址為“x+ PAGE_OFFSET”。
這里再次說明,宏__pa()僅僅把一個內(nèi)核空間的虛地址映射到物理地址,而決不適用于用戶空間,用戶空間的地址映射要復(fù)雜得多。
2.內(nèi)核映像
? 在下面的描述中,我們把內(nèi)核的代碼和數(shù)據(jù)就叫內(nèi)核映像(kernel image)。當(dāng)系統(tǒng)啟動時,Linux內(nèi)核映像被安裝在物理地址0x00100000開始的地方,即1MB開始的區(qū)間(第1M留作它用)。然而,在正常 運行時, 整個內(nèi)核映像應(yīng)該在虛擬內(nèi)核空間中,因此,連接程序在連接內(nèi)核映像時,在所有的符號地址上加一個偏移量PAGE_OFFSET,這樣,內(nèi)核映像在內(nèi)核空間 的起始地址就為0xC0100000。
例如,進程的頁目錄PGD(屬于內(nèi)核數(shù)據(jù)結(jié)構(gòu))就處于內(nèi)核空間中。在進程切換時,要將寄存器CR3設(shè)置成指 向新進程的頁目錄PGD,而該目錄的起始地址在內(nèi)核空間中是虛地址,但CR3所需要的是物理地址,這時候就要用__pa()進行地址轉(zhuǎn)換。在 mm_context.h中就有這么一行語句:
asm volatile(“movl %0,%%cr3”: :”r” (__pa(next->pgd));
這是一行嵌入式匯編代碼,其含義是將下一個進程的頁目錄起始地址next_pgd,通過__pa()轉(zhuǎn)換成物理地址,存放在某個寄存器中,然后用mov指令將其寫入CR3寄存器中。經(jīng)過這行語句的處理,CR3就指向新進程next的頁目錄表PGD了。
?
評論
查看更多