Lec 19 操作系统
到目前为止我们见到的都是单程序机器:硬件运行一个程序,独占处理器、内存、磁盘、网卡、显示器、键盘等全部资源,软硬件之间唯一的契约就是 ISA(如 RISC-V),程序直接写到 ISA 上。但只有洗碗机控制芯片这类简单系统才这样工作。现实中我们同时运行多个程序,它们不直接碰硬件,而是用“文件”等更高层抽象。
本讲引入操作系统(OS):它介于多个程序与硬件之间,管理它们如何共享资源。这需要硬件提供专门支持(模式、异常、虚拟内存),是“让计算机好用”的关键一环。
Outline
- 为什么需要操作系统
- 程序 vs 进程;ISA vs ABI;虚拟机
- 支持 OS 的四项 ISA 扩展
- 异常与中断
- 异常处理机制
- 三个用例:调度 / 指令仿真 / 系统调用
- 进程生命周期与 CSR
为什么需要操作系统
OS 内核(kernel)是一个有特权的进程,存在三大理由:
- 保护与隔离(protection & privacy):不让进程访问/读取彼此的数据,即使某进程崩溃或有 bug。最好让每个进程以为自己独占一台机器。
- 抽象(abstraction):隐藏底层硬件细节,进程通过文件系统等抽象访问设备,而不必(也不该)直接操作硬盘等(直接访问还很危险)。
- 资源管理(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 扩展
- 两种执行模式:用户模式(user)与管理模式(supervisor)。OS 内核跑在 supervisor,其它一切跑在 user。
- 特权状态与指令:某些寄存器/指令是特权的,只有 supervisor 模式能用。
- 异常与中断(exceptions & interrupts):当程序出错或有事件发生时,安全地从 user 切到 supervisor,让内核介入。
- 虚拟内存:提供私有地址空间、抽象存储资源(见 Lec 18)。
软硬件约定必须严格对齐,这套机制才成立。
异常与中断
异常(exception):应用自己处理不了、需要 OS 内核处理的事件(通常罕见或意外)。可把它看成一次隐式的“调用进 OS”:处理器跳到内核里的异常处理程序(handler),处理完通常把控制权交还给进程(在原指令
| 异常(狭义/同步) | 中断(异步) | |
|---|---|---|
| 来源 | 程序指令本身产生 | I/O 设备“按门铃” |
| 时机 | 同步(可预测到是哪条指令) | 异步(随时可能来) |
| 例子 | 非法指令、系统调用、除 0 | 键盘按键、定时器到期、磁盘传输完成 |
两词常混用;本课统称“异常”,把同步的叫“同步异常”。
异常处理机制
异常发生在指令
- 完成
及之前的所有指令,但不完成 ;(单周期下很容易;到了流水线,同时有多条指令在飞,这会带来麻烦——见 Lec 16。) - 保存现场:把当前 PC、异常原因等存入只有内核可访问的特权寄存器;
- 进入 supervisor 模式,跳转到一个预先指定的 handler PC(地址固定,程序不能跳到任意位置);
- 处理完后(若非致命)回到
继续——异常对进程是透明的。
这很像每条指令都额外具备“跳转到别处”的能力的 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。