异常控制流
异常
异常就是控制流中的突变,用来响应处理器状态终端某些变化。
当处理器状态中发生一个重要的变化时,处理器正在执行某个当前指令,在处理器中状态被编码为不同的位和信号。状态变化称为事件。
在任何情况下,当处理器检测到有事件发生时,它就会通过一张叫做异常表的跳转表,进行一个间接跳转过程调用,到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序)。当异常处理程序完成处理后,会根据引起异常的事件的类型,会发生以下3种情况中的一种:
- 处理程序将控制返回给当前指令,即当事件发生时正在执行的指令。
- 处理程序将控制返回给下一条指令,即如果没有法伤异常将会执行的下一条指令。
- 处理程序终止被中断的程序。
系统种可能的每种类型的异常都分配了一个唯一的非负整数的异常号,操作系统分配和初始化一张称为异常表的跳转表,使得表目k包含异常k的处理程序的地址。异常表的起始地址放在一个叫做异常表基址寄存器的特殊CPU寄存器里。
在处理程序处理完事件之后,它通过执行一条特殊的“从中断返回‘指令,可选的返回到被中断的程序,该指令将适当的状态弹回到处理器的控制和数据寄存器中,如果异常中断的是一个用户程序,就将状态回复位用户模式,然后将控制返回给被中断的程序。
异常可以分为4个类别:中断(interrupt)、陷阱(trap)、故障(fault)和终止(abort),下表是这几个类别的异常的属性:
进程
系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
进程提供给应用程序的关键抽象:
- 一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占的使用处理器。
- 一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用内存系统。
处理器通常是用某个控制寄存器中的一个模式位来限制一个应用可以执行的指令以及它可以访问的地址空间范围,这个寄存器描述了进程当前享有的特权。
当设置了模式位时,进程就运行在内核模式中。一个运行在内核模式下的进程可以执行指令集中的任何指令,并且可以访问系统中任何内存位置。
没有设置模式位时,进程就运行在用户模式下。用户模式中的进程不允许执行特权指令,比如停止处理器、改变模式位,或者发起一个I/O操作。也不允许用户模式中的进程直接引用地址空间中内核去内的代码和数据。任何这样的尝试都会导致致命的保护故障。反之,用户程序必须通过系统调用接口间接地访问内核代码和数据。
内核位每个进程维持一个上下文,上下文就是内核重新启动一个被抢占的进程所需的状态。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度,是由内核中称为调度器的代码处理的。
上下文切换:1)保存当前进程的上下文,2)恢复某个先前被抢占的进程被保存的上下文,3)将控制传递给这个新回复的进程。
当内核代表用户执行系统调用时,可能会发生那个上下文切换。如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程。此外,中断也可能引发上下文切换。
进程控制
获取进程ID
每个进程都有一个唯一的整数进程ID(PID)。getpid返回调用进程的PID;getppid函数返回它的父进程的PID(创建调用进程的进程)。
创建和终止进程
从程序员的角度,我们可以认为进程总是出与下面三种状态之一:
- 运行。进程要么在CPU上执行,要么在等待被执行且最终会被内核调度。
- 停止。进程的执行被挂起,且不会被调度。当收到SIGSTOP、SIGTSTP、SIGTTIN或者SIGTTOU信号时,进程就停止,并且保持停止直到它收到一个SIGCONT信号,在这个时刻,进程再次开始运行。
- 终止。进程永远的停止了。进程会因为3个原因终止:1)收到一个信号,该信号的默认行为是终止进程,2)从主程序返回,3)调用exit函数。
exit函数已status推出状态来终止进程(另一种设置推出状态的方法是从主程序中返回一个整数值)
父进程通过调用fork函数创建一个新的运行的子进程。
父进程和新建的子进程之间最大的区别在于它们有不同的PID。fork函数只被调用一次,却会返回两次:一次是在调用进程(父进程)中,一次是在新创建的子进程中。在父进程中,fork函数返回子进程的PID。在子进程中,fork函数返回0。因为子进程的PID总是为非零,返回值就提供一个明确的方法来分辨程序是在父进程还是在子进程中执行。
此外,fork函数创建的子进程和父进程之间还有以下关系:
- 并发执行。父进程和子进程是并发运行的独立进程。内核能够以任意方式交替执行它们的逻辑控制流中的指令。
- 相同但是独立的地址空间。父进程和子进程由相同的用户栈、相同的本地变量值、相同的堆、相同的全局变量值,以及相同的代码。但是,因为父进程和子进程是独立的进程,它们都有自己的私有地址空间,所以它们对某一个变量的任何修改都是独立的,不会反映在另一个进程的内存中。
- 共享文件。子进程会继承父进程所有的打开文件。
进程图
回收子进程
当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反进程会被保持在一种已终止的状态中,直到被它的父进程回收。当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程,从此时开始,该进程就不存在了。一个终止了但还未被回收的进程称为僵尸进程。
如果一个父进程终止了,内核会安排init进程成为它的孤儿进程的养父。一个进程可以通过调用waitpid函数来等待它的子进程终止或者停止。
默认情况下,waitpid挂起调用进程的执行,直到它的等待集合中的一个子进程终止。如果等待集合中的一个进程在刚调用的时刻就已经终止了,那么waitpid就立即返回。在这两种情况中,waitpid返回导致waitpid返回已经终止子进程的PID。此时,已终止的子进程已经被回收,内核会从系统中删除掉它的所有痕迹。
等待集合的成员是由参数PID确定的:
修改默认行为,可以通过修改参数options来修改默认行为:
wait函数,时waitpid函数的简单版本:
调用wait(&status)等价于调用waitpid(-1,&status,0)。
让进程休眠
sleep函数将一个进程挂起一段指定的时间。
如果请求的时间量已经到了,sleep返回0,否则返回还剩下的要休眠的秒数。后一种情况是可能的,如果因为sleep函数被一个信号中断而过早地返回。
另一个函数是pause函数,该函数让调用函数的进程休眠,直到该进程收到一个信号。
加载并运行程序
execve函数在当前进程的上下文中加载并运行一个新程序。
execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。所以,与fork函数一次调用返回两次不同,execve调用一次并从不返回。
信号
一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的事件。
发送信号:
内核通过更新目的进程上下文中的某个状态,发送一个信号给目的进程。发送信号可以由两个原因1)内核检测到一个系统事件,比如除零错误或者子进程终止。2)一个进程调用了kill函数,显示的要求内核发送一个信号给目的进程。一个进程可以发送信号给它自己。
一个发出而没有被接收的信号叫做待处理信号,在任何实可,一种类型至多只会有一个待处理信号。
当一种信号被阻塞时,它仍可以被发送,但是产生的待处理信号不会被接收,直到进程取消对这种信号的阻塞。
- 使用/bin/kill程序发送信号
比如:/bin/kill -9 15213 发送信号9(SIGKILL)给进程15213。
/bin/kill -9 -15213发送一个SIGKILL给进程组15213中的每个进程。
- 使用键盘发送信号
比如:在键盘上输入Ctrl+C会导致内核发送一个SIGINT信号到前台进程组中的每个进程。默认情况下,结果是终止前台作业。
- 用kill函数发送信号
其原理与/bin/kill类似。
- 用alarm函数发送信号
进程可以通过调用alarm函数向它自己发送SIGALRM信号。
alarm函数安排内核在secs秒后发送一个SIGALRM信号给调用进程。如果secs是零,那么不会调度安排新的闹钟。
接收信号:
当目的进程被内核强迫以某种方式对信号的发送做出反应时,它就接收了信号。进程可以忽略这个信号,终止或者通过执行一个称为信号处理程序的用户层函数捕获这个信号。
一个待处理信号最多只能被接收一次。
每个信号类型都有一个预定义的默认行为,是下面的一种:
- 进程终止
- 进程终止并转出内存
- 进程停止(挂起)直到被SIGCONT信号重启
- 进程忽略信号
阻塞和解除阻塞
隐式阻塞机制。内核默认阻塞任何当前处理程序正在处理信号类型的待处理的信号。
显示阻塞机制。应用程序可以使用sigprocmask函数和它的辅助函数,明确地阻塞和解除阻塞选定的信号。
非本地跳转
C语言提供了一种用户及异常控制流形式,称为非本地跳转(nonlocal jump),它将控制直接从一个函数转移到另一个当前正在执行的函数,而不需要经过正常调用-返回序列。非本地跳转是通过setjump和longjump函数来提供的。
setjump函数在env缓冲区中宝尊当前调用环境,以供后面的longjump使用,并返回0。调用环境包括程序计数器、栈指针和通用目的寄存器。
setjump函数的返回值不能被赋给变量。但是可以安全地用在switch或条件语句的测试中。
longjump函数从env缓冲区中恢复调用环境,然后触发一个从最近一次初始化env的setjump调用的返回。然后setjump返回,并带有非零的返回值retval。
setjump函数只被调用一次,但返回多次:一次是当第一次调用setjump,而调用环境保存在缓冲区env中时,一次是为每个相应的longjump调用。另一方面,longjump函数被调用一次,但从不返回。
非本地跳转的一个重要应用就是允许从一个深层嵌套的函数调用中立即返回,通常是有检测到某个错误情况引起的。