重读 xv6(I)

xv6 启动与概览

前言 #

五月份我做完 MIT6.1810 的 Lab 后,一直没有做回顾

虽然有零散的 xv6 book 与 源码阅读笔记,还是没有完成把知识体系化的工作

因此我决定重读一次 xv6 ,一方面是为了复盘项目,捋一遍 OS 设计的 best parctice,另一方面,也是通过产出文本的手段来丰富个人博客,准备面试

本系列会以 程序流 的视角展开写作,从而表现系统中的逻辑关联,编排顺序较为随意

以讨论设计为主,对基础概念会一笔带过,不会大量罗列代码,请自行结合源码阅读

#

我们从启动流程开始,检查初始化过程,认识 xv6 的系统模块

源码文件:

  • entry.S
  • start.c
  • main.c

启动流程:

  1. 机器启动,将 kernel 加载到内存
  2. 运行 entry.S,为每个 CPU 设置运行栈,此时为 machine mode
  3. 运行 start,进行一些初始配置,切换为 kernel mode
  4. 运行 main,初始化各个模块,创建首个进程,切换为 user mode
  5. 运行 init,设置标准 I/O,启动 shell,开始提供服务

混沌状态 #

机器刚刚启动时,我们还没有完成好基础设施的配置,如何完成上面的流程呢?

  • 加载 kernel,用的是预留的 load 程序
  • entry.S 放在 kernel 的起始地址 0x80000000,作为物理上的程序起点
  • 通过 call start ,并重定向 ret 的返回地址到 main ,依次执行 start, main
  • 再调用userinit(),通过调度器自然转到 init 进程,正常调用 shell 循环

到这里完成开机,成功地将启动过程 透明化

初始化模块 #

下面是启动过程中涉及到的模块

start

  • 时钟中断,用于虚拟化 CPU,切换进程
  • 页表映射,用于虚拟化内存地址

main

  • 内存池、分页,用于分配用户空间,虚拟化内存
  • trap 中断,用于响应异常,实现系统调用
  • 进程管理,用于分配资源,管理进程
  • 文件系统,用于控制文件读写
  • PLIC、设备驱动、磁盘

加上用于控制并发的 ,基本已经涵盖我们所需要的基础设施,这些就是我们讨论的主题了

第一个进程 #

初始化结束后,main 会调用 proc.c/userinit ,创建第一个进程 initcode.S,引导执行 init.c 程序

随后 initfork 第二个进程 shell,作为 dummy proc 进入死循环,等待停机

shell 负责接收用户输入,提供 OS 服务,启动流程结束

#

下面讨论 OS 服务的底层逻辑

分时系统 #

OS 需要同时响应多个用户的任务,因此产生 虚拟化 CPU 的设计

我们为用户进程提供独占 CPU 的假象,透明地进行 CPU 调度,保存进程的执行上下文,这就是 进程管理

隔离空间 #

OS 需要对用户屏蔽底层存储的细节,因此产生 虚拟化内存 的设计

我们为用户进程提供独占 内存 的假象,借此隔离不同进程,动态分配物理地址,这就是 页表机制

操控权限 #

我们的 CPU 数量是有限的,如果都运行着用户进程,OS kernel 又如何发挥作用呢?

必须让 kernel 夺取运行权限,这种抢夺机制又可以用来处理 异常与设备 I/O,这就是 中断机制(Trap)

记录结果 #

程序的运行结果需要持久化,存储在磁盘里

我们不可能精确到磁道层面去考虑问题,需要 OS 层面提供足够好的抽象

Unix 的选择是 文件、管道、设备 的字节流抽象,加上对文件结构的组织与保护,构成了文件系统

API #

用户编写的程序,应该是面向 OS 接口的,我们向外提供的 API 就是他们与系统交互的唯一方式

但是我们也必须假设有恶意用户的存在,需要对资源进行保护,只有 kernel 才能操作敏感资源,这就是 系统调用 的想法


以上就是这些设计的动机,我们会分别进行展开讨论