從零開始寫 OS 內核 - 虛擬內存完善

系列目錄

開闢虛擬空間

虛擬內存初探一篇中已經在 loader 階段初步爲 kernel 創建了虛擬內存的框架,包括 page directory,page table 等。在那篇裏,咱們在 0xC0000000 以上的 kernel 空間已經開闢了它前三個 4MB,而且手工指定了它們的功能:java

  • 0xC0000000 ~ 0xC0400000:映射初始低 1MB 內存;
  • 0xC0400000 ~ 0xC0800000:頁表;
  • 0xC0800000 ~ 0xC0C00000:kernel 加載;

在這一階段,全部的內存都是咱們手動規劃好的,virtual-to-physical 的映射也是手動分配的,這固然不是長久之計。後續的 virutal 內存將會以一種更靈活的方式動態分配,所映射的 physical 內存也再也不是提早分配,而是按需取用,這就須要進行缺頁異常(page fault)的處理。git

缺頁異常

page fault 的概念這裏不作贅述,咱們在上一篇中斷處理的最後嘗試了觸發一個 page fault,可是它的處理函數只是一個 demo,沒有作真正解決 page fault 的問題,如今咱們就來解決它。shell

page fault 處理的核心問題有兩個:c#

  • 肯定發生 page fault 的 virtual 地址,以及異常的類型;
  • 分配物理 frame,並創建映射;

page fault 詳情

第一個問題比較簡單,咱們直接看代碼:segmentfault

void page_fault_handler(isr_params_t params) {
  // The faulting address is stored in the CR2 register
  uint32 faulting_address;
  asm volatile("mov %%cr2, %0" : "=r" (faulting_address));

  // The error code gives us details of what happened.
  // page not present?
  int present = params.err_code & 0x1;
  // write operation?
  int rw = params.err_code & 0x2;
  // processor was in user-mode?
  int user_mode = params.err_code & 0x4;
  // overwritten CPU-reserved bits of page entry?
  int reserved = params.err_code & 0x8;
  // caused by an instruction fetch?
  int id = params.err_code & 0x10;
  
  // ...
}
  • 發生 page fault 的地址,儲存在了 cr2 寄存器中;
  • page fault 的類型以及其它信息,儲存在了 error code 中;

還記得 error code 嗎? 上一篇中斷處理中提到過,對於某些 exception 發生時,CPU 會自動 壓入 error code 到 stack 中,記錄 exception 的一些信息,page fault 就是這樣一種:數組

這個 error code 很容易獲取到,它就在 page_fault_handler 的參數 isr_params_t 中,還記得這個 struct 嗎?它對應的正是圖中綠色部分的中斷 context 壓棧,被看成參數傳入粉色的中斷 handler 中:數據結構

typedef struct isr_params {
  uint32 ds;
  uint32 edi, esi, ebp, esp, ebx, edx, ecx, eax;
  uint32 int_num;
  uint32 err_code;
  uint32 eip, cs, eflags, user_esp, user_ss;
} isr_params_t;

page_fault_handler 裏對 error code 作了解析,裏面記錄了不少有用的信息,對咱們來講有兩個字段比較重要:多線程

  • present:是不是由於 page 沒有分配致使的 page fault?
  • rw:觸發 page fault 的指令,對內存的訪問操做是不是 write?

它們對應的是 page table 表項的兩個 bit 位:app

  • present 容易理解,它若是是 0,說明該 page 沒有被映射到物理 frame 上,那麼會引發 page fault,這是 page fault 的最多見緣由;
  • 可是即便 present 位爲 1,可是 rw 位爲 0,若是此時對內存作寫操做,也會引起 page fault,咱們須要對這種狀況作特殊處理;這會在後面的進程 fork 中用到的 copy-on-write 技術裏詳解;

分配物理 frame

physical 內存的分配以前咱們都是手動規劃好的,整整齊齊,目前用完了 0 ~ 3MB 的空間。可是從後面開始,剩下的 frames 咱們須要創建數據結構來管理它們,需求無非是兩個:框架

  • 分配 frame;
  • 歸還 frame;

