《深入理解计算机系统》 异常控制流
2026/7/13 17:52:42 网站建设 项目流程

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


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

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

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

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

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


2. 异常(Exceptions)

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

异常的四种类型
类型英文特点返回行为例子
中断Interrupt异步,来自外部 I/O 设备返回下一条指令键盘输入、网卡数据包到达
陷阱Trap同步,有意为之的系统调用返回下一条指令syscallfork,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 对于编写健壮的并发程序、理解操作系统原理以及进行底层系统编程至关重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询