從零開始寫 OS 內核 - 虛擬內存初探

系列目錄

kernel 虛擬內存概覽

接上一篇 GDT 與保護模式,這一篇將是 loader 的重點。首先咱們須要創建 kernel 空間的虛擬內存。若是你對虛擬內存的原理還不熟悉,請務必先自學,這裏能夠提供一個文檔供參考。html

到目前爲止咱們始終在物理內存上操做,確切地說是在 1MB 的低地址空間內操做,這一切都很簡單直接。可是接下來 loader 即將爲加載 kernel 作準備,咱們須要在更廣闊的 4GB 虛擬內存空間上規劃數據和代碼。java

仿照 Linux 系統,咱們將使用 3GB 以上的高地址空間做爲內核空間來開展後續全部工做。例如最基本的,目前的物理低地址 1MB 會被映射到 virtual 地址 0 ~ 1MB 以及 3GB 以上空間 0xC0000000 ~ (0xC0000000 + 1MB) 處:git

進入 kernel 之後,對低 1MB 空間的訪問將會使用 0xC0000000 ~ (0xC0000000 + 1MB) 虛擬地址,這裏主要包括當前使用的 stack,以及顯示器對應的內存映射:web

因此 video 內存基地址將從 virtual 地址 0xC00B8000 開始,不過目前沒必要深究,後續將會在顯示與打印一篇中詳解。shell


除了最基本的低 1MB 內存空間,loader 還須要進一步在 0xC0000000 以上的 virtual 空間中開疆拓土,這主要包括兩部分:segmentfault

  • kernel 所使用的頁目錄(page directory)和頁表(page table);
  • kernel 二進制鏡像的讀取,以及代碼、數據的加載;

下面給出整個 loader 階段將要搭建的 virtual-to-physical 內存映射關係圖:多線程

這張圖是本篇最重要的全局圖,其中第二行是第一行通過「扭曲」比例的圖示,咱們將 3GB 如下的用戶空間縮小顯示,當前重點只關注 3GB 以上的內核空間(粗框部分)。因爲是 virtual 地址空間,咱們的空間劃分能夠比較隨意和「奢侈」,咱們以 4MB 爲單位,從 0xC0000000 開始在 virtual 空間切割劃分出如下幾個區域:框架

  • 第一個 4MB 保留,其中低 1MB 空間映射到了 physical 地址的低 1MB,這是上面已經解釋過的;
  • 第二個 4MB(橙色)用來映射 kernel 的全部 page tables
  • 第三個 4MB(綠色),即從 0xC0800000開始,做爲加載、存放 kernel 代碼和數據的空間,也就是說 kernel 從該處開始編址;

這裏要說一句,實現一個 OS 並無固定的方式,以上只是我我的的實現方式。實際上對於內存的規劃是很靈活的,就像這個項目的名字 scroll 同樣,內存就是一幅畫卷,CPU 則是畫筆,在遵循必定規則的前提下,能夠作自由發揮。ide

下面咱們首先開始橙色部分,即內核 page directorypage tables 的創建。函數

創建 kernel 虛擬內存

在開始這一段以前,咱們仍是回顧一下頁目錄(page directory)和頁表(page table)的相關原理。

有一些關鍵數字須要記住:

  • 頁(page)的大小爲 4096;
  • 頁目錄項 pde (page directory entry) 和頁表項 pte (page table entry),本質上是同樣的結構,大小爲 4 bytes
  • page direcotry 一共有 1024 項,指向總共 1024 張 page table,一共 4MB
  • 每一個 page table 都有 1024 項,指向 1024 張 pages,管理着 1024 * 4KB = 4MB 的 virtual 空間;
  • 因此每一個 pde 管理着 4MB 的 virtual 空間;

好了,下面咱們開始創建 kernel 空間的頁表。按照慣例給出代碼連接:這一部分相關的代碼從函數 setup_page 開始,供你參考。

從這裏開始如下,按照術語慣例,virtual 頁我將用 page 表述,而 physical 頁將用 frame 來表述。

創建 page directory

首先咱們須要拿出一個 frame,用來做爲 page directory。回到 physical 內存分佈的那張圖,目前 1MB 如下的部分已被佔用,咱們可使用的部分就從 1MB 即 0x100000 開始。

我選擇的是 0x100000 + 4KB,即 0x100000 後的第 2 個 frame 做爲 page directory,固然這徹底是我的選擇;0x100000 後的第 1 個 frame 我選擇將它做爲第一個 page table

再次強調,這是個人我的選擇;frame 的選擇是很是自由的,只要是還沒被佔用的均可以使用,固然了你要記住本身用過了哪些 frames,合理緊湊而且儘可能「美觀」地規劃使用。

