第8章 异常控制流

8.1 异常

  • 所谓异常就是控制流过程中的突变,用来响应处理器状态中的某些变化
  • 处理器的状态的变化称为事件(event),事件可能和当前指令的执行相关(虚拟存储器缺页,算术溢出,除数为零),也可能没有关系(定时器信号,I/O请求完成);
  • 当有异常发生时,处理器会通过异常表(exception table),进入异常处理程序(exception handler)。
  • 通常异常处理程序处理完成之后会有三种情况:
    • 处理程序将控制权返回给当前指令
    • 处理程序将控制权返回给下一条指令
    • 处理程序终止被中断的程序
8.1.1 异常处理
  • 系统中可能的每种异常都分配了一个唯一的非负整数的异常号(exception number),一部分由处理器设计这分配(被零除、缺页、存储器访问违例、断点、算术溢出),一部分由操作系统内核设计者分配(系统调用和I/O设备的信号);
  • 在启动系统是,操作系统维护了一个异常表,此异常表为一个跳转表;异常表的起始地址存储在异常表基址寄存器(exception table base register);

  • 异常处理程序调用和一般的过程调用的不同之处
    • 一般过程调用时会将返回地址压入栈中,而异常返回时要么返回当前指令,要么是下一条指令
    • 处理会把一些处理器的状态压入栈中,返回时重新恢复在这些状态;
    • 当控制从用户程序转移到内核时,所有这些项目都会被压入到内核的栈中,而不是用户的栈;
    • 异常处理程序运行在内核的模式下,这意味着对所有的系统资源有完全的访问权限;
8.1.2 异常的类别

异常可以为四种:中断(interrupt)、陷阱(trap)、故障(fault)、终止(abort)

  • 中断(中断是异常的一种,异常不等于中断)
    • 中断是异步发生的,是来自处理器外部I/O设备信号的结果,而不是由任何一条指令造成的(异步);
    • 中断的过程:
      • I/O设备通过向处理的引脚发送一个发送信号,并将异常号放到系统总线上,以触发中断,异常号标识了触发中断的设备;
      • 处理器注意到引脚的电压发生变化,便从系统总线上读取异常号,然后调用相应的中断处理程序;
      • 当处理程序结束时,返回到下一条指令;

剩下的异常类型(陷阱、故障和终止)是同步发生的,是执行当前指令的结果。此类指令叫做故障指令(faulting instruction)。


  • 陷阱和系统调用
    • 陷阱是有意的异常;其用途是在用户程序和内核之间提供一个像过程一样的接口,叫系统调用;
    • 当用户程序向内核请求服务时(read、fork、execve、 exit), 为了允许对这些内核 服务的受控访问,处理器提供了“syscall n”指令;当用户想调用服务n时,调用此指令,会导致到一个异常处理程序的陷阱,此异常处理程序对参数解码(应该是n),并调用相应的内核处理程序;
    • 一般的过程调用运行在过程模式(限制了指令的类型、并且在相同的函数栈中),而系统调用运行在内核模式(允许系统调用执行指令(syscall),并且在内核栈中);

  • 故障
    • 由错误引起,可能被修正
    • 当故障发生时,它就将控制转移到故障处理程序;如果程序能够修正这个错误的情况,就返回到引起故障的指令,否则返回到内核的abort例程;

  • 终止
    • 终止是不可恢复的致命的错误造成的,通常为硬件错误;终止程序不应将控制返回给应用程序;

8.2 进程

  • 异常是允许操作系统提供进程(process)的概念所需要的基本构造块;

  • 进程经典的定义是一个执行中的程序实例;系统中每个程序都是运行在某个进程上下文中的;上下文包括:存储器中的代码和数据、通用寄存器中的内容、程序计数器、栈、环境变量以及打开文件描述符的集合。

  • 进程提供两个关键的抽象:

    • 一个独立的逻辑控制流:它提供一个假象,好像程序独占的使用处理器;

    • 一个私有的地址空间:它提供一个假象,好像程序独占的使用存储器系统;

8.2.1 逻辑控制流
  • 逻辑控制流就是PC值对应的指令序列;

  • 一个处理器的物理流可以分成多个逻辑流;

