天天看點

Linux核心虛拟位址空間

作者:Linux碼農

Linux核心位址空間劃分

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

Linux核心虛拟位址空間

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 即為高端記憶體,這就是記憶體高端記憶體概念的由來。

Linux核心虛拟位址空間

其中把 0~896M 區域為直接映射區,也即是虛拟記憶體中(3G~3G+896M)區域和實體記憶體的 0~896M 進行直接映射。由于虛拟記憶體中核心空間隻有1G,是以還剩下的 128M 虛拟記憶體區域(3G+896M~4G)。

那麼如核心是如何借助 128MB 高端記憶體位址空間是如何實作通路可以所有實體記憶體?

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

Linux核心虛拟位址空間

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

邏輯位址 實體記憶體位址
0xF8700000 0x80000000
0xF8700001 0x80000001
0xF8700002 0x80000002
0xF87FFFFF 0x800FFFFF

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

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

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

高端記憶體分布

在核心的虛拟位址空間的高端記憶體區中又分為三個區,分别是:非連續記憶體區、永久核心映射區、固定映射區。

  • 非連續記憶體區是為系統硬體中斷處理和核心子產品生産空間一次性準備用的。
  • 永久映射區是給系統底層空間分區和硬體及驅動準備的。
  • 固定映射區是為使用者配置和應用軟體運作提供可用空間準備的。
Linux核心虛拟位址空間

在圖中,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 使用和做一些硬體映射的。如下圖:

Linux核心虛拟位址空間

在裡面我們值得注意的就是 _end,它在代碼裡表明了核心鏡像在記憶體中的結束位址,頁表的初始化會先初始化未被核心使用的區域,最後再初始化核心使用的區域。

Linux核心虛拟位址空間

符号 _text 對應實體位址 0x00100000,表示核心代碼的第一個位元組的位址。核心代碼的結束位置用另一個類似的符号 _etext 表示。核心資料被分為兩組:初始化過的資料和未初始化過的資料。初始化過的資料在 _etext 後開始,在 _edata 處結束,緊接着是未初始化過的資料,其結束符号為 _end,這也是整個核心映像的結束符号。

圖中出現的符号是由編譯程式在編譯核心時産生的。你可以在System.map 檔案中找到這些符号的線性位址(或叫虛拟位址),System.map 是編譯核心以後所建立的。

啟用分頁機制

當 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 0x1000
ENTRY(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

/*
* The page tables are initialized to only 8MB here - the final page
* tables are set up later depending on memory size.
*/
.org 0x2000
ENTRY(pg0)

.org 0x3000
ENTRY(pg1)

/*
* empty_zero_page must immediately follow the page tables ! (The
* initialization loop counts until empty_zero_page)
*/

.org 0x4000
ENTRY(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, but
PRESENT+RW+USER */
2: stosl
add $0x1000,%eax
cmp $empty_zero_page-__PAGE_OFFSET,%edi
jne 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 頁(即零頁),零頁存放的是系統啟動參數和指令行參數。

Linux核心虛拟位址空間
.org 0x1000
ENTRY(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,%eax
movl %eax,%cr3 /* set the page table pointer.. */
movl %cr0,%eax
orl $0x80000000,%eax
movl %eax,%cr0 /* ..and set paging (PG) bit */
jmp 1f /* flush the prefetch-queue */
1:
movl $1f,%eax
jmp *%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 的空間,而且有着相同的映射,如圖:

Linux核心虛拟位址空間

核心開始運作後運作在核心空間,那麼,為什麼把使用者空間的低區(8M)也

進行映射,而且與核心空間低區的映射相同?

簡而言之,是為了從實模式到保護模式的平穩過渡。具體地說,當 CPU 進入核心代碼的起點 startup_32 後,是以實體位址來取指令的。在這種情況下,如果頁目錄隻映射核心空間,而不映射使用者空間的低區,則一旦開啟頁映射機制以後就不能繼續執行了,這是因為,此時 CPU 中的指令寄存器 EIP 仍指向低區,仍會以實體位址取指令,直到以某個符号位址為目标作絕對轉移或調用子程式為止。是以,Linux 核心就采取了上述的解決辦法。

比如不映射使用者空間的低區,核心代碼的起點 startup_32 後,是以實體位址來取指令的,比如 eip 裡面的位址為 0x0010010,當開啟頁面映射後,eip 裡面的位址就要按照虛拟位址來處理了,這個時候要通過查頁表進行把虛拟位址 0x0010010 轉換為實體位址,這個時候沒有映射使用者空間的低區,找不到虛拟位址 0x0010010 到實體位址的映射,這個時候就會出現問題。

在 CPU 轉入核心空間以後,應該把使用者空間低區的映射清除掉。後面将會看到,頁目錄 swapper_pg_dir 經擴充後就成為所有核心線程的頁目錄。在核心線程的正常運作中,處于核心态的 CPU 是不應該通過使用者空間的虛拟位址通路記憶體的。清除了低區的映射以後,如果發生 CPU 在核心中通過使用者空間的虛拟位址通路記憶體,就可以因為産生頁面異常而捕獲這個錯誤。

經過這個階段的初始化,初始化階段頁目錄及幾個頁表在實體空間中的位置如圖所示。

Linux核心虛拟位址空間

/*

* 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 空間。如下圖:

Linux核心虛拟位址空間

比如當通路虛拟核心位址空間 0xC0001002,通過 swapper_pg_dir 進行虛拟位址到實體位址轉換時,發現 0xC0001002 處于 swapper_pg_dir [768],而 swapper_pg_dir [768] 指向 pg0 的實體記憶體位址,然後經過 pg0 找到其對應的實體頁框。

關于整個虛拟位址空間和實體空間分布關系如下:

Linux核心虛拟位址空間

繼續閱讀