中断和异常——内核的动力

本文内容关注一种内核工作的基本机制——中断和异常。标题称这种机制为内核的动力,或许不那么恰当,主要想强调中断和异常对内核工作的重要性。对于操作系统上层的应用程序开发者而言,或许感觉离中断和异常的机制很远,因为我们不需要开发内核。但是毕竟我们使用的是内核所提供的各种服务,理解内核的基本运作原理能够帮助我们更好地写程序、优化程序。

问在前面:

  • 通常所说的“陷入内核”,是怎么个陷入法呢?
  • 我们的网卡收到数据,内核怎么知道该去处理数据了呢?时刻去轮询一下,恐怕不太现实?
  • 我们访问了一个非法的内存地址,或者执行了一个除0操作。毫无悬念,内核崩溃。那么内核是怎么知道进程崩溃了,该来收拾这个烂摊子了?

直观来看,内核是什么?

内核地址空间和进程的地址空间相互独立?

我们都知道,Linux内核与进程的地址空间相互独立。这是什么意思?直观的比喻就是,每个进程在自己的内存区域工作,内核也在自己的区域工作,它们之间互相都是不可见的。(这点由虚拟内存管理机制保证)。在同一个地址空间内部,我们可以简单的使用函数调用(或者说过程调用)来使用别的函数提供的功能。比如,你自己写的程序,函数funcA调用funcB。又比如,在Linux内核内部,函数可以直接调用。但是在两个地址空间之间,交互就没那么容易了。比如,如果进程想要使用内核提供的服务(文件读写,网络通信等等),怎么做呢?答案是,系统调用(陷入内核)。

系统调用怎么实现呢?

系统调用的目的就是进程能够使用内核的服务。但是具体是怎么向内核发起调用(请求)的呢。答案是中断。我们知道,CPU不断的取指令,执行指令。我们跟CPU约定好,如果遇到一种指令,叫“软中断指令”,CPU就保存当前进程的上下文,改变CPU的模式,然后跳转到内核的中断向量表,获取“软中断处理程序”的地址,然后执行。这就算陷入内核了。

内核是由中断驱动的一个程序实体

内核由各种各样的服务组件组成,各种系统调用、文件系统、网络系统、调度子系统等等。各种各样的内核活动的产生并不是凭空的,都是需要某种方式驱动,这就是中断。调度:定时器按一个时间片的周期触发一个中断,内核由此触发一次调度过程。系统调用:进程触发一个软中断,内核由此执行一个具体的系统服务。设备驱动:网卡收到数据,触发一次中断,内核由此执行一次对网卡的读操作。等等。

中断和异常

本质上,中断和异常属于同一个范畴。

我们知道,CPU的工作就是不断的取指令,执行指令。每执行一次指令,或者在执行指令期间会发生三种情况:

    1. 指令按预想的结果执行,然后取下一条指令,继续执行。
    1. 这是一条特殊指令,预示一种异常的流向。比如除0操作,越界访问,缺页异常或者系统调用的软中断指令。
    1. 执行期间,外部设备触发了一个中断,而CPU不得不响应这个中断,转去执行中断服务程序。这是另一种类型的异常流向。

对于第2种情况,我们称之为异常。就是CPU执行指令过程中出现的异常情况。因为它是CPU执行完一条指令之后触发的,所以也称为“同步中断”。对于第3种情况,称为“中断”,CPU随时都有可能收到外部设备的中断,具有随机性,也称之为“异步中断”。

不管是中断还是异常,它们的处理程序都是内核来执行的。

中断上下文与进程上下文

首先谈进程上下文:

示例:

同样是一次read调用,进程A和进程B得到效果是不一样的,进程A读取文件a,获取到x个字节;进程B读取文件b,获取到y个字节。而内核肯定只有一个read调用。
内核如何做到区别对待?

只告诉内核执行什么系统调用是不够的,内核必须知道,为哪个进程服务,读取哪个文件,预期读取多少个字节,要拷贝用户缓冲的目的地址等等。简言之,内核需要一个执行环境,这个环境就是所谓的“进程上下文”。即,当一个进程正在进行时,CPU所有寄存器中的值、进程的状态以及堆栈中的内容称之为进程的上下文。所以,进程执行系统调用,“内核代表进程执行,在进程上下文”。

再来就是中断上下文,和进程上下文是一个道理,当硬件触发中断的时候,内核需要获取到当前该硬件的各种变量和参数,内核根据这些参数来执行中断服务程序。而硬件传过来的各种参数和内核需要保存的一些环境(被中断的进程环境),可以看作是中断上下文

总结

总的来说,内核就是一堆处理过程合成的实体。它基本没有主动性,靠的是各种中断来驱动的。进程有需求了,触发中断告诉它;程序执行出错了,触发中断告诉它;设备状态变化了,还是触发中断告诉它;时间片到期,该调度了,触发中断告诉它。