8.2.2 并发流
  • 计算机中流有不同的形式:异常处理程序、进程、线程、信号处理程序;

  • 并发流:一个逻辑流在执行时间上与另一个流重叠;

  • 并发:多个流并发的执行的现象;

  • 多任务:一个进程和其他进程轮流执行的概念称为多任务;

  • 时间片:一个进程执行它的控制流的一部分的每一时间段;多任务也叫时间分片

  • 并行流:并发的真子集,如果两个流并发的运行在不同的处理器核或者计算机上,称为并行流;

8.2.3 私有地址空间
  • 一个进程为每个程序提供提供他自己的私有地址空间
  • 和此空间中某个地址相关联的存储器字节时不能被其他进程读写的;
  • 每个私有地址空间的内容不同,但有相同的通用结构;
  • 32位进程代码段从0x08048000开始,64位进程从0x00400000开始;
  • 地址空间顶部保留给内核,包含内核的代码、数据和栈;(例如执行一个系统调用);
8.2.4 用户模式和内核模式
  • 处理器通常使用某个控制寄存器中的一个模式位(mode bit)来切换用户模式或者内核模式
  • 内核模式可以执行所有的指令和访问任何存储器位置
  • 用户模式则没有上述功能;任何尝试都会导致一般保护故障;必须通过系统调用接口访问内核代码和数据;
8.2.5 上下文切换
  • 操作系统使用上下文切换这种较高形式的异常控制流来实现多任务;上下文切换机制是建立在前面所述的较低层异常机制之上的;

  • 内核为每个进程维护一个上下文(内核重新启动一个被抢占的进程所需要的状态),上下文由:通用目的寄存器、浮点寄存器、程序计数器、用户栈、处理器状态寄存器、内核栈和内核数据结构(描绘地址空间的页表、有关当前进程信息的进程表、包含进程已打开文件的信息的文件表)

  • 内核重新启动一个之前被抢占的进程叫调度,内核中称为调度器;

  • 上下文切换:

    • 保存当前进程的上下文;
    • 恢复某个先前被抢占的进程的上下文;
    • 将控制权交给新恢复的进程

发生上下文切换的情况:

  • 当内核代表用户执行系统调用时,可能发生上下文切换;比如系统调用因为某个事件而阻塞,内核可以让该进程休眠,切换到另一个进程(比如read访问磁盘、sleep);在此种情况下,在内核中的工作一部分代表进程A,一部分代表进程B。

  • 一般而言,即使系统调用没有发生阻塞,内核也可以决定执行上下文切换;

  • 中断也可以发生上下文切换;比如所有系统都有产生某种周期性定时器中断的机制(1ms或者10ms),每当定时器中断时,切换新的进程;

8.3 系统调用错误处理

  • 当Unix系统级函数遇到错误时,典型的会返回-1,并设置全局变量errno;
if((pid = fork()) < 0){
    fprintf(stderr,"fork error: %s\n",strerror(errno));   //strerror()函数返回错误关联的字符串;
    exit(0);
}

但这样使得代码繁琐,简单一层包装:

if((pid = fork()) < 0)
    unix_error("fork error");

void unix_error(char * msg){
    fprintf(stderr,"%s,%s\n",msg,strerror(errno));
    exit(0);
}

使用错误包装处理函数:

pid_t Fork(void){
    pid_t pid;
    if((pid = fork()) < 0){
        unix_error("fork error");
    }
    return pid;
}

8.4 进程控制

8.4.1 获取进程状态
pid_t getpid(void); //获取当前进程
pid_t getppid(void); //获取父进程
8.4.2 创建和终止进程

进程的三种主要状态:

  • 运行 要么再CPU上运行,要么等待且最终被CPU执行;

  • 停止 进程的执行被挂起,且不会被调度;

  • 终止 进程永久停止

    • 收到一个信号,改信号默认终止进程

    • 从主程序返回 返回值为程序的最终返回状态

    • 调用exit()函数 参数值为最终的返回状态

创建进程:

pid_t fork(void);

子与父进程的同与不同:

  • 子进程得到父进程虚拟地址空间的一份拷贝,但是是独立的,包括代码段、数据段、推以及用户栈,此外还有文件描述符表(意味着子进程可以操作父进程打开的文件);

  • 子进程与父进程最大的区别在于有不同的pid;

fork()函数的特点:

  • 调用一次返回两次,一次在父进程中,返回子进程pid;一次在子进程中,返回0;
