chapter08_异常控制流

  1. 基本概念

    1. 控制流

       a0, a1, ..., an-1
      

      其中,ai代表指令Ii的地址。ai->ai+1的过渡称为控制转移,这样的控制转移队列称为控制流

    2. ECF(Exceptional Control Flow, 异常控制流)

      现代os通过使得控制流发生突变,来对特殊情况做出反应,称为异常控制流

    3. ECF发生在计算机系统的各个层次

       硬件层 --- 硬件检测到的事件会触发控制转移到异常处理程序
      
       操作系统层 --- 内核上下文切换将控制从一个用户进程转向另一个用户进程
      
       应用层 --- 一个进程向另一个进程发**信号**,接收进程会将控制转向信号处理程序
      
    4.  异常 --- 硬件和操作系统交界
      
       进程和信号 --- 应用和操作系统交界
      
       非本地跳转(例如try/catch/throw) --- 应用层
      
  2. 异常

    1. 处理器的状态变化称为事件

      (1) 事件可能和当前指令相关

       eg. 缺页、除0
      

      (2) 事件可能和当前指令无关

       eg. 定时器信号、IO请求完成
      
    2. 异常处理的原理

      (1) 系统为每种可能出现的异常分配一个异常号

      (2) 在系统启动时,系统初始化一个异常表:异常表的索引是异常号,对应的是异常处理程序的地址

      (3) 异常处理程序运行在内核态

    3. 异常的类别

      (1) 中断

      1° IO设备(网络适配器、磁盘控制器、定时器),向处理器芯片的一个管脚发信号触发中断

      2° 唯一一种异步类型的异常

      (2) 陷阱

      有意的异常,目标是用户程序向内核请求服务,陷阱提供了这个接口

      2° 执行

       syscall n 
      

      指令,请求某个内核服务(例如读文件read、创建新进程fork、加载一个新程序execve、终止当前进程exit)

      系统调用的过程类似函数调用。但是函数调用在用户模式下,系统调用在内核模式

      (3) 故障

      1° 执行故障处理程序,如果能修复就重新执行,修复不了就返回内核的abort例程终止程序

      2° 典型的故障异常是缺页异常

      (4) 终止

      1° 典型的终止异常是硬件错误

      2° 终止处理程序会直接返回内核的abort例程终止程序

      (5) 总结

      类别 原因 同步or异步 返回行为
      中断 IO设备的信号 异步 总是返回到下一条指令
      陷阱 有意进行的系统调用 同步 总是返回到下一条指令
      故障 潜在可恢复的错误 同步 要么返回到当前指令重新执行,要么abort终止应用程序
      终止 不可恢复的错误 同步 abort终止应用程序
  3. 进程

    1. 基本概念

      (1) 定义

      一个执行中程序的实例。

      系统中的每个程序都是运行在某个进程的上下文

      (2) 上下文(context)

      由程序正确运行所需的状态组成。

      包括:

       1° 存储器中程序的代码和数据
      
       2° 栈
      
       3° 通用目的寄存器中的内容
      
       4° 程序计数器
      
       5° 环境变量
      
       6° 打开文件的描述符集合
      

      (3) 进程提供的抽象

      1° 一个独立的逻辑控制流

       ---- 提供了一个假象,让人们觉得程序在独占处理器
      

      2° 一个私有的地址空间

       ---- 提供了一个假象,让人们觉得程序在独占存储器系统
      
    2. 逻辑控制流

      (1) 进程轮流使用处理器,然后被抢占

       A    ------
      
       B          -----
      
       C               --
      
       A                 -------
      

      但是每个进程看上去都像是在独占处理器

      (2) 并发进程

      任何逻辑流在时间上和其他逻辑流有重叠的进程之间称为并发进程

      eg.

       A 和 B
      
       A 和 C
      
       但是 B 和 C 就不是并发进程
      

      (3) 时间片

      一个进程在执行它的控制流的一部分的每一时间段叫做时间片

    3. 私有地址空间

      (1) 一个进程为每个程序提供它自己的私有地址空间,一般而言,和这个空间中某个地址相关联的存储器字节不可被读写

      (2) 对于n位地址的机器,地址空间是2^n个可能地址的集合

      (3) 每个地址空间的结构类似

      顶部1/4预留给内核,底部3/4预留给用户程序,包括通常的文本、数据、堆、栈。

    4. 用户模式和内核模式

      (1) 内核模式

      一个运行在内核模式的进程,可以

      1° 执行指令集中的任何指令

      2° 访问任何存储器的位置

      (2) 用户模式

      1° 不允许执行特权指令

      2° 不允许直接使用地址空间中的内核区的代码和数据 —> 必须通过系统调用接口的方式

      (3) 初始时应用程序都在用户模式,进程从用户模式到内核模式的唯一方法是通过异常

    5. 上下文切换

      (1) 上下文是内核重新启动一个被抢占进程所需的状态,由一些对象的值组成,包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈、各种内核数据结构(页表、进程表、文件表等)

       页表 --- 描述地址空间
      
       进程表 --- 包含有关当前进程信息
      
       文件表 --- 包含进程已打开文件的信息
      

      (2) 调度

      内核中的调度器决定抢占当前进程,并重新开始一个先前被抢占的进程

      (3) 上下文切换

      工作:

      1° 保存当前进程的上下文

      2° 恢复某个先前被抢占进程的上下文

      3° 将控制传递给这个新恢复的进程

      典型的发生时刻:

      1° 系统调用

      2° 阻塞

      3° 定时器中断

      (4) 一般而言,cache不能和中断/上下文切换这样的ECF很好的交互,cache中的数据往往在ECF之后被污染,难以存放有效数据

  4. 系统调用和错误处理

    1. man syscalls

      查看完整的系统调用列表

    2. 标准C库提供了一组针对常用系统调用的包装函数,一般直接用这种包装函数。系统调用和它们的包装函数可以互换的称为系统级函数

    3. 当Unix系统级函数遇到错误时,会典型的返回-1,并设置全局整数变量errno来表示错误类型

    4. Unix风格的错误处理包装函数(自己写的)

       #include <stdio.h>
       #include <stdlib.h>
       #include <unistd.h>
       #include <sys/types.h>
       #include <errno.h>
       #include <string.h>
      
       pid_t Fork(void) {
           pid_t pid;
           if (pid = fork() < 0) {
               unix_error("Fork error");
           }
      
           return pid;
       }
      
       void unix_error(char* msg) {
           fprintf(stderr, "%s: %s\n", msg, strerror(errno));
      
           exit(0);
       }
      

      在错误处理包装函数Fork()中,既完成了和fork()相同的功能,同时记录错误类型,并exit程序。

  5. 进程控制

    1. 系统调用函数可以靠

       man xxx
      

      的方式获取必须包含的include文件

    2. 获取进程pid

      (1) 每个进程都有一个唯一的正数PID

      (2) 相关系统调用

       #include <unistd.h>
       #include <sys/types.h>
      
       pid_t getpid(void)    --- 返回调用进程的pid
       pid_t getppid(void)   --- 返回调用进程父进程的pid
      
    3. 创建和终止进程

      (1) 每个进程一定处于以下三种状态之一

      运行

      在CPU上执行或者等待被调度

      暂停

      进程的执行被挂起

       运行 --- 收到 SIGSTOP/SIGTSTP/SIDTTIN 信号 --- 暂停 --- 收到 SIGCONT 信号 --- 运行
      

      信号是一种软件中断

      终止

      进程永远的停止了。有三种原因:

      I. 收到一个信号,这个信号的默认行为是终止进程

      II. 从主程序返回

      III. 调用 exit() 函数

      (2) 相关系统调用

       #include <stdlib.h>
      
       void exit(int status);    --- 终止进程,并以status状态退出(在主程序中return status也可以设置退出状态)
      
       ---------
      
       #include <unistd.h>
       #include <sys/types.h>
      
       pid_t fork(void);  --- 父进程创建新的运行子进程
      

      (3) 关于fork系统调用

      1° fork函数会被调用1次,但是会返回2次:一次是在父进程中返回,一次是在子进程中返回

      2° 出错时fork函数的返回值是-1,不出错时子进程中的返回值为0,父进程中的返回值为子进程的PID,以此来区分父子进程

       示例
      
       #include <stdio.h>
       #include <stdlib.h>
       #include <unistd.h>
       #include <sys/types.h>
       #include <errno.h>
       #include <string.h>
      
       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;
       }
      
       int main() {
      
           pid_t pid;
           int x = 1;
      
           pid = fork();
      
           if (pid == 0) {  // child
               printf("child : x=%d\n", ++x);
               exit(0);
           } 
      
           // parent
           printf("parent: x=%d\n", --x);
      
           return 0;
       }
      

      3° 父子进程最大的区别在于PID不同

      4° 子进程得到与父进程用户级虚拟地址空间相同的一份拷贝(但是独立的),包括文本、数据、bss段、堆、用户栈、打开的文件描述符(子进程可以读写父进程打开的任意文件)。它们拥有相同(fork刚调用完)但独立(从此以后空间私有)的地址空间

      5° 父子进程是并发执行独立进程

      6° 画进程图的方式可以方便理解fork函数

       int main() {
           int x = 1;
           if (fork() == 0) {
               printf("f1: x = %d\n", ++x);
           }
      
           printf("f2: x = %d\n", --x);
           exit(0);
       }
                子进程  ------------x = 1---> f1, x=2 ----> f2, x=1 ---->
                       |
                       |fork()
                       |
       ---------父进程--------------x = 1---> f2, x=1 ---->
      
    4. 回收子进程

      (1) 当进程终止时,内核并不是立刻清除,而是保持终止状态,直到被它的父进程回收

      (2) 父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程,此时开始子进程才不复存在

      (3) 僵尸进程

      已经终止但是未被回收的进程

      (4) 若父进程没有回收它的僵尸子进程就终止了,内核会安排init进程(PID恒等于1)来回收僵尸子进程

      (5) 等待子进程终止或暂停(然后将子进程从系统中去除)

       #include <sys/types.h>
       #include <sys/wait.h>
      
       pid_t waitpid(pid_t pid, int* status, int options);
      

      (6) 检查已回收子进程的退出状态

      P519 有几个宏,例如

       WIFEXITED(status);
       WIFSIGNALED(status);
      

      (7) 示例

       #include <sys/types.h>
       #include <unistd.h>
       #include <stdlib.h>
       #include <wait.h>
       #include <stdio.h>
       #include <errno.h>
      
       #define N 2
      
       int main() {
      
           int status, i;
      
           pid_t pid[N+1], retpid;
      
           for (int i = 0; i < N; i++) {
               if ((pid[i] = fork()) == 0) { // child process
                   exit(100 + i);
               }
           }
      
           for (int i = 0; i < N; i++) {
               printf("%d\n", pid[i]);
           }
      
           // parent process collects N zombie child processes in order
           i = 0;
           while ((retpid = waitpid(pid[i++], &status, 0)) > 0) {
      
               // judge if a process exited by exit(status) or return
               if (WIFEXITED(status)) {
                   printf("child %d terminated normally with exit status %d\n", retpid, WEXITSTATUS(status));
               } else {
                   printf("child %d terminated abnormally\n", retpid);
               }
           }
      
           exit(0);
       }
      
    5. 让进程休眠

       #include <unistd.h>
      
       unsigned int sleep(unsigned int secs);  // 休眠一段secs秒,直到休眠结束或是被信号中断提前返回;返回剩余要休眠的秒数
      
       int pause(void);  // 让调用函数休眠,直到被信号唤醒
      
    6. 加载并运行程序

      (1)

       #include <unistd.h>
       
       int execve(char* filename, char* []argv, char* []envp);
      
       filename: 可执行目标文件名
       argv: 参数列表数组,以null结尾,argv[0]是文件名
       envp: 环境变量数组,以null结尾,形式是"name=value"
      

      几个操作环境变量数组的系统调用

       #include <stdlib.h>
      
       char* getenv(const char* name);
       int setenv(const char* name, const char* newvalue, int overwrite);
      
       void unsetenv(const char* name);
      

      (2) 程序与进程

      程序:代码和数据的集合。可以作为目标模块存在于磁盘上,也可以作为段存在于地址空间。

      进程:执行中程序的一个特殊实例。 —> 程序总是运行在某个进程的上下文中

      fork: 在新的子进程中,运行相同的程序。 – 调用一次,返回两次

      execve: 在当前进程的上下文中,运行一个新的程序,覆盖当前进程的地址空间,具有相同的PID,并且继承了调用函数的所有打开的文件描述符。 – 调用一次,成功了就不返回

      (3) Unix shell 的原理

       while (1) {
      
           read_user_input();   // 读取用户输入
      
           ifBackGround, cmd, argv, env = parse_input();  // 解析用户输入
      
           // 如果是内置shell命令(例如pwd这种),立刻解释这个命令
           if (built_in_cmd(cmd)) {
               do_built_in_cmd(cmd);
               continue;
           }
      
           pid = fork();    // fork()出一个子进程,用于执行非内置的可执行文件
      
           // 子进程中调用execve将地址空间完全替换成新程序
           if (pid == 0) {
               execve(argv[0], argv, env);
               exit(0);
           }
      
           // 父进程根据用户输入的是否在后台执行的请求,决定是否等待子进程返回再进行下一轮循环
           if (!ifBackGround) {
               waitpid(pid, &status, 0);
           }
       }
      
  6. 信号

    1. 低级的硬件异常是由内核异常处理程序处理的,通常对用户进程不可见。信号提供了一种机制,向用户进程通知这些异常的发生。

      eg.

       某个进程除零错误 ---> 内核给它发送 SIGFPE 信号
      
    2. Linux系统上支持30种不同类型的信号,它们有号码,名字,默认行为,相应事件(P529)

      示例

       号码      名字      默认行为   相应事件
      
        9      SIGKILL      终止     杀死程序
      
    3. 步骤

      (1) 发送信号

      1° 内核通过更新目的进程上下文的某个状态,将信号发送给目的进程

      2° 发送信号的可能原因

      I. 内核检测到系统事件(eg. 除零错误、子进程终止)

      II. 一个进程调用了kill函数,显式要求内核发送信号给目的进程

      进程组

      I. 每个进程都属于一个进程组

      II. 默认地,一个子进程和它的父进程同属于一个进程组

      III. 改变进程组

       #include <unistd.h>
      
       pid_t setpgid(pid_t pid, pid_t pgid);
      

      4° 发送信号的方式

      I. 用kill程序发送信号

       unix> kill .9 12213    // 发送信号9(对应着SIGKILL)给PID为12213的进程
      
       unix> kill .9 .12213    // 发送信号9(对应着SIGKILL)给PGID为12213的进程组的每个进程
      

      II. 从键盘发送信号

       ctrl-c    ----  SIGINT, 终止前台作业
       
       ctrl-z    ----  SIGTSTP, 暂停前台作业
      

      III. 用kill函数发送信号

       #include <sys/types.h>
       #include <signal.h>
      
       int kill(pid_t pid, int sig);  // 发送sig信号给pid进程,如果pid小于0,发送给abs(pid)的进程组
      

      IV. 用 alarm 函数发送信号

       #include <unistd.h>
      
       unsigned int alarm(unsigned int secs);  // 安排内核在secs秒内,发送一个SIGALRM信号给调用进程
      

      示例

       #include <unistd.h>
       #include <signal.h>
       #include <stdio.h>
       #include <stdlib.h>
      
       void handler(int sig) {
      
           static int beeps = 0;
           printf("beep\n");
      
           if (++beeps < 5) {
               alarm(1);
           } else {
               printf("boom\n");
               exit(0);
           }
       }
      
       int main() {
      
           signal(SIGALRM, &handler);
           alarm(1);
      
           while(1) {
               ;
           }
      
           exit(0);
       }
      

      (2) 接收信号

      待处理信号阻塞信号

      一个只发出而没有被接收的信号称为待处理信号,任何时刻一种类型的待处理信号只有一个,如果一种类型已经有一个待处理信号了,那么同一类型的其他信号会被直接丢弃

      一个进程可以选择性阻塞某种类型信号的接收,此时这种类型的信号可以发送但是不会被接收。

      2° 原理

      内核每个进程在pending位向量中维护着待处理信号集合,在blocked位向量中维护着被阻塞信号集合

      每次只要一个类型为k的信号被传送,内核就检查一下blocked对应的位,如果没有被阻塞就set pending的第k个位;只要一个类型为k的信号被接收,内核就会在pending中reset第k个位

      3° 每个信号类型都有一个预定义的默认行为 P529

      I. 进程终止

      II. 进程终止并转储存储器

      III. 进程暂停直到被SIGCONT信号重启

      IV. 进程忽略该信号

      4° 改变和信号相关联的行为 – signal函数

       #include <signal.h>
      
       typedef void handler_t(int)
      
       handler_t* signal(int signum, handler_t* handler);   // 若成功则返回前次信号处理程序的函数指针
      

      I. 如果handler是SIG_IGN,那么忽略signum的信号

      II. 如果handler是SIG_DFL,那么signum的信号恢复默认行为

      III. 否则,handler是用户定义的函数指针——称为信号处理程序

    4. 信号处理的问题

      (1) 待处理信号被阻塞

      一个k信号正在被handler处理,同样的k信号到达,只能在待处理信号集合中等待

      (2) 待处理信号不会排队等待

      一个k信号在待处理集合中,同样后来的k信号再到达时,直接丢弃

      (3) 系统调用可以被中断

      类似read/write/accept一类的慢速系统调用,在某些系统中(例如Solaris)一旦被中断且handler返回以后,慢速系统调用不再继续而是直接返回错误

    5. 可移植的信号处理

      (1) 想要解决的问题:

      中断后的慢速系统调用重启or放弃,在不同的系统上表现不一致

      (2) 解决办法:POSIX标准定义的sigaction函数

       #include <signal.h>
      
       int sigaction(int signum, struct sigaction* act, struct sigaction* oldact);
      

      一般使用的是它的Wrapper函数Signal

       handler_t *Signal(int signum, handler_t *handler) {
      
           struct sigaction action, old_action;
      
           action.sa_handler = handler;  
           
           sigemptyset(&action.sa_mask); /* Block sigs of type being handled */
      
           action.sa_flags = SA_RESTART; /* Restart syscalls if possible */
      
           if (sigaction(signum, &action, &old_action) < 0) {
               unix_error("Signal error");
           }
      
           return (old_action.sa_handler);
       }
      

      Signal函数的语义是:

      I. 只有这个处理程序当前正在处理的那种类型的信号被阻塞

      II. 信号不会排队,直接丢弃

      III. 只要可能,被中断的系统调用会自动重启

      IV. 一旦设置了信号处理程序就会一直保持,直到Signal带着handler参数为SIG_IGN或者SIG_DFL被调用

    6. 显式地阻塞信号

       #include <signal.h>
      
       int sigpromask(int how, const sigset_t* set, sigset_t* oldset);
      
       int sigemptyset(sigset_t* set);
      
       int sigfillset(sigset_t* set);
      
       int sigaddset(sigset_t* set, int signum);
      
       int sigdelset(sigset_t* set, int signum);
      
       int sigismember(const sigset_t* set, int signum);
      
  7. 非本地跳转

    1. 作用

      将控制直接从一个函数转移到另一个当前正在执行的函数,而不需要经过正常的调用-返回序列

    2. 系统调用

       #include <setjmp.h>
      
       int setjmp(jmp_buf env);
       int sigsetjmp(sigjmp_buf env, int savesigs);
      
      
       void longjmp(jmp_buf env, int retval);
       void siglongjmp(sigjmp_buf env, int retval);
      

      (1) setjmp和sigsetjmp函数在env缓冲区中保存当前栈的内容,返回0

      (2) longjmp和siglongjmp会从env缓冲区中恢复栈的内容,然后触发从最近一次初始化env的setjmp调用的返回,然后从setjmp返回,并带有非0的返回值retval

      (3) 关系

      1° setjmp函数只被调用一次,但返回多次——一次是第一次调用setjmp,后面是每个相应的longjmp

      2° longjmp函数只被调用一次,从不返回

    3. 非本地跳转的应用

      (1) 从深层嵌套的函数调用中立刻返回,而不是费力解开各层栈

       #include <setjmp.h>
       #include <stdio.h>
       #include <stdlib.h>
      
       jmp_buf buf;
      
       int error1 = 0; 
       int error2 = 1;
      
       void foo(void), bar(void);
      
       int main() {
      
           int rc;
           rc = setjmp(buf);
      
           if (rc == 0) {
               foo();
           } else if (rc == 1) {
               printf("Detected an error1 condition in foo\n");
           } else if (rc == 2) {
               printf("Detected an error2 condition in foo\n");
           } else {
               printf("Unknown error condition in foo\n");
           }
      
           exit(0);
       }
      
       /* Deeply nested function foo */
       void foo(void) {
      
           if (error1) {
               longjmp(buf, 1); 
           }
      
           bar();
       }
      
       void bar(void) {
           if (error2) {
               longjmp(buf, 2); 
           }
       }
      

      (2) 中断发生时,使一个信号处理程序分支到一个特殊位置,而不是返回到被信号中断了的指令的位置

      下面这个示例程序的效果是按下ctrl-c时程序重新到sigsetjmp指令处

       #include <stdlib.h>
       #include <stdio.h>
       #include <setjmp.h>
       #include <signal.h>
       #include <unistd.h>
      
       sigjmp_buf buf;
      
       void handler(int sig) {
           siglongjmp(buf, SIGINT);
       }
      
       int main() {
      
           signal(SIGINT, &handler);
      
           if (!sigsetjmp(buf, SIGINT))  {
               printf("starting\n");
           } else {
               printf("restarting\n");
           }
      
           while(1) {
               sleep(1);
               printf("processing...\n");
           }
      
           exit(0);
       }
      
    4. C++和Java中提供的异常机制是更高层次

      catch 类似于 setjmp 的封装

      throw 类似于 longjmp 的封装

发布了391 篇原创文章 · 获赞 7 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/captxb/article/details/103166626