深入理解计算机系统 第八章–异常控制
异常
异常是异常控制流的一种形式,一部分由硬件,一部分由操作系统实现。
异常就是控制流中的突变,以相应处理器状态的一些变换。
当异常处理程序完成后,会发生下面三种情况中的一种:
- 处理程序将控制返回给当前质量
- 将控制返回给下一条指令
- 终止被中断的程序
异常处理
系统为每个类型的异常分配了异常号。系统启动时候,操作系统会初始化一张异常表,其中包含了异常k以及对应处理异常的地址。当我们遇到某个异常时候,确定对应异常号k,就可以通过异常表基地址寄存器+异常号来查询异常表获取地址。
异常类似于过程调用,但是有一些不同:
-
过程调用会将返回地址压如栈,而异常则不一定(上述三种情况)
-
异常处理运行在内核模式
-
如果控制从用户程序转移至内核,这些项目都被压到内核栈
-
处理器会把一些额外状态压进栈中
异常四种类型
-
中断(异步)
-
陷阱(同步)
-
故障(同步)
-
终止(同步)
Linux系统调用所有参数都是使用通用寄存器而不是栈传递。
进程
异常是允许操作系统内核提供进程概念的基本构造块
进程—一个执行中程序的实例。他提供了应用程序的关键抽象:
- 一个独立逻辑控制流:好像程序独占处理器
- 一个私有地址空间:好像程序独占系统内存
实现方式:并发+时间切片+虚拟内存
代码段总是从0x00400000开始。
用户模式和内核模式
内核模式可以执行指令集的任何指令,可以访问系统的任何内存位置
用户模式不允许执行特权执行,只能访问自己的虚拟内存来简介访问真实地址空间的数据
可以通过系统调用syscall,异常处理使得用户态切换到内核态
上下文切换
内核为每个进程维持一个上下文。上下文即:内核重新启动一个被抢占的内存所需状态。
当内核抢占了进程A调度了进程B,就使用了上下文切换来实现。这个操作保存了当前进程的上下文,之后可以恢复先前被抢占进程的上下文,最后将控制传递给恢复的进程。
对于一些较长时间的中断如磁盘读取数据,内核可以先使得需要读数据的进程A切换到进程B,以避免在这段时间内什么都不做。
进程控制
进程的三种状态:
- 运行
- 停止
- 终止
父进程通过调用fork函数来创建一个新运行的子进程
fork在父进程中返回子进程的pid,在子进程中返回0,通过此可以判断是父进程还是子进程。
fork之后,父进程和子进程是并发执行的。他们有着相同但是独立的地址空间。子进程会继承父进程打开的所有文件。
回收子进程
一个子进程终止后会被父进程回收,一个已经终止但还没回收的进程成为僵尸进程。如果长时间不回收僵尸进程将浪费系统的内存资源。
如果一个父进程终止了,但是子进程还没被回收,这些子进程就是孤儿进程,会被init进程领养,被init回收。
一些函数
一个进程可以使用waitpid来等待将子进程终止或是停止。(简化版为wait)
进程休眠与暂停:sleep() pause()
可以使用execve()函数在当前进程的上下文中加载并运行一个新的程序。
fileName为可执行文件,argv为参数列表,envp为环境变量数组
通常使用的方法是fork一个子进程,在子进程中使用execve函数
信号
信号可以通知进程系统发生的一个某种类型事件。
一个发送了,但是还没有被接收的信号称作待处理信号。任何时刻一种类型只能有一种待处理信号。重复的待处理信号会被丢弃。
发送信号的方式有很多,使用函数,键盘控制(ctrl+c),使用/bin/kill等。在此不详细说明了
每个信号类型都有一个预定义的默认行为
- 进程终止
- 进程终止并转储内存
- 进程挂起,直至被SIGCONT信号重启
- 忽略信号
可以通过这个函数来修改接收到对应信号的行为。但是SIGSTOP和SIGKILL信号的默认行为是不能修改的。
信号隐式阻塞 内核默认阻塞任何当前处理程序正在处理信号类型的待处理信号。
信号显式阻塞 使用sigpromask函数以及相关函数,明确地阻塞和接触阻塞选定的信号。
当父进程在等待子进程结束时候,使用while(!pid) sleep(1)这样的语句会浪费资源,合适的方法是使用sigsuspend.
非本地跳转
将控制直接从一个函数转移到另一个函数,而不需要经过正常的调用返回程序。
通过setjump和longjump实现。
Longjump会导致一些意外的后果,如果中间函数调用了某些数据结构,本应在函数结尾时候释放,那么这些释放代码就会被longjump导致跳过,从而产生内存泄露。
非本地跳转一个应用是使一个信号处理程序分支到一个特殊的代码位置,而不是返回到被信号到达中断了的指令位置。