操作系统:期末复习

Last updated on December 12, 2025 pm

本文为 SJTU-CS3601 操作系统课程的期末复习,主要复习内容为操作系统概述、内存管理、进程管理、文件管理

Lecture 1: 操作系统的演化

  • 操作系统的功能
    • 从硬件角度:管理硬件、对硬件进行抽象
    • 从应用角度:服务应用、管理应用

操作系统内核架构

  • 宏内核:内核所有模块运行在内核态,具备直接操作硬件的能力

    • 优点
      • 拥有丰富的沉淀和积累
      • 内核各模块间可以相互调用,性能高
    • 缺点
      • 安全性与可靠性问题:模块之间没有很强的隔离机制
      • 实时性支持:系统太复杂导致无法做最坏情况时延分析
      • 系统过于庞大而阻碍创新
      • 向上向下的扩展:很难去剪裁/扩展一个宏内核系统支持从 KB 级别到 TB 级别的场景
      • 硬件异构性:很难长期支持一些定制化的方式去解决一些特定问题
      • 功能安全:无法通过汽车安全完整性认证
      • 信息安全:单点错误会导致整个系统出错
      • 确定性时延:目前依然不确定是否能支持确定性时延
  • 微内核

    • 设计原则:最小化内核功能
      • 将内核功能拆分到用户态,成为独立的“服务”
      • 内核仅保留极少功能,为这些服务提供通信
    • 优点
      • 易于扩展:直接添加一个用户进程即可为操作系统增加服务
      • 易于移植:大部分模块与底层硬件无关
      • 更加可靠:在内核模式运行的代码量大大减少
      • 更加安全:即使存在漏洞,服务与服务之间存在进程粒度隔离
      • 更加健壮:单个模块出现问题不会影响到系统整体
    • 缺点
      • 性能较差:内核中的模块交互由函数调用变成了进程间通信
      • 生态欠缺:尚未形成像 Linux 一样具有广泛开发者的社区
      • 重用问题:重用宏内核操作系统提供兼容性,带来新问题
  • 外核:不提供硬件抽象;不管理资源,只管理应用

    • 应当由应用来尽可能地控制对硬件资源的抽象
    • 将对硬件的抽象以库(LibOS)的形式提供,不同应用可使用不同的 LibOS
    • 优点
      • OS 无抽象,能在理论上提供最优性能
      • 应用对计算有更精确的实时等控制
      • LibOS 在用户态更易调试,调试周期更短
    • 缺点
      • 对计算资源的利用效率主要由应用决定
      • 定制化过多,导致维护难度增加
  • 多内核:通过多内核来管理异构多核设备

    • 思路
      • 默认的状态是划分而不是共享
      • 维持多份状态的 copy 而不是共享一份状态
      • 显式的核间通信机制
    • 设计
      • 在每个 core 上运行一个小内核
      • OS 整体是一个分布式系统
      • 应用程序依然运行在 OS 之上

Lecture 2: ARM 汇编基础

  • 寄存器x1 - x30 是 31 个 64 位寄存器,w0 - w30 是其低位的 31 个 32 位寄存器

基础指令

  • 寄存器之间的数据搬移mov dst, src
    • src:立即数或寄存器
    • dst:寄存器
  • 算术指令

  • 移位指令

  • 逻辑运算指令

  • 修改过的寄存器:对操作数进行移位或位扩展

    • 无符号扩展:uxtb, uxth, uxtw (Zero-extend single-word / half-word / byte)
    • 符号扩展:sxtb, sxth, sxtw (Sign-extend single-word / half-word / byte)
  • 访存指令


  • 寻址模式


