Skip to content

Lec 19 操作系统

到目前为止我们见到的都是单程序机器:硬件运行一个程序,独占处理器、内存、磁盘、网卡、显示器、键盘等全部资源,软硬件之间唯一的契约就是 ISA(如 RISC-V),程序直接写到 ISA 上。但只有洗碗机控制芯片这类简单系统才这样工作。现实中我们同时运行多个程序,它们不直接碰硬件,而是用“文件”等更高层抽象。

本讲引入操作系统(OS):它介于多个程序与硬件之间,管理它们如何共享资源。这需要硬件提供专门支持(模式、异常、虚拟内存),是“让计算机好用”的关键一环。

Outline

  • 为什么需要操作系统
  • 程序 vs 进程;ISA vs ABI;虚拟机
  • 支持 OS 的四项 ISA 扩展
  • 异常与中断
  • 异常处理机制
  • 三个用例:调度 / 指令仿真 / 系统调用
  • 进程生命周期与 CSR

为什么需要操作系统

OS 内核(kernel)是一个有特权的进程,存在三大理由:

  1. 保护与隔离(protection & privacy):不让进程访问/读取彼此的数据,即使某进程崩溃或有 bug。最好让每个进程以为自己独占一台机器
  2. 抽象(abstraction):隐藏底层硬件细节,进程通过文件系统等抽象访问设备,而不必(也不该)直接操作硬盘等(直接访问还很危险)。
  3. 资源管理(resource management):把算力分配给重要进程,防止某进程独占 CPU 或内存。

于是出现两个接口:ISA 管硬件与 OS 之间,ABI(Application Binary Interface) 管 OS 与普通程序之间。

程序 vs 进程;虚拟机

  • 程序(program):一组指令(你写的代码)。
  • 进程(process):程序的一次运行实例——既含代码,也含寄存器、内存等运行时状态。(类比“类 vs 对象”“函数 vs 函数的一次运行”。)

OS 为每个进程提供三样虚拟化:私有地址空间(虚拟内存)、CPU 时间片(调度)、外设访问接口(系统调用)。这三者合起来,让每个进程都以为自己运行在一台独占的机器上——这就是虚拟机(VM)的抽象。

这个抽象层层嵌套:Lab 的 RISC-V 程序 → 跑在 sim.py(RISC-V 模拟器,一个 Python 程序)→ 跑在 CPython(Python VM)→ 跑在 Linux 内核(x86 VM)→ 可能又跑在 VMware(模拟整台 x86)→ ……可达五层不同类型的 VM。纯软件模拟(如 Python 解释器)很慢(比原生慢 10–100 倍),所以我们给处理器加硬件支持,让应用大多数时候直接用硬件,只在必要时让 OS 安全介入。

支持 OS 的四项 ISA 扩展

  1. 两种执行模式:用户模式(user)与管理模式(supervisor)。OS 内核跑在 supervisor,其它一切跑在 user。
  2. 特权状态与指令:某些寄存器/指令是特权的,只有 supervisor 模式能用
  3. 异常与中断(exceptions & interrupts):当程序出错或有事件发生时,安全地从 user 切到 supervisor,让内核介入。
  4. 虚拟内存:提供私有地址空间、抽象存储资源(见 Lec 18)。

软硬件约定必须严格对齐,这套机制才成立。

异常与中断

异常(exception):应用自己处理不了、需要 OS 内核处理的事件(通常罕见或意外)。可把它看成一次隐式的“调用进 OS”:处理器跳到内核里的异常处理程序(handler),处理完通常把控制权交还给进程(在原指令 Ii 处继续);若是非法操作(如段错误)则终止该进程。

异常(狭义/同步)中断(异步)
来源程序指令本身产生I/O 设备“按门铃”
时机同步(可预测到是哪条指令)异步(随时可能来)
例子非法指令、系统调用、除 0键盘按键、定时器到期、磁盘传输完成

两词常混用;本课统称“异常”,把同步的叫“同步异常”。

异常处理机制

异常发生在指令 Ii 时,处理器需要:

  1. 完成 Ii1 及之前的所有指令,但不完成 Ii;(单周期下很容易;到了流水线,同时有多条指令在飞,这会带来麻烦——见 Lec 16。)
  2. 保存现场:把当前 PC、异常原因等存入只有内核可访问的特权寄存器
  3. 进入 supervisor 模式,跳转到一个预先指定的 handler PC(地址固定,程序不能跳到任意位置);
  4. 处理完后(若非致命)回到 Ii 继续——异常对进程是透明的。

这很像每条指令都额外具备“跳转到别处”的能力的 branch。

三个用例

用例 1:CPU 调度(靠定时器中断)

要限制每个进程的 CPU 时间,用一个可编程定时器(如经 MMIO)。内核把控制交给进程 1 前,设定时器在 ~20ms 后触发中断、载入进程 1 的状态、进入 user 模式;定时器中断到来时,CPU 跳到 handler,保存进程 1 状态、调度进程 2、重设定时器、载入进程 2 状态,如此往复。这种“不靠程序主动让出、而由 OS 强行切换”叫抢占式多任务(preemptive multitasking)。通过控制时间片长短,可按优先级调控各进程的 CPU 速度。

用例 2:指令仿真(靠非法指令异常)

可以“伪造”硬件里不存在的指令、用软件实现其功能。例如 mul(RISC-V M 扩展)在 RV32I 上是非法指令——一执行就抛非法指令异常,内核 handler 发现可以用软件(反复相加)模拟乘法,替进程算完,然后返回到该指令之后(避免再次触发)。于是 RV32I 机器“假装”支持 RV32IM。代价是慢得多(来回切换内核、存恢复状态)。

用例 3:系统调用(靠 ecall

进程访问文件、调用其它系统服务要用系统调用——类似函数调用,但进入 OS。为防程序乱跳,用专门的、会触发异常的指令:RISC-V 用 ecall(environment call)。系统调用可用于开关/读写文件、网络(socket)、内存管理、查询系统信息、休眠/让出、创建/中断其它进程等。

程序通常不直接发系统调用,而是经低层库间接调用:如 C 的 printf 内部调 sys_write。有些系统调用会阻塞进程(如休眠、等文件读完),服务完成后再唤醒。

进程生命周期与 CSR

进程生命周期:创建 → 就绪(ready) → 被调度进/出执行(executing);系统调用可能阻塞它进入等待(waiting),唤醒后回到 ready;完成时用系统调用通知并被终止。OS 始终维护一张“所有进程及其状态(ready/executing/waiting)”的列表。

RISC-V 用一组特权的控制状态寄存器(CSR, Control and Status Registers)记录这些信息,例如:

  • mepc:异常 PC(出异常时的指令地址);
  • mcause:异常原因;
  • csrr / csrw 读写 CSR,并有专门指令把控制权交回用户态。

系统调用的约定与函数调用类似(见 Lec 9 调用约定)。

下一步——“共享内存”的硬件支持,即虚拟内存,见 Lec 18