从零开始写 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 时再把这个坑填上。

相关文章
相关标签/搜索