它打破了我们在第 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某些情况) |
异常处理流程
- 当前指令执行完毕(或出错)。
- 硬件识别异常号kkk。
- 间接调用异常表中第kkk项指向的处理程序。
- 处理程序执行(通常在内核模式)。
- 根据异常类型,返回到被中断的程序(下一条指令或当前指令 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就是一个循环:
- 打印提示符。
- 读取命令行。
fork()创建子进程。- 子进程
execve()运行命令。 - 父进程
waitpid()等待子进程结束。
6. 信号(Signals)
信号是软件层面的异常,用于通知进程发生了某种事件。
基本概念
- 发送信号:内核检测到事件(如 Ctrl+C -> SIGINT)或进程调用
kill()。 - 接收信号:目标进程更新 pending 位向量。
- 处理信号:当进程从内核模式返回用户模式时,检查 pending 信号。如果有未屏蔽的信号,则跳转到信号处理程序(Signal Handler)。
重要特性
- pending 位向量:每个信号对应一位。
- blocked 位向量:进程可以阻塞某些信号。
- 默认行为:终止、忽略、或停止(Stop)。
- 自定义处理:通过
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,确保addjob在deletejob之前执行。
总结
ECF 是连接硬件中断、操作系统内核和用户程序的桥梁。
- 异常是硬件触发的 ECF。
- 进程是操作系统利用 ECF 提供的并发抽象。
- 信号是软件触发的 ECF,用于进程间通信和异步事件处理。
理解 ECF 对于编写健壮的并发程序、理解操作系统原理以及进行底层系统编程至关重要。