8.4.3 回收子进程
  • 当进程终止时,还是保存在内核中的,等待被其父进程回收;

  • 当父进程回收子进程时,内核将子进程的退出状态传递给父进程,然后抛弃子进程;(要求父进程回收为了获取子进程退出状态,内核会抛弃子进程,维护系统资源);终止却未被回收的子进程是僵尸进程;

  • 如果父进程没有回收其子进程就终止,那么由init进程回收;

waitpid()函数:

pid_t waitpid(pid_t pid,int *status,int options);
  • 参数pid:

    • pid > 0 : 等待集合就一个单独的子进程,就是此pid;
    • pid = -1:所有子进程;
    • Unix进程组
  • 参数options

    • WNOHANG 立即返回,不等待;
    • WUNTRACED 挂起调用进程,直到等待集合中一个进程被终止或者停止,返回相关pid;
  • 检查子进程退出状态

    • status参数非空,则此参数保存函数退出状态;

    • WIFEXITED(status) 子进程通过exit或者return返回正常终止时,返回真。

      • WEXITSTATUS(status)返回一个正常终止的子进程的状态,依赖于上一个函数返回真;
    • WIFSIGNALED(status) 如果子进程因为一个未捕获的信号终止,返回真;

      • WTERMSIG (status),返回导致子进程终止的信号的数量,之上上一个函数返回真时;
    • WIFSTOPPED(status)如果返回的子进程当前是被停止的,返回真

      • WSTOPSIG(status)返回引起子进程停止的信号的数量;依赖上一个函数返回真;
  • 调用错误

    • 当调用进程没有子进程时,返回-1,并且设置errno为ECHILD;

    • 函数被一个信号中断,返回-1,并设置errno为EINTR;

  • wait函数

pid_t wait(int* status); //等价调用waitpid(-1,&status,0);
8.4.4 让进程休眠
unsigned int sleep(unsigned int secs);
  • 如果请求时间到,则返回0;否则(函数被信号中断而过早的返回),返回剩下要休眠的秒数;
int pause(void);
  • 让调用进程休眠,直到该进程收到信号;
8.4.5 加载并运行程序
int execve(const char* filename,const char argv[],const char *envp[]); //调用一次,成功不返回
  • 参数 argv指向一个指针数组,每个指针指向一个参数串;

  • 参数envp指向一个指针数组,每个指针指向一个环境变量串;

在加载了filename之后,execve调用启动代码,启动代码设置栈,并将控制传递给新程序的主函数;

8.5 信号

每种信号类型都对应某种系统事件;底层的硬件异常,是由内核异常处理程序处理的;信号提供了一种机制,通知用户进程发生了这些异常;

8.5.1 信号术语
  • 发送信号

    • 内核监测到一个系统事件;
    • 一个进程调用kill函数,显式要求内核发送一个信号给目的进程;一个进程可以给自己发送信号。
  • 接收信号

    • 当目的进程被内核强制以某种方式对内核做出反应时,目的进程就接收了信号;
    • 进程可以忽略这个信号,终止或者通过执行用户层函数信号处理程序;
  • 一个只要发出而没有被接收的信号,叫待处理信号

  • 在任何时刻,一种类型,至多只有一个待处理信号,多余的同类型信号会被直接抛弃;
  • 一个进程可以有选择的阻塞接收某种信号,此信号仍可被发送,但待处理信号不会被接收,直到进程取消对这种信号的阻塞;
  • 带处理信号最多只能被处理一次,内核为每个进程维护着待处理信号的集合和被阻塞信号的集合;当接收到一个信号时,在集合中标记,当接收时,消除;

8.5.2 发送信号

Unix提供了多种向进程发送进程的机制。这些机制是基于进程组的;

  1. 进程组

    • 每个进程只属于一个进程组,进程组ID号由正整数表示;
    pid_t getpgrp(void);//获得当前进程的进程组id   
    int setpgid(pid_t pid,pid_t pgid);
    • 当pid为0时,改变调用进程,非零时,对应进程
    • 当pgid为0时,调用进程pid作为进程组号,非零时对应号;
  2. 用/bin/kill程序发送信号

    /bin/kill -9 15213 当-15213时,结束进程组号为15213的每一个进程

  3. 从键盘发送信号
    • 作业: shell对一个命令行求职而创建的进程;
    • 任何时刻,只有一个前台作业,0个或者多个后台作业
    • 外壳为每个作业创建独立的进程组
    • ctrl+c 导致SIGINT被发送到外壳,外壳捕获信号,然后发送到前台进程中的每个进程
  4. 用kill函数发送信号

    int kill(pid_t pid,int sig);
    • pid > 0,发送给指定进程, pid < 0,给进程组每个进程
  5. 用alarm函数发送信号