所以須要一個數據結構來記錄下哪些 frame 已經被分配了,哪些還可用,這裏使用了 bitmap 來完成這項工做 。bitmap 但願你並不陌生,它的原理很是簡單樸實,就是用一連串 bit 位,每一個 bit 位表明一個 true / false,咱們這裏就用它來表示 frame 是否已經被使用。

固然做爲一個一貧如洗的 kernel 項目,bitmap 須要咱們本身實現,個人簡單實現代碼在 src/utils/bitmap.c,你能夠看到 src/utils 目錄下有我實現的各類數據結構,這在後面都會用到。

typedef struct bit_map {
  uint32* array;
  int array_size;  // size of the array
  int total_bits;
} bitmap_t;

個人 bitmap 很是簡單,使用一個 int 數組做爲存儲:

分配時也很是簡單粗暴,就是從 0 開始一個個找,找到爲止。固然它有最壞 O(N) 的時間複雜度,不過性能暫時還不是咱們這個項目須要考慮的因素,咱們如今的目標就是簡單,正確。

並且這是一個蛋雞問題:咱們的 bitmap 是用於解決 page fault 的,在 page fault 尚未解決,以及基於 heap 的動態分配內存還沒實現的狀況下,要實現一個複雜的數據結構是很麻煩的。複雜的數據結構勢必涉及到動態分配內存,而一旦動態分配,則隨時會再次引起 page fault,那咱們又回到了原點。

因此一個簡單,能提早分配好靜態內存的數據結構對咱們來講是最簡單高效的實現方式。這裏 bitmap,以及它內部的 array 數組是咱們在 src/mem/paging.c 裏定義的全局變量,它們已經被編譯在 kernel 內部,屬於 databss 段,分配內存的問題固然無需考慮。

static bitmap_t phy_frames_map;
static uint32 bitarray[PHYSICAL_MEM_SIZE / PAGE_SIZE / 32];

注意數組長度爲 PHYSICAL_MEM_SIZE / PAGE_SIZE / 32,應該不難理解。

所以分配 frame 的問題就很是簡單了,就是關於 bitmap 的操做而已:

int32 allocate_phy_frame() {
  uint32 frame;
  if (!bitmap_allocate_first_free(&phy_frames_map, &frame)) {
    return -1;
  }
  return (int32)frame;
}

void release_phy_frame(uint32 frame) {
  bitmap_clear_bit(&phy_frames_map, frame);
}

處理 page fault

萬事具有,接下來處理 page fault 的問題其實已經水到渠成。page_fault_handler 會調用 map_page 函數:

page_fault_handler
  --> map_page
    --> map_page_with_frame
      --> map_page_with_frame_impl

最終來到 map_page_with_frame_impl 這個函數,這個函數略長,但邏輯是很簡單的,這裏以僞代碼爲它註釋:

find pde (page directory entry)
if pde.present == false:
    allocate page table

find pte (page table entry)
if frame not allocated:
    allocate physical frame

map virtual-to-physical in page table

pdepte 的數據結構定義在了 src/mem/paging,h,有了 C 語言的幫助一切都變得很方便,不用像以前在 loader 裏那樣對着一個個 bit 位眼花繚亂了 : )

注意到代碼裏,咱們對 page direcotrypage tables 的訪問,所有使用了 virtual 地址,這是以前在虛擬內存初探一篇中重點講解過的:

  • page directory 的地址爲 0xC0701000
  • page tables 共 1024 張,在地址空間 0xC0400000 ~ 0xC0800000 依次排列;

進程 fork 的虛擬內存處理

代碼裏還涉及到了對 rwcopy-on-write 的處理,以及對當前進程的整個虛擬內存的複製,這都是後面在進程系統調用 fork 時用到的。簡單來講進程 fork 時,須要對虛擬內存作幾件事:

  • 複製整個 page directorypage tables,這樣新 process 的內存空間其實是老 process 的一個鏡像;
  • 新老進程的 kernel 空間共享,user 空間的內存用 copy-on-write 機制隔離;

本篇不作展開,到後面進程系統調用 fork 時再把這個坑填上。

相關文章
相關標籤/搜索