它打破了我们在第 1-7 章建立的“指令一条接一条顺序执行”的简单模型,引入了控制流的突变。ECF 是操作系统实现进程、虚拟内存、I/O 和网络通信的基础机制。


1. 什么是异常控制流 (ECF)?

在传统的程序执行中控制流是平滑的(PC 递增或跳转)。ECF 是指控制流发生**突然变化且不是由程序内部正常逻辑引起的情况。

EC2 发生在硬件和软件的所有层次:

  • 硬件层:中断、陷阱、故障、终止。
  • 操作系统层:进程上下文切换、信号处理。
  • 应用层longjmp、非局部跳转。

核心思想:ECF 是操作系统用来模拟并发(Concurrency)的基本机制。


2. 异常(Exceptions)

异常是 ECF 的最底层形式,由硬件事件触发,通过异常表(Exception Table)跳转到内核中的异常处理程序

异常的四种类型
类型 英文 特点 返回行为 例子
中断 Interrupt 异步,来自外部 I/O 设备 返回下一条指令 键盘输入、网卡数据包到达
陷阱 Trap 同步,有意为之的系统调用 返回下一条指令 syscall fork, read, write
故障 Fault 同步,可恢复的错误 可能返回当前指令 缺页异常(Page Fault)、除零错误(部分架构)
终止 Abort 同步,不可恢复的错误 不返回(杀死进程) 非法指令、硬件严重错误、段错误(Segfault某些情况)
异常处理流程
  1. 当前指令执行完毕(或出错)。
  2. 硬件识别异常号 kkk
  3. 间接调用异常表中第 kkk 项指向的处理程序。
  4. 处理程序执行(通常在内核模式)。
  5. 根据异常类型,返回到被中断的程序(下一条指令或当前指令 a 或终止。

关键点:系统调用(System Call)本质上是一种特殊的**陷阱(Trap)**指令。用户程序通过执行 syscall 指令陷入内核,内核执行相应服务后返回用户态。


3. 进程(Processes)

进程是 CSAPP 中最重要的抽象之一。

逻辑控制流
  • 每个进程都有一个独立的逻辑控制流。
  • 多个进程可以并发运行(单核交替执行)或并行运行(多核同时执行)。
  • 上下文切换(Context Switch):内核保存当前进程的状态(上下文),恢复下一个进程的状态。这是开销较大的操作。
私有地址空间

每个进程拥有独立的虚拟地址空间,包含:

  • 代码段 (.text)
  • 数据段 (.data, .bss)
  • 堆 (Heap)
  • 栈 (Stack)
  • 共享库区域
Linux 进程结构
  • PID:进程 ID。
  • 父进程与子进程关系形成树状结构
  • Init 进程:祖先进程。

4. 系统调用错误处理

在 Unix/Linux中许多系统调用出错时返回 -1 并设置全局变量 errno

  • Unix 风格:检查返回值。
  • Wrapper 函数:CSAPP 书中提倡使用包装函数(如 Popen, Fork),如果出错直接打印错误信息并退出,简化代码逻辑。

5. 进程控制(Process Control)

这是 Shell Lab 的基础。

创建进程
  • pid_t fork(void)
    • 调用一次,返回两次。
    • 在父进程中返回子进程的 PID (>0)。
    • 在子进程中返回 0。
    • 出错返回 -1。
    • 关键特性:子进程获得父进程数据空间、堆、栈的副本(写时复制 Copy-on-Write),但共享代码段。
回收进程
  • pid_t waitpid(pid_t pid, int *statusp, int options)
    • 挂起当前进程,直到指定的子进程终止。
    • 防止僵尸进程(Zombie Process):已终止但未被父进程回收的进程。
    • WNOHANG 选项允许非阻塞等待。
加载并运行程序
  • execve(const char *filename, char *argv[], char *envp[])
    • 在当前进程上下文中加载并运行新程序。
    • 不创建新进程,而是覆盖当前进程的代码、数据和栈。
    • 只有执行失败才返回。
Shell 的工作原理

Shell就是一个循环:

  1. 打印提示符。
  2. 读取命令行。
  3. fork() 创建子进程。
  4. 子进程 execve() 运行命令。
  5. 父进程 waitpid() 等待子进程结束。

6. 信号(Signals)

信号是软件层面的异常,用于通知进程发生了某种事件。

基本概念
  • 发送信号:内核检测到事件(如 Ctrl+C -> SIGINT)或进程调用 kill()
  • 接收信号:目标进程更新 pending 位向量。
  • 处理信号:当进程从内核模式返回用户模式时,检查 pending 信号。如果有未屏蔽的信号,则跳转到信号处理程序(Signal Handler)
重要特性
  1. pending 位向量:每个信号对应一位。
  2. blocked 位向量:进程可以阻塞某些信号。
  3. 默认行为:终止、忽略、或停止(Stop)。
  4. 自定义处理:通过 signal()sigaction() 注册处理函数。
信号处理的陷阱(难点)
  • 处理程序与主程序并发:处理程序可能在主程序任何指令间中断执行。
  • 共享全局数据结构:如果处理程序和主程序都访问全局变量,必须小心竞态条件。
  • 非重入函数:信号处理程序中只能调用异步信号安全函数(如 write, _exit),不能调用 printf, malloc 等。
  • 阻塞信号:在处理某个信号时,同类型的信号默认会被阻塞,防止递归调用导致栈溢出。
常用函数
  • kill(pid, sig):发送信号。
  • alarm(secs):设置定时器,到期发送 SIGALRM。
  • sigprocmask():阻塞/解除阻塞信号集。
  • sigsuspend():原子性地解除阻塞并挂起进程,等待信号。这是编写健壮信号处理代码的关键。

7. 非本地跳转(Nonlocal Jumps)

C 语言提供的用户级 ECF 机制,类似于异常,但完全在用户空间完成。

  • setjmp(jmp_buf env):保存当前上下文(栈指针、PC 等)到 env,返回 0。
  • longjmp(jmp_buf env, int retval):恢复 env 中的上下文,使程序看起来像是从 setjmp 返回,但返回值为 retval

用途:深层嵌套函数中的错误恢复(类似 try-catch 的早期实现),避免层层返回错误码。

风险:可能导致资源泄漏(因为跳过了中间的清理代码)。


8. 操作进程的工具

  • /proc 文件系统:查看进程状态。
  • ps:列出进程。
  • top / htop:动态监控进程。
  • strace:跟踪系统调用。
  • gdb:调试进程,查看堆栈。

本章核心实验:Shell Lab

你需要编写一个简易 Shell(tsh),支持:

  • 前台/后台作业管理。
  • 内置命令(quit, jobs, fg, bg)。
  • 正确处理 SIGCHLD(子进程终止)、SIGINT(Ctrl+C)、SIGTSTP(Ctrl+Z)。
  • 难点:竞争条件处理。例如,在 fork 之后、exec 之前阻塞 SIGCHLD,确保 addjobdeletejob 之前执行。

总结

ECF 是连接硬件中断、操作系统内核和用户程序的桥梁。

  • 异常是硬件触发的 ECF。
  • 进程是操作系统利用 ECF 提供的并发抽象。
  • 信号是软件触发的 ECF,用于进程间通信和异步事件处理。

理解 ECF 对于编写健壮的并发程序、理解操作系统原理以及进行底层系统编程至关重要。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