UP | HOME

操作系统之异常控制流

目录

1 简介

CSAPP - 深入理解计算机系统这本书断断续续的看了部分章节了, 但是在看到 异常控制流 这一章之前,我没有想到 操作系统 在我们编写的程序中扮演着如此总要的角色。

我看这本书的顺序没有按着目录顺序来, 一部分原因是为了适配学校的课程, 一部分是因为兴趣, 毕竟部分内容看着更有趣些。

现在看过的章节以及阅读顺序为:

  • 第一章 计算机系统漫游
  • 第二章 信息的表示和处理
  • 第三章 程序的机器级表示
  • 第七章 链接
  • 第十章 系统级 I/O
  • 第八章 异常控制流

第八章是刚看完不久的一个章节, 也是目前感触最深的一个章节。

这篇博客的一个目的就是记录下现在的感受。

2 异常控制流

如果学过其他高级语言, 那么 异常 这个词应该不陌生, 或者说很熟悉。

但是在 异常控制流 这一章中, 异常的概念和我之前的理解有着很大的不同, 但是也感觉的出来,书中的描述更符合 计算机 的想法。

假设存在一个值的序列: A0, A1, A2, ···, Ak. 其中每个值 Ak 是某个相应的指令 Ik 的地址。

每次从 AkA(k+1) 的过渡称为 控制转移, 这样的 控制转移序列 叫做处理器的 控制流.

如果执行顺序相邻的指令在内存中的地址也是相邻的, 那么这样的 控制流 称为 平滑控制流.

突变 的平滑控制流, 即执行顺序相邻的指令在内存中的地址不相邻, 那么这样的控制流就称为 异常控制流(ECF).

简单的解释就是: 异常控制流 就是存在执行顺序上相邻但内存地址上不相邻的指令的 控制流.

3 异常

异常异常控制流 的一种 表现形式, 也就是说异常就是异常控制流。

异常 分为 中断, 陷阱, 故障终止.

假设处理器正在执行某个应用程序的某条指令 I_curr, 该指令的下一条指令为 I_next.

中断

中断是由处理器外部的 I/O 设备引起的, 与指令的执行无关, 因此是 异步的. 这是唯一的 异步 异常,而处理 中断 的异常处理程序为 中断处理程序.

如果执行 I_curr 指令时发生中断异常, 那么处理器会在执行完 I_curr 指令后调用 中断处理程序, 此时控制流发生了突变, 因为 中断处理程序 的指令和当前程序的指令在内存上不相邻。

在执行完 中断处理程序 后, 控制流返回到指令 I_next.

陷阱

陷阱是 有意 的异常, 由某一条指令的执行引起。 当引起陷阱的指令 I_curr 执行完后, 系统会调用相应的 陷阱处理程序. 然后控制流返回到指令 I_next.

引起 陷阱 的指令通常为一个 系统调用, 系统调用是 内核用户程序 提供的一个像过程(函数)一样的接口。

用户程序执行的如 文件读取, 进程操作 等动作都是由 内核 提供服务的。

故障

故障是由正在执行的指令 I_curr 引起的, 这条指令可能导致了某个错误。 错误发生后, 处理器会停下当前指令的执行, 调用对应的 故障处理程序.

如果 故障处理程序 能够修复这个错误, 那么控制流就返回到停下的指令 I_curr 处。 反之, 终止当前程序。

终止
终止是不可恢复的致命错误造成的异常, 发生时会直接终止程序。

可以看到, 这四种基本的 异常 发生时都会导致 控制流 的突变, 而控制流的突变使得 控制流 转向另外的程序。

4 信号

在原书上, 信号前还有一节的内容是 进程, 进程 属于 异常 的一种应用方式, 需要详细了解的话可以查阅相关资料。

相较于 进程, 这一章带给我最大感触的两节就是 异常信号.

操作系统 通过修改目标进程的 上下文 来向目标进程发送信号。

这里不讨论 信号 的具体表现形式, 只需要知道, 发送信号是通过改变目标进程 上下文 来完成的就足够了。

而进程可以通过获取这些变化来获取或处理信号。

为什么说这一节带来的感触很大, 因为这是我第一次感受到了 操作系统 和我编写的程序之间的距离。

之前的学习过程的章节中, 编写的程序都没有如此直白的和 操作系统 进行交流, 而这一节, 你能够感受到, 你的程序和操作系统 从来没有分开过.

当进程收到信号后, 会通过 异常控制流 调用相应的 信号处理程序. 操作系统提供了一些默认的处理行为, 我们可以修改其中的一部分。

想要了解相关操作, 可以看一下 标准库: <signal.h>.

5 非本地跳转

#include <setjmp.h>
#include <signal.h>
#include <stdio.h>

jmp_buf ex_buf__;

#define TRY do{ if(!setjmp(ex_buf__)) {
#define CATCH } else {
#define ETRY } } while(0)
#define THROW longjmp(ex_buf__, 1)

void sigint_handler(int sig) {
  THROW;
}

int main(void) {
  if (signal(SIGINT, sigint_handler) == SIG_ERR) {
    return 0;
  }

  TRY {
    raise(SIGINT);
  } CATCH {
    printf("KeyboardInterrupt");
  }
  ETRY;

  return 0;
}

上面这一段代码尝试在 C 语言中实现 try/catch 语句, 用到了 信号非本地跳转 相关的内容。

首先是 信号 相关的内容:

void sigint_handler(int sig);

这个函数定义了一个 信号处理程序, 这个信号处理程序使用函数 signal 完成注册, 替代了信号 SIGINT 的默认处理程序。

SIGINT 代表的是 Ctrl-C 按下时产生的信号。

然后语句 raise(SIGINT) 主动发送一个 SIGINT 信号, 使得系统调用 sigint_handler.

接下来是 非本地跳转 相关的内容:

jmp_buf ex_buf__;

这个变量用于保存上下文信息, 当调用 setjmp 时, 会将调用时的上下文信息保存到 ex_buf__ 中。并返回数值 0.

当执行到 longjmp(ex_buf__, 1) 时, 会恢复当前的上下文到 ex_buf__ 保存时的状态。

当时正在执行的调用是 setjmp, 此时, setjmp 会再次返回一个值, 这个值就是 longjmp 第二个参数指定的值。

如果第二个参数的值是 0, 那么 setjmp 会返回 1.

宏展开后的主要程序代码为:

jmp_buf ex_buf__;

void sigint_handler(int sig) {
  longjmp(ex_buf__, 1);
}

int main(void) {
  if (signal(2, sigint_handler) == ((_crt_signal_t)-1)) {
    return 0;
  }

  do{ if(!_setjmp(ex_buf__)) { {
        raise(2);
      } } else { {
        printf("KeyboardInterrupt");
      }
    } } while(0);

  return 0;
}

因此, 程序的执行流程为:

  • _setjmp 第一次的返回值为 0, 执行语句 raise(2).
  • 对应的消息处理程序中执行 longjmp, 使得程序再次跳转到执行 _setjmp
  • 此时 _setjmp 返回的值为 1, 因此程序会输出 KeyboardInterrupt.

可以由此猜测高级语言的 异常机制 的实现。

猜测: 高级语言抛出异常时, 执行一次非本地跳转, 然后判断该次非本地跳转的返回值以执行相应的 catch 块。

同时注册 信号处理程序 用于捕获如键盘中断的信号, 并转换为内置的异常抛出。

6 参考链接

版权声明:本作品采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可