映射 1MB 低內存空間

值得注意的是,第 0 和第 768 個 pde 都指向了同一個 page table,這個 page table 咱們將用它映射 0 ~ 1MB 低內存,即咱們目前所處的 1MB 內存空間。固然這個 page table 能夠管理 4MB 的空間,咱們只映射了其中的 1MB,剩餘 3MB 的 virtual 空間就閒置了,不過這沒有關係,閒置就閒置,反正這是 virtual 空間。

下圖展現了低 1MB 內存在頁表中被映射的方式:

pde[0] 管理的是 virtual 空間最低的 4MB,其中的起始 1MB,被映射到了 physical 的低 1MB 上,這是一一對應的映射,virtual 地址徹底等於 physical 地址,這樣在打開 paging 以後,咱們對 1MB 低內存的訪問變爲使用 virtual 地址,和以前的 physical 地址訪問同樣,不會感知到任何變化。

pde[768] 管理的是 0xC0000000 即 3GB 開始的第一個 4MB 空間,回到本篇開始的第一張圖,其起始的 1MB 也被映射到低 1MB 內存上。在打開 paging 並進入 kernel 後,咱們將使用 0xC0000000 ~0xC0000000 + 1MB 的空間訪問低 1MB 內存:

映射 page directory 以及 page tables 自己

這裏是本節的重點和難點。咱們知道 page directorypage tables 所指向的都是 physical 頁,而一旦打開了 paging 模式,咱們之後全部對內存的訪問將所有經過 virtual 地址,沒法再直接操做 physical 地址。那麼問題來了,咱們如何訪問並修改 page directorypage tables 自己呢?

一種方法固然是在須要時關閉 paging,直接訪問 physical 地址,以前推薦的教程 JamesM's kernel development tutorials 在不少地方都是這麼作的,不過這並非一種好的作法,緣由有如下幾點:

  • 進入複雜的 kernel 之後,代碼的執行會大量涉及到 stack 和 heap,以及其餘全局變量等內存訪問,這些所有都是 kernel 空間的 virtual 地址,若是此時忽然關閉 paging,對它們的訪問將沒法進行。你必須很是當心地安排你的代碼對內存的訪問,不然將會出現不可預知的後果,可是這其實很是難作到;
  • 一旦開啓多線程,若是在關閉 paging 的狀況下發生了中斷,CPU 將進行一些自動的 stack 操做以及中斷處理,所有都是對 virtual 地址的操做,顯然其結果也是災難性的;

一個更合理的作法是,咱們將 page directorypage tables 自己也映射到 virtual 空間,這樣就能夠像訪問其餘正常內存同樣訪問它們。從本質上說 page directorypage tables 無非也是一些 page,徹底能夠和其它內存訪問一視同仁。問題就是,應該如何創建這種映射?來看下圖:

咱們將 pde[769] 指向了 page directory 這個 frame 自己。這樣 page direcotry 實際上同時也充當了一個 page table,它所管理的正好是 1024 張 page tables 自己,一共 4MB。這 1024 張 page tables,其中有一張就是 page direcotry 它本身。

是否是有點繞?換言之,因爲 pde[769] 指向了 page directory 它本身,所以 0xC0400000 ~ 0xC0800000 這 4MB 的 virtual 空間,如今被映射到了 1024 張 page tables 上,並且更好的是,它們的 virtual 地址是徹底連續地,緊密地排布在這 4MB 空間裏。

由此,上面的問題已經解決,page tables 對應的 virtual 地址空間爲:

0xC0400000 ~ 0xC0800000

這是 4GB 空間中第 769 個 4MB 空間 (總共 1024 個 4MB 空間,組成 4GB)。

而且咱們同時還獲得了 page directory 它本身的 virtual 地址爲:

0xC0701000

0xC0400000 ~ 0xC0800000 這 4MB 空間中的第 769 個 page,是否是很巧妙:)


這裏的核心思想是,page directory 其實本質上是一個特殊的 page table,它和其它 page table 同樣,都管理着 4MB 的空間。

若是感受仍是有點繞的話,你不妨反過來驗證一下,從上面給出的 virtual 地址開始,推導實際指向的 physical 地址是哪裏,我想很快就能理清這裏面的邏輯。

若是你進一步思考的話,就會發現這並非惟一的實現方式。你徹底能夠不選擇 pde[769],而使用其它 virtual 空間來映射 page tables,例如用 pde[770] 也能夠,這樣全部 page tables 對應的 virtual 空間就變成了 0xC0800000 ~ 0xC0C00000。用 pde[769] 只是我我的的選擇,由於它是 0xC0000000 後的第二個 4MB 空間,這樣的安排,virtual 空間的使用能比較緊湊整齊一點。

