异常控制流
异常控制流
定义
一系列指令组成的序列称为控制流,现代系统通过控制流发生突变对系统状态变化作出反应,这些突变称为异常控制流(ECF)。
异常控制流发生在系统各个层次,操作系统,硬件层,应用层。
理解 ECF 的重要性;
- 有助理解应用程序如何与操作系统交互(陷阱或系统调用)
- 有助理解并发
- 有助理解异常如何工作
类型
| 类别 | 原因 | 异步/同步 | 返回行为 |
|---|---|---|---|
| 中断 | 来自 IO设备的信号 | 异步 | 总是返回下一条指令 |
| 陷阱 | 有意识的异常 | 同步 | 总是返回到下一条指令 |
| 故障 | 潜在可恢复的错误 | 同步 | 可能返回当前指令 |
| 终止 | 不可恢复的错误 | 同步 | 不会返回 |
中断不是从任何一个专门的指令造成的,从这个意义上它是异步的;所以同步是执行一条指令的结果,所以称为同步的???
中断
来自处理器外部的IO设备的信号,是唯一的异步发生的异常,硬件中断的异常处理程序通常称为中断处理程序。
过程:硬件先产生信号,比如网络适配器,硬盘控制器等,它们向处理器芯片的一个引脚发送信号,将异常号放在系统总线上来触发中断。处理器感知到引脚的电压变化后就去系统总线读取异常号,并调用相应的中断处理程序,处理完成后就将控制返回到下一条本来要执行的指令
陷阱
是有意的异常,其最重要的作用是在用户程序和内核之间提供一个像过程一样的接口,即系统调用。陷阱允许了用户程序对内核服务的受控访问。
过程:处理器提供一条特殊的 syscall n 的指令来请求服务,执行 syscall 指令会引起一个转到异常处理器的陷阱,这个处理程序对参数解码并调用适当的内核程序。从程序员的角度看,系统调用和普通函数调用没什么区别。
但是普通函数是运行在用户模式的,他们使用与调用函数相同的栈,而系统调用运行在内核模式,允许系统调用执行指令,并访问定义在内核的栈。
常见的陷阱/系统调用如下:结束进程(exit),创建进程(fork),读/写/打开/关闭文件,等待子进程结束(waitpid)等等
故障
故障(fault)由错误引起,可能能够被故障处理程序修正,即潜在可恢复的错误。
过程:当故障发生时,处理器将控制转移给故障处理程序,如果能修正这个错误,将返回到引起故障的指令而重新执行它,如果不能修正则返回到内核的abort 例程。
常见的故障是缺页异常
终止
终止(abort)是不可恢复的致命错误造成的,通常是一些硬件错误,比如RAM位损坏时发生的奇偶校验错误。终止处理程序不再将控制返回给应用程序,而是直接终止其运行。

异常处理
- 为每个异常分配了一个非负的 异常号(exception number)
- 一些号码由处理器设计者分配
- 其他号码由操作系统内核的设计者分配
- 系统启动时,操作系统分配和初始化一张称为 异常表 的跳转表。
- 条目 k 包含异常 k 的处理程序的地址
异常表的地址放在叫异常表基址寄存器的特殊CPU寄存器中- 异常类似过程调用,不过有以下不同
- 过程调用,跳转到处理程序前,处理器将返回地址压入栈中。对于异常,返回地址是当前,或下一跳指令
- 处理器会把额外的处理器状态压入栈中
- 如果控制一个用户程序到内核,那么所有这些项目会被压入内核栈中,而是用户栈
- 异常处理程序运行在内核模式下,这意味他们对所有系统资源有完整访问权限
进程
一个执行中的程序的实例,系统的每个程序都运行在某个进程的上下文中的。上下文包含程序正确运行所需的状态,这些状态包括在存储器中的程序代码和数据,栈,通用寄存器的内容,程序计数器,环境变量以及打开的文件描述符的集合。
进程为程序提供了两个关键的抽象:
- 独立的逻辑控制流
- 私有的地址空间
逻辑控制流
PC值的序列叫做逻辑控制流,简称逻辑流。它为每个程序提供了独占使用处理器的假象。一个逻辑流执行与另一个相重叠时称为并发流,这种现象叫并发(concurrency)。
- 一个进程和其他进程轮流执行的概念称为
多任务。 - 一个进程执行它的控制流的一部分的每一时间段叫做
时间片。 - 并行流是并发流的一个真子集,两个流并发地运行在不同的处理器核或者计算机上,我们称为
并行流。(parallel)
在私有地址空间
进程 为每个程序提供了独占使用系统地址空间的假象
一个
进程为每个程序提供它自己的私有地址空间。不同系统一般都用相同的空间结构。

