Linux內核地址空間劃分
通常 32 位 Linux 內核地址空間劃分 0~3G 為用戶空間,3~4G 為內核空間 。64 位內核地址空間劃分是不同的 。

文章插圖
Linux內核高端內存
當內核模塊代碼或線程訪問內存時,代碼中的內存地址都為邏輯地址,而對應到真正的物理內存地址,需要地址一對一的映射,如邏輯地址 0xc0000003 對應的物理地址為 0x3,0xc0000004 對應的物理地址為 0x4 , … …,邏輯地址與物理地址對應的關系為
物理地址 = 邏輯地址 – 0xC0000000
物理內存地址
0xc0000000
0x0
0xc0000001
0x1
0xc0000002
0x2
0xc0000003
0x3
…
…
0xe0000000
0x20000000
…
…
0xffffffff
0x40000000
假設按照上述簡單的地址映射關系,那么內核邏輯地址空間訪問為0xc0000000 ~ 0xffffffff,那么對應的物理內存范圍就為0x0 ~ 0x40000000,即只能訪問 1G 物理內存 。若機器中安裝 8G 物理內存,那么內核就只能訪問前 1G 物理內存,后面 7G 物理內存將會無法訪問 , 因為內核的地址空間已經全部映射到物理內存地址范圍 0x0 ~ 0x40000000 。即使安裝了 8G 物理內存,那么物理地址為 0x40000001 的內存,內核該怎么去訪問呢?代碼中必須要有內存邏輯地址的,0xc0000000 ~ 0xffffffff 的地址空間已經被用完了 , 所以無法訪問物理地址 0x40000000 以后的內存 。
顯然不能將內核地址空間 0xc0000000 ~ 0xfffffff 全部用來簡單的地址映射 。
因此,Linux 又把物理頁面劃分為3 個區:
- 專供 DMA 使用的 ZONE_DMA 區(小于 16MB);
- 常規的 ZONE_NORMAL 區(大于 16MB 小于 896MB);
- 內核不能直接映射的區 ZONE_HIGME 區(大于 896MB) 。
以上每個區都用 struct zone_struct 結構來表示 。
ZONE_HIGHMEM 即為高端內存 , 這就是內存高端內存概念的由來 。

文章插圖
其中把 0~896M 區域為直接映射區,也即是虛擬內存中(3G~3G+896M)區域和物理內存的 0~896M 進行直接映射 。由于虛擬內存中內核空間只有1G,因此還剩下的 128M 虛擬內存區域(3G+896M~4G) 。
那么如內核是如何借助 128MB 高端內存地址空間是如何實現訪問可以所有物理內存?
當內核想訪問高于 896MB 物理地址內存時,從 0xF8000000 ~ 0xFFFFFFFF 地址空間范圍內找一段相應大小空閑的邏輯地址空間,借用一會 。借用這段邏輯地址空間,建立映射到想訪問的那段物理內存(即填充內核 PTE 頁面表),臨時用一會,用完后歸還 。這樣別人也可以借用這段地址空間訪問其他物理內存 , 實現了使用有限的地址空間 , 訪問所有所有物理內存 。如下圖 。

文章插圖
例如內核想訪問 2G 開始的一段大小為1MB的物理內存,即物理地址范圍為0x80000000 ~ 0x800FFFFF 。訪問之前先找到一段 1MB 大小的空閑地址空間 , 假設找到的空閑地址空間為 0xF8700000 ~ 0xF87FFFFF,用這 1MB 的邏輯地址空間映射到物理地址空間 0x80000000 ~ 0x800FFFFF 的內存 。映射關系如下:
物理內存地址
0xF8700000
0x80000000
0xF8700001
0x80000001
0xF8700002
0x80000002
…
…
0xF87FFFFF
0x800FFFFF
當內核訪問完 0x80000000 ~ 0x800FFFFF 物理內存后 , 就將 0xF8700000 ~ 0xF87FFFFF內核線性空間釋放 。這樣其他進程或代碼也可以使用 0xF8700000 ~ 0xF87FFFFF 這段地址訪問其他物理內存 。
從上面的描述,我們可以知道高端內存的最基本思想:借一段地址空間,建立臨時地址映射,用完后釋放 , 達到這段地址空間可以循環使用,訪問所有物理內存 。
看到這里,不禁有人會問:萬一有內核進程或模塊一直占用某段邏輯地址空間不釋放,怎么辦?若真的出現的這種情況,則內核的高端內存地址空間越來越緊張,若都被占用不釋放,則沒有建立映射到物理內存都無法訪問了 。
高端內存分布
在內核的虛擬地址空間的高端內存區中又分為三個區,分別是:非連續內存區、永久內核映射區、固定映射區 。
- 非連續內存區是為系統硬件中斷處理和內核模塊生產空間一次性準備用的 。
- 永久映射區是給系統底層空間分區和硬件及驅動準備的 。
- 固定映射區是為用戶配置和應用軟件運行提供可用空間準備的 。