映射 kernel 空間的其它區域

到目前爲止,pde 768 和 769 已經被使用,即 0xC0000000 ~ 0xC04000000xC0400000 ~ 0xC0800000 這兩塊 4MB 空間已被徵用。剩下的 pde[770] ~ pde[1023] 對應的 254 個 page tables,咱們依次爲它們安排上 frames。這樣咱們最終徵用了 256 個 pages & frames,總共 1MB 的內存(virtual & physical),來創建 kernel 空間(3GB ~ 4GB)的 page tables,管理這 1GB 的空間。

咱們將本章開始的那個 virtual-to-physical 內存映射關係圖中的橙色部分抽出放大,展現 kernel 的 256 張 page tables 的內存分佈:

注意到咱們只分配了 kernel 空間即 3GB 以上的 page tables,共 256 張,佔地 1MB,它們映射的也是 0xC0400000 ~ 0xC0800000 空間的後 1/4 部分即 0xC0700000 ~ 0xC0800000;而 3GB 如下的用戶空間此時並無分配 page tables,由於目前咱們並無使用到。

這 256 張 kernel 頁表(其中有一張是 page directory 自己),是咱們編寫 kernel 期間最核心的 page tables,而且在 page directory 裏創建了 pde[768] ~ pde[1023] 這所有的 256 個表項,指向了這些 page tables。

其實除了前兩個 page table,後面 254 個目前都是空的,沒有被用到,咱們只是爲它們安排好了 frame 而已。這裏用去了足足 1MB 的 physical 內存,這看上有點奢侈了,畢竟這個項目配置裏 physical 內存總共只有 32 MB(見 bochsrc.txt,固然如今的計算機內存遠不止 32 MB,這已經不是個問題)。這樣作有一個很是重要的緣由,那就是這 256 張 kernel page tables 後面將被全部的進程(process)共享,也就是說對於用戶 process 而言,3GB 如下的空間是隔離的,而 3GB 以上的 kernel 的空間是共享的,這也是理所固然的,不然就有多個 kernel 在內存中獨立運行了。

每次 fork 出一個新的 process,它的 page directory 的後 1/4 即 768 ~ 1023 項將會直接複製 kernel 的 page directory 的 768 ~ 1023 項,共同指向這 256 張 kernel page tables。因此咱們要求這 256 張 page tables 對應的 frames 從一開始就固定下來,後面也再也不變化,這樣才能實現全部 process 共享的效果。

打開 paging

page tables 都準備就緒之後,就能夠打開 paging 了:

enable_page:
  sgdt [gdt_ptr]

  ; move the video segment to > 0xC0000000
  mov ebx, [gdt_ptr + 2]
  or dword [ebx + 0x18 + 4], 0xC0000000

  ; move gdt to > 0xC0000000
  add dword [gdt_ptr + 2], 0xC0000000

  ; move stack to > 0xC0000000
  mov eax, [esp]
  add esp, 0xc0000000
  mov [esp], eax

  ; set page directory address to cr3 register 
  mov eax, PAGE_DIR_PHYSICAL_ADDR
  mov cr3, eax
  
  ; enable paging on cr0 register
  mov eax, cr0
  or eax, 0x80000000
  mov cr0, eax

這裏最重要的就是設置 CR3 寄存器,使之指向 page directory 的 frame (注意是 physical 地址),而後打開 CR0 寄存器上的 paging 比特位開關。

總結

至此,loader 階段關於 kernel 虛擬內存初始化的部分就結束了。這一段的代碼並不長,核心僅僅是 setup_page 這一個函數,可是其背後的原理倒是很是深入複雜。在 loader 階段初步創建起 virtual memory 的框架,這對後面進入 kernel 以後的內存管理打下了良好的基礎。

在當前階段咱們全部的 virtual-to-physical 的內存分配和映射都是提早規劃,預先分配再使用的,每一塊 physical frame 都是手動安排。這其實並無徹底發揮出 virtual memory 的做用。在後面進入 kernel 以後,咱們將進一步完善 virtual memory 相關的工做,這將包括缺頁異常 (page fault)的處理,進程 page directory 的複製等。

virtual memory 的處理是貫穿 kernel 實現和運行的底層核心工做,必須保證絕對的正確和穩定。一旦出錯,系統會馬上出現各類難以預知的奇怪錯誤甚至崩潰,而且 debug 很是困難。

下一篇咱們將會加載真正的 kernel 到內存而且轉到 kernel 開始執行代碼,這將是進入 kernel 前的最後一道關卡。

相關文章
相關標籤/搜索