用户态和内核态的切换
处理器用某个控制寄存器中的一个模式位控制进程的运行模式
- 当设置了这个位时,进程运行在内核模式,此时可以执行任何指令访问任何存储器位置。
- 否则运行在用户模式,不允许执行特权指令,只能访问私有空间的数据。运行在用户模式的进程从用户模式转变为内核模式的唯一方法是通过异常。
Linux提供一种聪明的机制,叫 /proc 文件系统
- 允许用户模式访问内核数据结构的内容
/proc文件将许多内核数据结构输出为一个用户程序可以读的文本文件的层次结构/proc目录下的文件包含一般的系统属性,如CPU信息(/pro/cpuinfo),或者某个进程使用存储器的映射/sys文件系统提供关于系统总线的额外的底层信息
上下文切换
什么是上下文切换?
- 上下文就是重新启动一个被抢占的进程所需的状态。(包括通用寄存器,浮点寄存器,PC,用户栈,状态寄存器,内核栈,内核数据结构)
- 内核为每个进程维护一个上下文。
- 操作系统内核使用一种称为上下文切换的 较高层次 的异常控制流来实现多任务。
什么时候会发生上下文切换?
- 内核代表用户执行系统调用
- 如果系统调用因为某个事件阻塞,那么内核可以让当前进程休眠,切换另一个进程。
- 或者可以用
sleep系统调用,显式请求让调用进程休眠。 - 即使系统调用没有阻塞,内核可以决定执行上下文切换
- 中断也可能引发上下文切换。
- 所有系统都有某种产生周期性定时器中断的机制,典型为1ms,或10ms。
- 每次定时器中断,内核就能判断当前进程运行了足够长的时间,切换新的进程。
进程控制
getpid()返回调用进程的PID,getppid()返回它的父进程的PID。
先了解进程的三种状态:
- 运行。进程要么在CPU上执行,要么在等待执行且最终会被内核调度
- 停止。进程的执行被挂起(suspend),且不会被调度。直到它收到一个 SIGCONT 信号会再次开始执行
- 终止。进程永远地停止了。进程终止有三种原因:收到终止进程的信号;从主程序返沪;调用了exit函数
创建和终止,回收进程
- 创建:父进程通过调用
fork函数创建一个新的运行子进程pid_t fork(void);这个函数很奇妙,会有两个返回值,子进程返回0,父进程返回子进程的PID,如果出错,返回-1- 子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份拷贝
- 父进程和新创建的子进程之间最大的区别在于有不同的PID
- 子进程继承了父进程所有打开的文件
- 创建了子进程后,内核可以以任何方式交替执行他们的逻辑控制流中的指令,下面这张图对进程的理解很有帮助

- 终止:
void exit(int status); - 回收:当一个进程由于某种原因终止时,内核并不是立即把它从系统清除,而是保持一种已终止的状态,知道被它的父进程回收(reap)。终止了但是还没有回收的进程称为僵死进程(zombie)
- 当父进程回收已终止的进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程
- 如果父进程不回收僵死进程,内核会安排init进程(PID为1的系统进程)进行回收操作
- 父进程进行回收的函数
pid_t waitpid(pid_t pid, int *statud, int options),第一个参数指定等待的子进程的PID;第二个参数是是一个指针,用于返回子进程终止的状态码(exit(status));第三个参数是对默认行为的修改,waitpid默认的行为是挂起调用进程等待子进程终止。参数可以参考书中P496
其他进程的相关函数,调用时要加载 <unistd.h> 库,不细说了 P497后面几页有
- 休眠
unsigned int sleep (unsigned int secs);,int pause(void); - 加载与执行
int execve(const char *filename,const char *argv[],const char *envp[]); - fork与execve不同,fork是在新的子进程中运行相同的程序,子进程是父进程的一个复制品,execve 则是在当前进程的上下文加载并运行一个新的程序,它会覆盖当前的地址空间,而不是创建一个新的进程
信号
一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的事件。
发送
内核通过更新目的进程上下文中的某个状态,发送一个信号给目的进程。发送信号可以有如下两种原因:
1)内核检测到一个系统事件,比如被零除错误或者子进程终止。
2)一个进程调用 kill 函数,显式地要求内核发送一个信号给目的进程。一个进程可以发送信号给它自己。
接受
当目的进程被内核强迫以某种方式对信号的发送做出反应时,目的进程就接收了信号。进程可以忽略这个信号,终止或者通过执行一个称为信号处理程序(signal handler)的用户层函数捕获这个信号
处理
一个只发出而没有被接收的信号叫做待处理信号(pending signal)。在任何时刻,一种类型至多只会有一个待处理信号。如果一个进程有一个类型为 k 的待处理信号,那么任何接下来发送到这个进程的类型为 k 的信号都不会排队等待,它们只是被简单地丢弃。一个进程可以有选择地阻塞接收某种信号。当一种信号被阻塞时,它仍可以被发送,但是产生的待处理信号不会被接收,直到进程取消对这种信号的阻塞。
一个待处理信号最多只能被接收一次。内核为每个进程在 pending 位向量中维护着待处理信号的集合,而在 blocked 位向量中维护着被阻塞的信号集合。只要传送了一个类型为 k 的信号,内核就会设置 pending 中的第 k 位,而只要接收了一个类型为 k 的信号,内核就会清除 pending 中的第 k 位。
不同系统之间,信号处理语义的差异是 Unix 信号处理的一个缺陷。为了处理这个问题,Posix 标准定义了
sigaction函数,它允许用户明确指定他们想要的信号处理语义。使用
sigprocmask函数可以显示的阻塞信号
非本地跳转
C语言提供了一种用户级异常控制流方式,称为非本地跳转(nonlocal jump),它将控制直接从一个函数转移到另一个当前正在执行的函数,而不需要经过正常的调用-返回序列。非本地跳转是通过 setjmp 和longjmp 函数来提供的。
小结
主要三部分,异常控制流,进程和信号。
异常控制流通过四种类型的异常来提供进程的并发机制和进程与系统的交互,对进程间消息的传递需要信号的支持。
进程提供了两种重要的抽象:逻辑控制流和私有地址空间。