文章插圖
在圖中 , high_memory是高端內存區( ZONE_HIGHMEM )起始地址 , VMALLOC 是非連續內存區 。
在直接映射的物理頁幀末尾與第一個內存區 VMALLOC_START 之間插入了一個 8MB(VMALLOC_OFFSET)的區間,這是一個安全區,目的是為了“捕獲”對非連續區的非法訪問 。出于同樣的理由,在其他非連續的內存區之間也插入了 4KB 大小的安全區 。每個非連續內存區的大小都是 4096 的倍數 。
在內核中,永久內核映射區和固定映射區大小一般都為 4MB,也就是分別用一個頁表可以囊括其所包含地址范圍,其他都給非連續內存區使用 。不過如果物理內存大小小于 896MB 的情況下,內核并不會生成高端內存區,只會有 ZONE_DMA 和 ZONE_NORMAL 兩個區 。
我們知道 , 內核可使用的線性地址就只有1G大小( 0xC0000000 ~ 0xFFFFFFFF ),而用于 ZONE_DMA 和 ZONE_NORMAL 這兩個區的映射已經花掉了 896MB 的線性地址空間,最后只剩下 128MB 用于映射高端內存,如果內存大于 1G,比如 2G(2048M)的情況下,高端內存區大小就為 1152MB , 這個 128MB 大小的線性地址空間是完全不夠直接映射高端內存的,所以對于高端內存的處理,linux 并不會直接映射,而是在需要的時候才進行映射,不需要的時候就釋放映射,回收線性地址 。
在初始化頁表時,會對永久內核映射區和固定映射區分別進行初始化,但是都不會對他們進行映射處理,只有在需要使用時才會分配 。
以上是虛擬內存中高端內存(3G+896M~4G)的分布情況,那么 ZONE_DMA 和 ZONE_NORMAL (3G~3G+896M)區域內存布局是什么樣的呢?
內核啟動后內核區域內存布局
一般的,內核啟動會被加載到內存的 1MB 開始處,而普通配置的內核大小一般小于3MB,也就是說 , 內核鏡像被加載內存 1MB~4MB 的地方,而為什么0MB~1MB 的內存內核不使用,因為這段內存一般是由 BIOS 使用和做一些硬件映射的 。如下圖:

文章插圖
在里面我們值得注意的就是 _end,它在代碼里表明了內核鏡像在內存中的結束地址,頁表的初始化會先初始化未被內核使用的區域 , 最后再初始化內核使用的區域 。

