- Published on
MIT 6.S081 Lecture 2. OS organization and system calls
- Authors

- Name
- Vegetog
课堂笔记
每个进程有自己独立的页表,因而只能访问出现在页表中的物理地址,实现了进程之间的物理地址隔离
关于某个系统调用是否合法,这一步主要在内核态进行检验,例如检查write的参数是否合法
如何理解进程是对CPU的一种抽象
**物理层面:**CPU 的程序计数器(PC,Program Counter)在一个时钟周期内只能指向一条指令
并发执行:由操作系统的时间片轮转(Time Slicing)和抢占机制实现了并发执行
**虚拟化:**通过虚拟化(Virtualization)CPU,让进程认为自己在独占CPU
**快照机制:**当 OS 决定停止执行进程 A 改为执行进程 B 时,它会将当前 CPU 的所有寄存器值保存到进程 A 的 PCB(Process Control Block,进程控制块) 中,再次运行A时,就可以回到之前的状态
**复用方式:**具体的,复用CPU的方式有波分复用、时分复用、码分复用等
Q&A
xv6的启动过程
有点复杂,这里分两个版本来讲
简单来说
可以把这个过程简化为 4 个核心步骤:
1. 找个“落脚点” (汇编阶段)
- 文件:
entry.S - 动作:给每个 CPU 分配一小块内存作为栈 (Stack)。
- 原因:CPU 刚启动时只有寄存器,没有栈。没有栈就没法运行 C 语言代码(因为函数调用、局部变量都需要栈)。
- 结果:设置好栈指针
sp后,赶紧跳到 C 语言环境的start函数。
2. 身份“降级” (机器模式)
- 文件:
start.c - 动作:CPU 默认在最高权限(Machine Mode)运行。
start()函数会做一系列配置,然后通过执行mret指令,把权限降低到监管者模式 (Supervisor Mode),并跳入main()函数。 - 原因:就像公司老板不能事无巨细都管,内核通常运行在权限稍低一点的 S 模式,把最核心的权限留给硬件配置。
- 配置:顺便关掉内存分页(让地址直接对应物理内存)、把时钟中断委托给内核处理。
3. 大管家“装修” (内核初始化)
- 文件:
main.c - 动作:
- 主 CPU (Hart 0):负责所有的“装修”工作,包括初始化物理内存分配器、创建内核页表、初始化磁盘驱动、进程表等。
- 其他 CPU:在旁边“摸鱼”等待,直到主 CPU 把活干完。
- 关键任务:调用
userinit()准备好第一个用户进程(就像 Windows 启动后的桌面程序一样)。
4. 开启“正式营业” (调度循环)
- 文件:
main.c - 动作:所有 CPU 都会进入
scheduler()(调度器)函数。 - 结果:调度器会在进程表里找刚才准备好的第一个进程。一旦找到,就跳到那个程序去运行。至此,内核启动完成,系统开始运行用户程序。
总结一下
| 阶段 | 核心任务 | 状态变化 |
|---|---|---|
第一步 (entry.S) | 分配栈空间 | 硬件状态 环境就绪 |
第二步 (start.c) | 切换到内核权限 | Machine Mode Supervisor Mode |
第三步 (main.c) | 初始化内存和设备 | 只有代码 完整系统架构 |
第四步 (main.c) | 运行第一个程序 | 内核初始化 用户可用 |
完整过程
结合xv6的代码来讲
第一阶段:硬件加载与入口 (entry.S)
当 QEMU 模拟器启动时,它会将内核加载到内存地址 0x80000000 处。此时,所有的 CPU 核心(在 RISC-V 中称为 hart,即 Hardware Thread)都会从这个地址开始执行。
设置 C 语言运行环境:由于 C 语言的函数调用依赖于栈(Stack),而硬件复位时栈指针
sp是未定义的,所以_entry的首要任务是为每个 CPU 分配一个独立的栈。计算栈地址:内核在 start.c 中预定义了一个 4096 字节倍数的数组 stack0。entry.S 通过以下公式为每个核心设置 sp:
sp = stack0 + (hartid + 1) * 4096。
跳转到 C 代码:设置好栈后,核心会通过
call start进入start.c。
第二阶段:特权模式切换 (start.c)
RISC-V 硬件在复位后默认处于最高特权级——机器模式(Machine Mode)。为了系统的安全和稳定,内核通常运行在监管者模式(Supervisor Mode),而用户程序运行在用户模式(User Mode)。start() 函数的核心任务就是将特权级从 Machine Mode 降低到 Supervisor Mode。
- 准备切换(mret 机制):RISC-V 无法直接通过一条指令简单地“修改”当前模式,而是需要模拟一次“中断返回”。
- 通过设置
mstatus寄存器的MPP位为S(Supervisor),告诉硬件下次执行返回指令时进入 S 模式。 - 通过
w_mepc((uint64)main)将main函数的地址写入异常程序计数器,作为返回后的跳转目标。
- 通过设置
- 配置权限:
- PMP (Physical Memory Protection):配置物理内存保护,允许 S 模式访问所有的物理内存。
- 中断委托:将所有的中断和异常处理交给 S 模式处理。
- 最后一步:执行
asm volatile("mret")。这条指令会根据之前的设置,让 CPU 切换到 Supervisor Mode 并跳转到main()执行。
第三阶段:内核初始化 (main.c)
当代码进入 main() 时,系统已经运行在 Supervisor Mode。为了避免多个 CPU 同时初始化全局资源造成冲突,xv6 采用了“主核负责,从核等待”的策略:
- 主核(Hart 0)初始化全局资源:
consoleinit()/printfinit():初始化控制台,这样我们才能在屏幕上看到输出。kinit():初始化物理内存分配器。kvminit():创建内核页表。userinit():最关键的一步,创建第一个用户进程(通常是执行initcode.S)。- 同步信号:主核完成上述任务后,将全局变量
started置为 1,并执行内存屏障__sync_synchronize()确保所有核心都能看到这个变化。
- 从核(Other Harts)等待与局部初始化:
- 非 0 号核心会在
while(started == 0)循环中自旋等待。 - 一旦
started变为 1,它们会执行自己核心相关的局部初始化,例如kvminithart()(开启本核心的分页机制)和trapinithart()(设置本核心的中断向量表)。
- 非 0 号核心会在
第四阶段:进入调度器
最后,所有的核心(包括主核和从核)都会调用 scheduler()。
- 此时内核初始化宣告完成。
scheduler()是一个死循环,它会不断寻找可运行(RUNNABLE)的进程。- 主核之前通过
userinit()创建的进程将被第一个选出并运行,最终引导出 Shell。
如何从用户态(User Mode)转换到内核态(Kernel Mode)
在操作系统中,从用户态(User Mode)转换到内核态(Kernel Mode)是保证系统安全与稳定的核心机制。对于正在学习 MIT 6.S081 并准备 C++ 面试的你来说,这不仅是一个操作系统概念,更是理解高性能编程(如减少上下文切换开销)的基础。 主要有三种途径可以触发这种转换:
- 系统调用 (System Call) 这是最常见的一种方式,是用户程序主动申请内核服务。
- 场景: 当你需要读写文件(read/write)、申请内存(brk/mmap)或创建进程(fork)时。
- 机制: 用户程序通过执行特定的汇编指令(如 x86 上的 syscall 或 int 0x80)来触发。这会引发一个特殊的陷阱(Trap),让 CPU 跳转到预定义的内核入口点。
- 异常 (Exceptions) 这是由 CPU 在执行指令时检测到的被动转换。
- 场景: 典型的例子是缺页异常 (Page Fault)、除以零、非法指令等。
- 机制: 当发生这些情况时,当前指令无法继续执行,CPU 会自动跳转到内核定义的异常处理程序中。
- 外设中断 (Hardware Interrupts) 这是由外部设备触发的异步事件。
- 场景: 硬盘读写完成、网卡接收到数据包、时钟中断(用于进程调度)。
- 机制: 硬件向 CPU 发出信号,CPU 在执行完当前指令后,会根据**中断向量表(Interrupt Vector Table)**跳转到对应的内核驱动程序。 转换过程的底层步骤 无论哪种途径,转换的核心逻辑是一致的,目的是安全地切换“上下文”:
- 保存现场 (Save Context): CPU 会将用户态的寄存器(如 PC、SP 以及通用寄存器)压入该进程对应的**内核栈(Kernel Stack)**中。
- 切换栈指针: 将堆栈指针从用户栈切换到内核栈。
- 提升特权级: 将 CPU 的运行级别从 Ring\ 3(用户态)切换到 Ring\ 0(内核态)。
- 执行处理程序: 根据陷阱号(Trap Number)或向量号,跳转到对应的内核处理函数。
- 恢复与返回: 处理完成后,通过执行 iret 或 sysret 指令,还原寄存器,切换回用户栈,并将特权级降回。
对于课程中提到qemu的作用的部分,补充一些计算机组成原理的知识
- 核心架构:冯·诺依曼模型 (Von Neumann Architecture) 计算机的基础是“存储程序”概念。它主要由五部分组成:
- 运算器 (ALU): 负责数学运算(加减乘除)和逻辑运算(与或非)。
- 控制器 (CU): 负责指挥全局,指挥各部件协调工作。
- 存储器 (Memory): 即内存,存放指令和数据。
- 输入设备 (Input): 键盘、鼠标。
- 输出设备 (Output): 显示器。
面试点: 在 C++ 中,程序的执行实际上就是将编译好的机器码从硬盘加载到内存,再由 CPU 依次读取。
- 寄存器 (Registers):CPU 的“贴身口袋” 寄存器是 CPU 内部存储速度最快的存储单元。由于内存(RAM)距离 CPU 相对较远且速度慢,CPU 需要寄存器来暂存正在处理的数据。 常见的寄存器包括:
- 程序计数器 (PC, Program Counter): 存储下一条要执行的指令在内存中的地址。它是指挥官,决定了程序运行到哪了。
- 指令寄存器 (IR, Instruction Register): 存放当前正在执行的指令内容。
- 通用寄存器 (GPRs): 如 rax, rbx 等(X86 架构),用于存放运算的操作数或中间结果。
- 栈指针寄存器 (SP, Stack Pointer): 永远指向当前函数调用栈的栈顶。
- 指令执行周期 (Instruction Cycle) 一条指令从产生到完成,通常经历以下四个阶段: 第一步:取指令 (Fetch) CPU 根据 PC 寄存器 中的地址,去内存里找到对应的指令,并将其读入到 IR (指令寄存器) 中。取完后,PC 会自动递增,指向下一条指令。 第二步:译码 (Decode) 控制器 (CU) 对 IR 中的指令进行“翻译”。它需要搞清楚:
- 这是什么操作?(加法还是跳转?)
- 操作数在哪里?(在寄存器里还是在内存里?) 第三步:执行 (Execute) 根据译码结果,运算器 (ALU) 开始干活。如果是 a + b,它就把两个数加起来。 第四步:访存与写回 (Memory Access & Write Back) 如果结果需要保存到内存,就执行访存操作。最后,将计算结果写回到指定的寄存器或内存地址。