关于操作系统:从零开始写-OS-内核-虚拟内存完善

4次阅读

共计 3986 个字符,预计需要花费 10 分钟才能阅读完成。

系列目录

  • 序篇
  • 筹备工作
  • BIOS 启动到实模式
  • GDT 与保护模式
  • 虚拟内存初探
  • 加载并进入 kernel
  • 显示与打印
  • 全局描述符表 GDT
  • 中断解决
  • 虚拟内存欠缺
  • 实现堆和 malloc
  • 创立第一个内核线程
  • 多线程运行与切换
  • 锁与多线程同步
  • 过程的实现
  • 进入用户态
  • 一个简略的文件系统
  • 加载可执行程序
  • 零碎调用的实现
  • 键盘驱动
  • 运行 shell

开拓虚拟空间

在虚拟内存初探一篇中曾经在 loader 阶段初步为 kernel 建设了虚拟内存的框架,包含 page directory,page table 等。在那篇里,咱们在 0xC0000000 以上的 kernel 空间曾经开拓了它前三个 4MB,并且手工指定了它们的性能:

  • 0xC0000000 ~ 0xC0400000:映射初始低 1MB 内存;
  • 0xC0400000 ~ 0xC0800000:页表;
  • 0xC0800000 ~ 0xC0C00000:kernel 加载;

在这一阶段,所有的内存都是咱们手动布局好的,virtual-to-physical 的映射也是手动调配的,这当然不是长久之计。后续的 virutal 内存将会以一种更灵便的形式动态分配,所映射的 physical 内存也不再是提前调配,而是按需取用,这就须要进行缺页异样(page fault)的解决。

缺页异样

page fault 的概念这里不做赘述,咱们在上一篇中断解决的最初尝试了触发一个 page fault,然而它的处理函数只是一个 demo,没有做真正解决 page fault 的问题,当初咱们就来解决它。

page fault 解决的外围问题有两个:

  • 确定产生 page fault 的 virtual 地址,以及异样的类型;
  • 调配物理 frame,并建设映射;

page fault 详情

第一个问题比较简单,咱们间接看代码:

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 位:

  • 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 时再把这个坑填上。

正文完
 0