分支指令

  • 条件码:包含 N(Negative)、Z(Zero)、C(Carry)、V(Overflow)

    • 设置
      • 带有 s 后缀的算术或逻辑运算指令(如 subsadds
      • 比较指令:cmp (操作数之差)、cmn (操作数之和)、tst (操作数相与)
        • cmp src1, src2:计算 src1 - src2
  • 跳转条件

  • 跳转指令

    • 直接分支指令:以标签对应的地址作为跳转目标
      • 无条件分支指令:b \<label>
      • 有条件分支指令:bcond \<label>,如 beqbneble
    • 间接分支指令:以寄存器中的地址作为跳转目标
      • br reg,如 br x0
  • for 循环翻译实例

  • while 循环翻译方法及实例

函数调用

  • 函数调用指令
    • bl label (直接调用,调用函数)
    • blr Rn (间接调用,调用函数指针)
    • 功能:将返回地址存储在链接寄存器 LR (即 x30 寄存器),并跳转到被调用者的入口地址
  • 函数返回指令
    • ret (不区分直接调用与间接调用)
    • 功能:跳转到链接寄存器 LR 中的返回地址
  • SP(Stack Pointer)寄存器:栈指针,指向栈顶(低地址)
  • FP (Frame Pointer) 寄存器:帧指针,即 x29 寄存器
  • 标准的函数首尾操作
1
2
3
4
5
stp x29, x30, [sp, #-32]!
mov x29, sp
...
ldp x29, x30, [sp], #32
ret
  • 参数传递
    • 调用者使用 x0 - x7 寄存器传递前 8 个参数
    • 第 8 个之后的参数,按声明顺序从右到左压到栈上,被调用者通过 SP + 偏移量访问
    • 被调用者使用 x0 寄存器传递返回值

  • 寄存器保存
    • 调用者保存:x9 - x15
      • 调用者:在调用前按需进行保存,在返回后进行恢复
      • 被调用者:可以随意使用
    • 被调用者保存:x19 - x28
      • 被调用者:在使用前进行保存,在返回前进行恢复
      • 调用者:这些寄存器的值在函数调用前后不会改变

  • 函数调用实例

Lecture 3: ARM 汇编 - 系统 ISA

  • 常见寄存器

  • 系统指令

    • mrs/msr:从系统寄存器读取值/向系统寄存器写入值
    • svc/eret:特权级切换和返回
  • 特权级切换的时机

    • 同步异常:执行当前指令触发异常
      • 第一类:用户程序主动发起系统调用(svc 指令)
      • 第二类:非主动,如用户程序意外访问空指针
    • 异步异常:CPU 收到中断信号
      • 从外设发来的中断,如屏幕点击、鼠标、收到网络包
      • CPU 时钟中断,如定时器超时

特权级切换过程

  • 处理器在切换过程中的任务

    1. 将发生异常事件的指令地址保存在 ELR_EL1 中
    2. 将异常事件的原因保存在 ESR_EL1
    3. 将处理器的当前状态(即 PSTATE)保存在 SPSR_EL1
    4. 栈寄存器不再使用 SP_EL0(用户态栈寄存器),开始使用 SP_EL1
    5. 修改 PSTATE 寄存器中的特权级标志位,设置为内核态
    6. 找到异常处理函数的入口地址,并将该地址写入 PC,开始运行操作系统
  • 操作系统在切换过程中的任务:将属于应用程序的 CPU 状态保存到内存中,用于之后恢复应用程序继续运行,包括:

    • 通用寄存器 x0 - x30
    • 特殊寄存器,主要包括 PC、SP 和 PSTATE
    • 系统寄存器,包括页表基地址寄存器等
  • 硬件操作的必要性

    • PC 寄存器的值必须由处理器保存:否则当操作系统开始执行时,PC 将被覆盖
    • 栈的切换必须由硬件完成:否则操作系统有可能使用用户态的栈,导致安全问题

系统调用的优化

  • vDSO:内核将一部分数据通过只读的形式共享给应用,允许应用直接读取
  • Flex-SC:允许应用以“向某一块内存页写入请求”的方式发起系统调用,并通过轮询来等待系统调用完成
    • 内核独占一个 CPU 核心,通过轮询来等待用户的请求,然后执行系统调用,并将返回值写入同一块内存页
    • 如果只有一个核心,可以将轮询改成批处理,即应用程序一次发起多个系统调用请求,内核一次性将所有系统调用处理完

Lecture 4: 从应用视角看操作系统抽象

  • 处理器上下文
    • 通用寄存器:所有(X0-X30)
    • 特殊寄存器:SP_EL0 (栈寄存器)
    • 系统寄存器:ELR_EL1(对应 PC), SPSR_EL1(对应 PSTATE)

常见的进程接口

  • exit():终止进程并带上一个 status 状态

    • 返回值:无返回值
    • 语法:void exit(int status);
  • fork():父进程创建新的子进程,调用一次返回两次

    • 返回值:子进程为 0,父进程为子进程 PID
    • 语法:pid_t fork(void);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "csapp.h"
int main()
{
pid_t pid;
int x = 1;
pid = Fork();
if (pid == 0) { /* child */
printf("child : x=%d\n", ++x);
exit(0);
}
/* parent */
printf("parent: x=%d\n", --x);
exit(0);
}
  • execve():加载和运行

    • 只调用一次,且永远不会返回(仅仅在运行报错的时候,返回调用程序)
    • 语法:int execve(const char *filename, const char *argv[], const char *envp[]);
  • waitpid():等待子进程终止后,内核回收子进程

    • 返回值:成功返回子进程 PID,出错返回 -1
    • 语法:pid_t waitpid(pid_t pid, int *status, int options);
      • pid>0 :等待集合中只有 pid 子进程
      • pid=-1:等待集合包括所有子进程
      • options=0
        • 挂起调用进程,等待集合中任意子进程终止
        • 如果等待集合中有子进程在函数调用前已经终止,立刻返回
        • 返回值是导致函数返回的终止子进程 pid
        • 该终止子进程被内核回收
      • status 指针带回被回收子线程的 exit 状态
  • 进程的终止:进程终止后,内核不会立刻销毁该进程,而是以终止态存在,等待父进程回收

    • 僵尸进程:终止状态下还未被回收的进程
      • 如果父进程在自己终止前没有回收僵尸子进程,内核会安排 init 进程回收这些子进程

ELF 文件格式

  • ELF 格式:可执行可链接格式

ELF 格式

  • ELF 头部:通常用于存元数据
  • 节头部表:每一个节都有一个节头部(节头部表的一项)描述
  • ELF 字符串表(.strtab):记录一系列 C 风格字符串,表示符号名或节名
  • 用以调试的节
    • .debug:调试符号表,包括变量、typedef、C 源文件
    • .line:C 源文件的行数与 .text 节中指令的映射
  • 代码和数据节
    • .text:代码
    • .rodata:只读数据,包括不可修改的常量数据
      • char *str = "apple" 中的 "apple" 存放在 .rodata
      • char str2[] = "apple" 中的 "apple" 存放在栈上
    • .data:初始化的全局变量和静态变量
    • .bss:未初始化的全局变量和静态变量(不占文件空间,运行时分配内存)

Lecture 5: 内存地址翻译’

Lecture 6: 系统初始化

Lecture 7: 操作系统管理页表映射

Lecture 8: 物理内存管理

Lecture 9: 进程

  • 进程控制块(PCB):用于表示进程,存储在内核态

    • 保存的信息包括:虚拟地址空间、处理器上下文、内核栈
    • 进程标识符 PID、退出状态、子进程列表、执行状态
  • 进程的退出与等待

    • 进程的退出:进程退出时,其上下文结构和虚拟地址空间会被销毁,但 PCB 和内核栈保留(处于僵尸状态)
    • 进程的等待:父进程会等待子进程退出,记录子进程的退出状态,回收 PCB 并销毁内核栈
  • 进程的五种典型执行状态

    • 新生(new):刚调用 process_create
    • 就绪(ready):随时准备执行(但暂时没有执行)
    • 运行(running):正在执行
    • 僵尸(zombie):退出但未回收
    • 终止(terminated):退出且被回收

进程的创建

进程创建的过程

  • 一、PCB 相关初始化:PCB 及其包含的内容都需要创建及初始化

    • 分配 PCB 本身的数据结构
    • 初始化 PCB:虚拟内存
      • 创建及初始化 vmspace 数据结构
      • 分配一个物理页,作为顶级页表
    • 内核栈:分配物理页,作为进程内核栈
  • 二、可执行文件加载:可执行文件通常有固定的存储格式,以 ELF 为例

    • 从程序头部表可以获取需要的段所在位置
    • 通常只有代码段和数据段需要被加载(loadable)
    • 加载即从 ELF 文件中映射到虚拟地址空间的过程
  • 三、准备运行环境:在返回用户态运行前,还需为进程准备运行所需的环境

    • 分配用户栈:分配物理内存并映射到虚拟地址空间
    • 准备程序运行时的环境:将参数和环境变量放到栈上
  • 四、处理器上下文初始化:最后才初始化处理器上下文,因为其包含的内容直到前序操作完成才确定

    • SP:设置为用户栈的栈顶地址(用户栈分配后才确定地址)
    • PC:设置 ELR_EL1 寄存器(加载 ELF 才知道程序入口地址)
    • PSTATE:设置 SPSR_EL1 寄存器
    • 大部分寄存器初始值可直接赋为 0

Linux 的进程创建

  • fork()

    • 实现:将父进程的 PCB 拷贝一份,包括虚拟内存、内核栈、处理器上下文等
    • 优点
      • 接口非常简洁,(过去)实现简单
      • 将进程创建和执行(exec)解耦,提高了灵活度
    • 缺点
      • 创建拷贝的复杂度与 PCB 复杂度相关,如今越来越复杂
      • 完全拷贝过于粗暴(不如 clone
      • 性能差、可扩展性差(不如 vforkspawn
      • 不可组合性 (如 fork() + pthread())
  • fork 的替代接口

    • vfork:类似于 fork,但让父子进程共享同一地址空间
      • 优点:连映射都不需要拷贝,性能更好
      • 缺点:只能用在“fork + exec”的场景中;共享地址空间存在安全问题
    • posix_spawn: 相当于 fork + exec
      • 优点:可扩展性、性能较好
      • 缺点:不如 fork 灵活
    • clone: fork 的进阶版,可以选择性地不拷贝内存
      • 优点:高度可控,可依照需求调整
      • 缺点:接口比 fork 复杂,选择性拷贝容易出错

进程切换

  • 一、p0 进入内核态:由硬件完成部分寄存器保存

    • PC 和 PSTATE 分别自动保存到 ELR_EL1 和 SPSR_EL1
  • 二、p0 处理器上下文保存:将处理器中的寄存器值保存到处理器上下文对应的位置

  • 三、由 p0 切换到 p1

    • 1. 虚拟地址空间切换:设置页表相关寄存器(TTBR0_EL1)
      • 使用 PCB 中保存的页表基地址赋值给 TTBR0_EL1
    • 2. 内核栈切换:设置内核中的栈寄存器 SP_EL1
      • 使用 PCB 中保存的内核栈顶地址赋值给 SP_EL1
    • 3. 进程上下文切换:设置 cur_proc 为之后要执行的进程(p1)
      • 表明之后操作系统将以 p1 的身份运行
  • 四、p1 处理器上下文恢复:从处理器上下文中加载各寄存器的值,放入对应寄存器中

  • 五、p1 回到用户态:由硬件自动恢复部分寄存器

    • 将 ELR_EL1 和 SPSR_EL1 中的值自动保存到 PC 和 PSTATE 中

参考资料

本文参考上海交通大学并行与分布式系统研究所(IPADS)操作系统课程 CS3601 华志超老师的 PPT 课件整理。

部分图片来源于上海交通大学程序语言与编译原理课程 CS2612 曹钦翔老师的讲义。


操作系统:期末复习
https://cny123222.github.io/2025/12/11/操作系统:期末复习/
Author
Nuoyan Chen
Posted on
December 11, 2025
Licensed under