unsigned int alarm(unsigned int secs);
  • alarm函数和sleep函数由相似之处,被打断时都会返回剩余秒数
  • 对alarm的调用都会取消正在等待的alarm,并返回秒数;没有真在等待的就返回0;
8.5.3 接收信号
  • 当内核从异常程序返回时,会检查进程的未被阻塞的待处理信号的集合(pending&~block);原因:被阻塞说明条件还不满足,遂不予处理;

    • 当集合为空时,说明没有待处理的消息,直接将逻辑控制流传递给下一条指令;
    • 当集合不为空时,内核会选择集合中的某个信号(通常为值最小的那个),并强制进程接收信号,触发对应此信号的处理程序;处理完成后,控制流给下一指令;
    • 信号的默认行为:
      • 进程终止 (SIGKILL)
      • 进程终止并转储存储器()
      • 进程停止,直到被SIGCONT信号重启
      • 进程忽略该信号(SIGCHLD)
  • 进程可以使用signal函数修改信号的默认处理行为;
    注意: SIGSTOP和SIGKILL是不可忽略的的;
    这里写图片描述
    此函数有三种修改关联方法:

    • handler为SIG_IGN,忽略该信号;
    • handler为SIG_DEF,默认行为
    • handler为用户定义的函数地址,则调用此信号处理程序;
  • 当处理程序处理完时,控制通常会返回给下一个指令;但在某些系统中,被中断的系统掉用,会立即返回一个错误;(下面将讲述);

  • 信号处理程序的逻辑控制流与主函数的逻辑控制流重叠;
8.5.4 信号处理问题
  • 待处理信号被阻塞: 当当前的正在处理的信号处理程序是刚到达的信号类型时,待处理信号会被阻塞;
  • 待处理信号不会排队等待:当有两个同类型的信号到达,且此类型的处理程序正在运行,信号被阻塞,且第二个信号直接被丢弃;所以不能用消息进行事件计数
  • 系统调用可以被中断:像read、wait、accept这样会阻塞进程一段时间的(慢速系统调用),当处理程序捕获到一个信号时,被中断的系统调用返回时不再继续,直接返回错误;
8.5.5 可移植信号处理
  • 不同系统对于信号的处理语义是有差异的(中断的慢速系统调用是重启还是放弃);
  • sigaction函数指明了信号处理语义:
    这里写图片描述
  • sigaction包装函数:Signal
    这里写图片描述
    此函数信号处理语义如下:
  • 当前处理信号被屏蔽
  • 信号不会排队(不会有超过1个的相同信号在排队)
  • 只要有可能,被中断的系统调用会重启;
  • 一旦被设置了处理函数,就一直会存在,除非将handler参数设置为SIG_DEF或者SIG_IGN;
8.5.6 显示地阻塞和取消阻塞信号

这里写图片描述

  • sigprocmask函数改变当前已阻塞信号的集合,具体行为依赖于how值:
    • SIG_BLOCK:添加set中的信号到block集合中;
    • SIG_UNBLOCk:从block集合中删除se集合中的信号;
    • SIG_SETMASK:将block集合设置为set集合;
8.5.7同步流以避免讨厌的并发错误
  • 当多个控制流对同一资源进行访问时,容易发生竞争;
  • 处理:
    • 对于信号造成的,可以先屏蔽信号
    • 这里写图片描述
    • 对于调度策略造成的可以随机休眠;
      这里写图片描述

8.6 非本地跳转

  • C语言提供用户级异常控制流形式,非本地跳转;从一个函数转移到另一个函数,无需经过调用-返回序列;这里写图片描述
  • longjump函数从env中恢复调用环境,然后触发最近一次调用的setjump返回,返回longjump第二个参数;longjump从不返回;

注意: setjump调用一次,返回多次

  • 第一次调用时,保存env环境,返回0;
  • 第二次调用longjump时,setjump返回longjump第二个参数;
    这里写图片描述

  • 另一个应用,从中断程序中不返回到打断位置,而是利用保存的环境,重新启动;
    这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值