文章插圖
符號 _text 對應物理地址 0x00100000,表示內核代碼的第一個字節的地址 。內核代碼的結束位置用另一個類似的符號 _etext 表示 。內核數據被分為兩組:初始化過的數據和未初始化過的數據 。初始化過的數據在 _etext 后開始,在 _edata 處結束,緊接著是未初始化過的數據,其結束符號為 _end,這也是整個內核映像的結束符號 。
圖中出現的符號是由編譯程序在編譯內核時產生的 。你可以在System.map 文件中找到這些符號的線性地址(或叫虛擬地址),System.map 是編譯內核以后所創建的 。
【linux內核虛擬地址空間】
啟用分頁機制
當 Linux 啟動時,首先運行在實模式下,隨后就要轉到保護模式下運行 。
將 Linux 內核的映像轉入內存中,并且做好了一些必要的準備后,CPU 就通過一條轉移指令轉到映象代碼段開頭的入口 startup_32, 從那里開始執行 。
Linux 內核代碼的入口點就是 /arch/i386/kernel/head.S 中的 startup_32 。(內核版本 2.4.16) 。
內核映象的起點時 stext,也就是 _stext, 引導和解壓縮以后的整個映象存放在內存中從 0x100000 也即是 1M 開始的區間 。CPU 執行內核映象的入口startup_32 就在內核映象開頭的地方,因此其物理地址也是 0x100000 。
在正常運行時整個內核映象都應該在系統空間中,系統空間的地址映射時線性的、連續的 , 虛擬地址與物理地址間有個固定的轉移,這就是 0xC0000000,也即是 3GB 。所以 , 在連續內核映象時已經在所有的符號地址上加了一個偏移量 0xC0000000,這樣 startup_32 虛擬地址就成了 0xC0100000 。
在進入 startup_32 時都運行于保護模式下的段式尋址方式 。段描述表中與__KERNEL_CS 和 __KERNEL_DS 相對應的描述項所提供的基地址都是0,所以實際產生的就是線性地址 。
其中代碼段寄存器 CS 已在進入 startup_32 之前設置成 __KERNEL_CS,數據段寄存器則尚未設置成 __KERNEL_DS 。不過,雖然代碼段寄存器已經設置成 __KERNEL_CS,從而 startup_32 的地址為 0xC0100000 。但是在轉入這個入口時使用的指令時 “ljmp 0x”100000” 而不是 “ljmp startup_32”,所以裝入CPU中寄存器IP的地址是物理地址 0x100000 而不是虛擬地址0xC0100000 。
這樣 CPU 在進入 startup_32 以后就會繼續以物理地址取指令 。只要不在代碼段中引用某個地址,例如向某個地址作絕對轉移或者調用某個子程序,就可以一直這樣運行下去,而與 CS 內容無關 。另外,CPU 的中斷已在進入 startup_32 之前關閉 。
/* page table for 0-4MB for everybody */extern unsigned long pg0[1024];pte_t pg1[1024];pgd_t swapper_pg_dir[1024];在系統初始化的時候 , 內核就要創建內核頁表 swapper_pg_dir 了 。struct mm_struct init_mm = INIT_MM(init_mm);#define INIT_MM(name) \{ \.mm_rb = RB_ROOT, \.pgd = swapper_pg_dir, \.mm_users = ATOMIC_INIT(2), \.mm_count = ATOMIC_INIT(1), \.mmap_sem = __RWSEM_INITIALIZER(name.mmap_sem), \.page_table_lock = __SPIN_LOCK_UNLOCKED(name.page_table_lock), \.mmlist = LIST_HEAD_INIT(name.mmlist), \.cpu_vm_mask = CPU_MASK_ALL, \}內核啟動過程中,存在一個實模式保護模式的切換過程 。在 linux 啟動的最初階段,內核剛剛被裝入內存時,分頁功能還未啟用 , 此時是直接存取物理地址的(或者說線性地址就等于物理地址) 。但初始化完成后,內核也需要有自己的虛擬地址空間(1個G大?。? ,該虛擬地址空間的地址映射關系,會被作為模版拷貝到其他進程的內核地址空間中 。臨時內核頁表只用來映射物理地址的前 8M 空間內容 。目的是允許 CPU 在實模式(直接存取物理地址)和保護模式(根據虛擬地址映射)之間切換的過程中 , 都能對這前 8M 的地址進行訪問 。(假如內核使用的全部內存可以存放在 8M 的空間里,因為一個頁表可以映射 4M 的地址,所以8M的空間需要兩個頁表 , 也就是需要兩個頁目錄項 。這兩張頁表我們稱為臨時內核頁表 pg0 和 pg1 。
從 startup_32 開始的匯編代碼在 /arch/i386/kernel/head.S,這就是初始化的第一階段 。
.org 0x1000ENTRY(s*** 0x2000ENTRY(pg0).org 0x3000ENTRY(pg1)/** empty_zero_page must immediately follow the page tables ! (The* initialization loop counts until empty_zero_page)*/.org 0x4000ENTRY(empty_zero_page)/** Initialize page tables*/movl $pg0-__PAGE_OFFSET,%edi /* initialize page tables */movl $007,%eax /* "007" doesn't mean with right to kill, butPRESENT+RW+USER */2: stosladd $0x1000,%eaxcmp $empty_zero_page-__PAGE_OFFSET,%edijne 2b內核的這段代碼執行時,因為頁機制還沒有啟用,還沒有進入保護模式 , 因此指令寄存器 EIP 中的地址還是物理地址,但因為 pg0 中存放的是虛擬地址(gcc 編譯內核以后形成的符號地址都是虛擬地址),因此 , “$pg0-__PAGE_OFFSET ”獲得 pg0 的物理地址(__PAGE_OFFSET 為 0xC0000000,也即是 3GB),可見 pg0 存放在相對于內核代碼起點為0x2000 的地方,即物理地址為 0x00102000,而pg1 的物理地址則為0x00103000 。Pg0 和 pg1 這個兩個頁表中的表項則依次被設置為 0x007、0x1007、0x2007 等 。其中最低的 3 位均為 1,表示這兩個頁為用戶頁,可寫,且頁的內容在內存中(參見下圖) 。所映射的物理頁的基地址則為 0x0、0x1000、0x2000 等 , 也就是物理內存中的頁面 0、1、2、3 等等,共映射2K 個頁面,即 8MB 的存儲空間 。由此可以看出,Linux 內核對物理內存的最低要求為 8MB 。緊接著存放的是 empty_zero_page 頁(即零頁),零頁存放的是系統啟動參數和命令行參數 。
文章插圖
.org 0x1000ENTRY(swapper_pg_dir).long 0x00102007.long 0x00103007.fill BOOT_USER_PGD_PTRS-2,4,0/* default: 766 entries */.long 0x00102007.long 0x00103007/* default: 254 entries */.fill BOOT_KERNEL_PGD_PTRS-2,4,0/** Enable paging*/3:movl $swapper_pg_dir-__PAGE_OFFSET,%eaxmovl %eax,%cr3 /* set the page table pointer.. */movl %cr0,%eaxorl $0x80000000,%eaxmovl %eax,%cr0 /* ..and set paging (PG) bit */jmp 1f /* flush the prefetch-queue */1:movl $1f,%eaxjmp *%eax /* make sure eip is relocated */1:/* Set up the stack pointer */lss stack_start,%esp // 將CPU的堆棧設置在 stack-start處這段代碼就是把頁目錄 swapper_pg_dir 的物理地址裝入控制寄存器cr3,并把 cr0 中的最高位置成1,這就開啟了分頁機制 。但是 , 啟用了分頁機制,并不說明Linux 內核真正進入了保護模式,因為此時 , 指令寄存器 EIP 中的地址還是物理地址,而不是虛地址 。“jmp 1f” 指令從邏輯上說不起什么作用,但是 , 從功能上說它起到丟棄指令流水線中內容的作用(這是 Intel 在 i386 技術資料中所建議的),因為這是一個短跳轉,EIP 中還是物理地址 。緊接著的 mov 和 jmp 指令把第 2 個標號為 1 的地址裝入EAX 寄存器并跳轉到那兒 。在這兩條指令執行的過程中, EIP 還是指向物理地址“1MB+某處” 。因為編譯程序使所有的符號地址都在虛擬內存空間中 , 因此,第2 個標號1 的地址就在虛擬內存空間的某處(PAGE_OFFSET+某處),于是 , jmp 指令執行以后,EIP 就指向虛擬內核空間的某個地址 , 這就使 CPU 轉入了內核空間,從而完成了從實模式到保護模式的平穩過渡 。
然后再看頁目錄 swapper_pg_dir 中的內容 。從前面的討論我們知道 pg0 和pg1 這兩個頁表的起始物理地址分別為 0x00102000 和 0x00103000 。頁目錄項的最低12位用來描述頁表的屬性 。因此 , 在 swapper_pg_dir 中的第0 和第1 個目錄項 0x00102007、0x00103007 , 就表示 pg0 和 pg1 這兩個頁表是用戶頁表、可寫且頁表的內容在內存 。
接著,把 swapper_pg_dir 中的第 2~767 共 766 個目錄項全部置為0 。因為一個頁表的大小為 4KB,每個表項占 4 個字節,即每個頁表含有 1024 個表項,每個頁的大小也為 4KB,因此這 768 個目錄項所映射的虛擬空間為768×1024×4K=3G,也就是 swapper_pg_dir 表中的前 768 個目錄項映射的是用戶空間 。最后 , 在第 768 和 769 個目錄項中又存放 pg0 和 pg1 這兩個頁表的地址和屬性,而把第 770~1023 共 254 個目錄項置 0 。這 256 個目錄項所映射的虛擬地址空間為256×1024×4K=1G,也就是 swapper_pg_dir 表中的后 256 個目錄項映射的是內核空間 。
由此可以看出,在初始的頁目錄 swapper_pg_dir 中,用戶空間和內核空間都只映射了開頭的兩個目錄項,即 8MB 的空間,而且有著相同的映射,如圖:

文章插圖
內核開始運行后運行在內核空間,那么 , 為什么把用戶空間的低區(8M)也
進行映射,而且與內核空間低區的映射相同?
簡而言之,是為了從實模式到保護模式的平穩過渡 。具體地說 , 當 CPU 進入內核代碼的起點 startup_32 后 , 是以物理地址來取指令的 。在這種情況下,如果頁目錄只映射內核空間,而不映射用戶空間的低區,則一旦開啟頁映射機制以后就不能繼續執行了,這是因為,此時 CPU 中的指令寄存器 EIP 仍指向低區,仍會以物理地址取指令,直到以某個符號地址為目標作絕對轉移或調用子程序為止 。所以,Linux 內核就采取了上述的解決辦法 。
比如不映射用戶空間的低區,內核代碼的起點 startup_32 后,是以物理地址來取指令的,比如 eip 里面的地址為 0x0010010,當開啟頁面映射后,eip 里面的地址就要按照虛擬地址來處理了,這個時候要通過查頁表進行把虛擬地址 0x0010010 轉換為物理地址,這個時候沒有映射用戶空間的低區,找不到虛擬地址 0x0010010 到物理地址的映射,這個時候就會出現問題 。
在 CPU 轉入內核空間以后,應該把用戶空間低區的映射清除掉 。后面將會看到,頁目錄 swapper_pg_dir 經擴充后就成為所有內核線程的頁目錄 。在內核線程的正常運行中,處于內核態的 CPU 是不應該通過用戶空間的虛擬地址訪問內存的 。清除了低區的映射以后,如果發生 CPU 在內核中通過用戶空間的虛擬地址訪問內存 , 就可以因為產生頁面異常而捕獲這個錯誤 。
經過這個階段的初始化,初始化階段頁目錄及幾個頁表在物理空間中的位置如圖所示 。

文章插圖
/*
* ZERO_PAGE is a global shared page that is always zero: used
* for zero-mapped memory areas etc..
*/
extern unsigned long empty_zero_page[1024];
其中 empty_zero_page 中存放的是在操作系統的引導過程中所收集的一些數據,叫做引導參數 。因為這個頁面開始的內容全為 0,所以叫做“零頁”,代碼中常常通過宏定義 ZERO_PAGE 來引用這個頁面 。不過,這個頁面要到初始化完成,系統轉入正常運行時才會用到 。
那 swapper_pg_dir 和 pg0 、pg1 怎么對物理內存進行映射的呢?
從上面的物理內存分布可知,swapper_pg_dir 、pg0 、pg1存在物理內存中,swapper_pg_dir [0] 和 swapper_pg_dir [768] 指向 pg0 所在的物理地址,swapper_pg_dir [1] 和 swapper_pg_dir [769]指向pg1所在的物理地址 。而他們每一項對應的映射為 4M 。pg0 和 pg1 二者映射物理內存的前 8M 空間 。如下圖:

文章插圖
比如當訪問虛擬內核地址空間 0xC0001002 , 通過 swapper_pg_dir 進行虛擬地址到物理地址轉換時,發現 0xC0001002 處于 swapper_pg_dir [768],而 swapper_pg_dir [768] 指向 pg0 的物理內存地址,然后經過 pg0 找到其對應的物理頁框 。
關于整個虛擬地址空間和物理空間分布關系如下:

文章插圖
- 餓了么干飯神卡有什么用,餓了么干飯神卡是虛擬的
- 桃核發霉的桃子還能吃嗎,怎樣避免挑到內核發霉的桃子
- 釘釘打卡虛擬位置會被發現,虛擬手機定位簽到會被發現嗎
- 小米手機怎么調出虛擬導航按鍵,小米手機怎么設置屏幕下方虛擬鍵
- 什么是ai虛擬機器人,ai是科技領域的什么身份
- 亞風速遞是虛擬快遞嗎,亞風速遞和哪個快遞是一回事
- 虛擬機是什么一鍵重裝系統
- 如何讓企業微信定位更準確,企業微信如何設置虛擬定位手機
- 華碩主板如何開啟VT功能,華碩主板開啟vt虛擬化 amd
- 虛擬貨幣如何開戶
