异常控制流
-
异常控制流(ECF)发生在计算机系统的各个层次
- 在硬件层,硬件检测到的事件会触发控制突然转移到异常处理程序。
- 在操作系统层,内核通过上下文转换将控制从一个用户进程转移到另一个用户进程。
- 在应用层,一个进程可以发送信号到另一个进程,而接收者会将控制突然转移到它的一个信号处理程序。
-
ECF是操作系统用来实现I/O、进程和虚拟存储器的基本机制。
-
应用程序通过使用陷阱(trap)或者系统调用的ECF形式,向操作系统请求服务。
-
ECF是计算机系统中实现并发的基本机制。
异常
-
异常就是控制流中的突变,用来响应处理器状态中的某些变化。
-
状态变化成为事件。
- 事件可能和当前指令的执行直接相关。
- 当处理器检测到有事件发生时,它会通过异常表的跳转表,进行一个间接过程调用,到一个专门涉及用来处理这类事件的操作系统子程序(异常处理程序)。
-
当异常处理程序完成处理后,根据引起异常的事件的类型,会发生以下三种情况的一种:
- 处理程序将控制返回给当前指令Icurr,即当事件发生时正在执行的指令。
- 处理程序将控制返回Inext,即如果没有发生异常将会执行的下一条指令。
- 处理程序终止被中断的程序。
异常处理
-
系统中可能的每种类型的异常都分配了一个唯一的非负整数的异常号。
- 包括被零除、缺页、存储器访问违例、断点以及算术溢出。
-
其他号码是由操作系统内核的设计者分配的。
- 包括系统调用和来自外部I/O设备的信号。
-
异常号是到异常表中的索引。
-
异常表的起始地址放在异常表机制寄存器的特殊CPU寄存器里。
-
过程调用时,在跳转到处理程序之前,处理器将返回地址压入栈中。根据异常的类型,返回地址要么返回当前指令,要么下一条指令。
-
处理器把额外的处理器状态压到栈中,在处理程序返回时,重新开始被终端的程序会需要这些状态。
-
如果控制从一个用户程序转移到内核,那么所有这些项目都被压倒内核栈中,而不是压到用户栈中。
-
异常处理程序运行在内核模式下,意味着它们对所有的系统资源都有完全的访问权限。
-
一旦硬件触发了异常,剩下的工作就是由异常处理程序在软件中完成。
异常的类别
- 异常的四种类别
- 中断
- 由异步发生,来自处理器外部的I/O设备的信号的结果。
- 总是返回到下一条指令
- 硬件中断的异常处理程序通常称为终端处理程序
- 陷阱
- 由同步发生,来自有意的异常
- 总是返回到下一条指令
- 在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
- 系统调用运行在内核模式中,内核哦屎允许系统调用执行指令,并访问定义在内核中的栈。
- 故障
- 由同步发生,来自潜在可恢复的错误
- 可能返回到当前指令
- 根据故障是否能够被修复,故障处理程序要么重新执行引起故障的指令,要么终止(abort)。
- 终止
- 由同步发生,来自不可恢复的错误
- 不会返回
- 直接返回给一个abort例程,该例程会终止这个应用程序。
- 中断
Linux/IA32
- IA32系统定义有高达256种不同的异常类型。
-
0~31的号码对应的是由Intel架构师定义的异常。
-
32~255的号码对应的是操作系统定义的终端和陷阱。
-
除法故障
- 当应用试图除以零时,或者当一个除法指令的结果对于目标操作数过大时,就会发生除法错误(异常0)。
- UNIX不会试图从除法错误中恢复,而是选择中止程序。
- Linux外壳通常会把除法错误报告为“浮点异常”
-
一般保护故障
- 因为一个程序引用了一个未定义的虚拟存储器区域,或者因为程序试图写一个只读的文本段。
- Linux不会尝试恢复这类故障,通常成为“段故障”
-
缺页
- 处理程序将磁盘上物理存储器相应的页面映射到虚拟存储器的一个页面,然后重新开始这条产生故障的指令。
-
机器检查
- 导致故障的指令执行中检测到致命的硬件错误时发生的。
-
Linux/IA32系统调用
- 我们将系统调用和它们相关联的包装函数称为系统级函数。
- 所有的到Linux系统调用的参数都是通过通用寄存器而不是栈传递的。
- IA32系统调用是通过一条成为int n的陷阱指令来提供的。
进程
-
异常是允许操作系统提供进程的概念所需要的基本构造块。
-
进程是一个执行中的程序的实例。
- 系统中的每个程序都是运行在某个进程的上下文中的。
- 上下文是由程序正确运行所需的状态组成的。
- 该状态包括存放在存储器中的程序和代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
-
进程提供给应用程序的关键抽象
- 一个独立的逻辑控制流,它提供一个假象。
- 一个私有的抵制空间,它提供一个假象。
逻辑控制流
- 用调试器单步执行程序,会看到一系列的程序计数器(PC),这个PC值的序列叫做逻辑控制流或者简称逻辑流。
并发流
- 一个逻辑流的执行在事件上与另一个流重叠,成为并发流,这两个流被成为并发地运行。
- 多个流并发地执行的一般现象成为并发。
- 一个进程和其他进程轮流运行的概念成为多任务(时间分片)。
- 一个进程执行它的控制流的一部分的每一事件段叫做时间片。
私有地址空间
- 进程为程序提供的假象,好像它独占的使用系统地址空间。一般而言,和这个空间中某个地址相关联的那个存储器字节是不能被其他进程读写的。
用户模式和内核模式
- 处理器通常是用某个控制寄存器中的一个模式位来提供这种功能的,该寄存器描述了进程当前享有的特权。
- 当设置了模式位时,进程就运行在内核模式中(超级用户模式)。
- 没有设置模式位时,进程就运行在用户模式中。
- 进程从用户模式变为内核模式的唯一方法是通过中断、故障或者陷入系统调用这样的异常实现的。
- Linux提供/proc文件系统,允许用户模式进程访问内和数据结构的内容。
上下文切换
- 上下文:内核重新启动一个被抢占的进程所需的状态
- 操作系统内核使用上下文切换的较高层形式的异常控制流来实现多任务。
- 内核为每个进程维持一个上下文。
- 在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,成为调度。
- 由内核中成为调度器的代码处理的。
- 使用上下文切换的机制来控制转移到新的进程
- 保存当前进程的上下文
- 恢复某个先前被抢占的进程被保存的上下文
- 将控制传递给这个新恢复的进程
系统调用错误处理
- 当Unix系统级函数遇到错误时,它们典型地会返回-1,并设置全局证书变量errno来表示什么出错了。
进程控制
获取进程ID
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); 返回调用进程的PID
pid_t getppid(void);返回父进程的PID(创建调用进程的进程)
- getpid和getppid函数返回一个类型为pid_t的整数值,在Linux系统上它在types.h中被定义为int
创建和终止进程
-
进程总是处于下面三种状态之一
- 运行
- 停止:被挂起且不会被调度
- 终止:进程永远停止
- 终止原因:
- 收到一个信号,该信号的默认行为是终止进程
- 从主程序返回
- 调用exit函数
- 终止原因:
-
父进程通过调用fork函数创建一个新的运行子进程:
#include <sys/types.h> #include <unistd.h> pid_t fork(void);
-
子进程返回0,父进程返回子进程的PID,如果出错,则为-1.
-
父进程和新创建的子进程之间最大的区别:
- 它们有不同的PID
-
fork函数只被调用一次,却返回两次。
- 一次是在父进程中
- 一次是在新创建的子进程中
- 在父进程中,fork返回子进程的PID
- 在子进程中,fork返回0.
- 子进程的PID总是非零的。
-
fork函数的并发执行
- 父进程和子进程是并发运行的独立进程
-
fork函数的相同的但是独立的地址空间
-
fork函数的共享文件
-
调用fork函数n次,产生2的n次方个进程
回收子进程
-
一个终止了但还未被回收的进程称为僵死进程。
-
如果父进程没有回收它的僵死子进程就终止了,那么内核就会安排init进程来回收它们。
-
一个进程可以通过调用waitpid函数来等待它的子进程终止或者停止。
- 成功返回子进程PID,如果WNOHANG,返回0,其他错误返回-1.
-
判定等待集合的成员
- 如果pid>0,那么等待集合就是一个单独的子进程,它的进程ID等于pid。
- 如果pid=-1,那么等待集合就是由父进程所有的子进程组成的。
-
修改默认行为
- 可以通过将optioins设置为常量WNOHANG和WUNTRACED的各种组合。
-
WNOHANG:如果等待集合中的任何子进程都还没有终止,那么就立即返回(返回值为0)。默认的行为是挂起调用进程,直到有子进程终止。
-
WUNTRACED:挂起调用进程的执行,直到等待集合中的一个进程变成已终止或者被停止。返回的PID为导致返回的已终止或被停止子进程的PID。默认的行为是只返回已终止的子进程。
-
WNOHANG|WUNTRACED:i级返回如果等待集合中没有任何子进程被停止或已终止的子进程。检查已终止和被停止的子进程时,该选项会有用。
-
- 可以通过将optioins设置为常量WNOHANG和WUNTRACED的各种组合。
-
检查已回收子进程的退出状态
- wait.h头文件定义了结束status参数的几个宏:
- WIFEXITED:如果子进程通过调用exit或者一个返回正常终止,就返回真。
- WEXXITSTATUS:返回一个正常终止的子进程的退出状态。只有在WIFEXITED返回为真时,才会定义这个状态。
- WIFSIGNALED:如果子进程时因为一个未被捕获的信号终止的,那么就返回真。
- WTERMSIG:返回导致子进程终止的信号的数量。只有在WIFSIGNALED返回为真时,才定义这个状态。
- WIFSTOPPED:如果引起返回的子进程当前是被停止的,那么就返回真。
- WSTOPSIG:返回引起子进程停止的信号的数量。只有在WIFSTOPPED返回为真时,才定义这个状态。
- wait.h头文件定义了结束status参数的几个宏:
-
错误条件
- 如果调用进程没有子进程,那么waitpid返回-1,并且设置errno为ECHILD。
- 如果waitpid函数被一个信号中断,那么它返回-1,并设置errno为EINTR。
wait函数
- 其是waitpid函数的简单版本
- 调用wait(&status)等价于调用waitpid(-1,&status,0)
让进程休眠
- sleep函数将一个进程挂起一段指定的时间
- 如果请求的时间量已经到了,sleep返回0,否则返回还剩下的要休眠的秒数。
- pause函数让调用函数休眠,直到该进程收到一个信号。
加载并运行程序
- execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。
- 只有出现错误时,execve才会返回到调用程序。
- 调用一次并从不返回。
- getenv函数在环境数据中搜索字符串"name=value",如果找到了,它就返回一个指向value的指针,否则它就返回NULL。
- 如果环境数据包含一个形如“name=oldvalue”的字符串,那么unsetenv会删除它,而setenv会用newvalue代替oldvalue,但是只有在overwrite非零时才会这样。如果name不存在,那么setenv就把“name=newvalue”添加到数组中。
信号
- Unix信号:更高层的软件形式的异常允许进程中断其他进程。
信号术语
-
传递一个信号到目的进程的两个步骤:
- 发送信号
- 接收信号
-
发送信号的原因:
- 内核检测到一个系统事件
- 一个进程调用了kill函数,显式的要求内核发送一个信号给目的进程。
-
一个进程可以发送信号给它自己。
-
接收信号:
- 1.忽略
- 2.终止
- 3.执行信号处理程序,捕获信号
-
待处理信号:
- 只发出没有被接收的信号
- 任何时刻,一种类型至多只会有一个待处理信号,多的会被直接丢弃
- 一个进程可以选择性的阻塞接受某种信号,被阻塞仍可以被发送,但是不会被接收
- 一个待处理信号最多只能被接收一次。
- pending:待处理信号集合
- blocked:被阻塞信号集合。
发送信号——基于进程组
-
进程组
- 每个进程都只属于一个进程组。
- 进程组ID:正整数
- 一个子进程和他的父进程属于同一进程组。
- 查看进程组id:getpgrp
- 修改进程组:setpgid
-
用/bin/kill程序发送信号
-
/bin/kill程序可以向另外的进程发送任意的信号,格式是:
-
/bin/kill -n m
-
n是信号,m是进程或进程组
-
当n>0时,发送信号n到进程m
-
当n<0时,使信号|n|发送到进程组m中的所有进程。
-
-
从键盘发送信号
- 在键盘输入ctrl-c会导致一个SIGINT信号被发送到外壳。
- 外壳捕获该信号,然后发送SIGINT信号到这个前台进程组中的每个进程。
- 在默认情况下,结果是终止前台作业。
- 输入ctrl-z会发送一个SIGTSTP信号到外壳,外壳捕获这个信号,并发送SIGTSTP信号给前台进程组中的每个进程。
- 在默认情况下结果时停止前台作业。
-
用kill函数发送信号
- 进程通过调用kill函数发送信号给其他进程(包括它们自己)
-
用alarm函数发送信号
- 进程可以通过调用alarm函数向它自己发送SIGALRM信号。
-
接收信号
-
信号类型的预定义的默认行为:
- 进程终止
- 进程终止并转储存储器
- 进程停止直到被SIGCONT信号重启
- 进程忽略该信号
-
signal函数通过下列三种方法来改变和信号signum相关联的行为:
- 如果handler是SIG_IGN,那么忽略类型为signum的信号。
- 如果handler是SIG_DFL,那么类型为signum的信号行为恢复为默认行为。
- 否则,handler就是用户定义的函数的地址,这个函数称为信号处理程序,只要进程接收到一个类型为signum的信号,就会调用这个程序。
- 通过把狐狸程序的抵制传递到signal函数从而改变默认行为,称为设置信号处理程序。
- 调用信号处理程序称为捕获信号
- 执行信号处理程序称为处理信号
-
信号处理问题
* 待处理信号被阻塞
* 带处理信号不会排队等待
* 系统调用可以被中断
* 不可以用信号来对其他进程中发生的事件计数
可移植的信号处理
- Signal包装函数设置了一个信号处理程序,其信号处理语义如下:
- 只有这个处理程序当前正在处理的那种类型的信号被阻塞。
- 和所有信号实现一样,信号不会排队等待。
- 只要可能,被中断的系统调用会自动重启。
- 一旦设置了信号处理程序,它就会移至保持,直到Signal带着handler参数为SIG_IGN或者SIG_DFL被调用。
显式地阻塞和取消阻塞信号 p517
- sigprocmask函数改变当前已阻塞信号的集合,具体行为依赖于how值:
- SIG_BLOCK:添加set中的信号到blocked中(blocked = blocked | set)
- SIG_UNBLOCK:从blocked中删除set的信号(blocked = blocked &~ set)
- SIG_SETMASK:blocked = set
- 如果oldset非空,blocked位向量以前的值会保存在oldset中。
非本地跳转
-
用户级的异常控制流形式,通过setjmp和longjmp函数提供。
-
setjump函数在env缓冲区中保存当前调用环境,以供后面longjmp使用,并返回0.
-
调用环境:程序计数器,栈指针,通用目的寄存器
-
longjmp函数从env缓冲区中恢复调用环境,然后触发一个从最近一次初始化env的setjmp调用的返回。然后setjmp返回,并带有非零的返回值retval。
-
setjmp函数只被调用一次,但返回多次;
-
longjmp函数被调用一次,但从不返回。
操作进程的工具
- STRACE:打印一个正在运行的程序和他的子程序调用的每个系统调用的痕迹
- PS:列出当前系统中的进程,包括僵死进程
- TOP:打印出关于当前进程资源使用的信息
- PMAP:显示进程的存储器映射
参考文献
1.《Computer.Systems.A.Programmer's.Perspective.2nd.CN》教材
2.《linux的中断和异常》 http://blog.chinaunix.net/uid-20556798-id-176960.html
心得体会
后面的内容真的是很难理解,花了那么多的时间看也没有看太清楚,还是需要通过视频来学习一下,看下是否能够加深一点理解。