Interrupts


真实操作系统内存使用情况

运行top指令,在Mem这一行,大部分被用掉的内存,都是被buff/cache用了。

大部分操作系统运行时几乎没有任何空闲的内存。因此很多时候要分配内存都要先撤回一些内存。

VIRT表示的是虚拟内存地址空间的大小,RES是实际使用的内存数量。实际使用的内存数量远小于地址空间的大小。之前介绍的基于虚拟内存和page fault提供的功能在这都有所使用。


Interrupt硬件部分

中断对应的场景是:硬件需要得到操作系统的关注。操作系统会保存当前的工作,处理中断,再恢复先前的工作。与系统调用、page fault的过程非常相似,因此使用相同的机制。

中断和系统调用的差别主要有三点:

  • asynchronous:当硬件生成中断时,Interrupt handler 与当前运行的进程在 CPU 上没有任何关联;但系统调用发生在运行进程的 context 下。

  • concurrency:对中断来说,CPU 和生成中断的设备是并行运行的。

  • program device:每个设备都需要被编程,如网卡、UART。

我们在这里主要关注外部设备的中断,而非定时器中断或软件中断。主板上,各种线路将外设与 CPU 连接在一起,处理器通过PLIC(Platform Level Interrupt Control)来处理设备中断。

共有53个不同的来自设备的中断。中断到达 PLIC 后,PLIC 会路由这些中断,图的右下角是 CPU 的核,PLIC 会将中断路由到某一个 CPU 的核。如果所有 CPU 核都在处理中断,PLIC会保留中断直到有一个 CPU 核可以用来处理中断。因此 PLIC 需要保存一些数据来跟踪中断状态。


设备驱动概述

管理设备的代码称为驱动,所有驱动都在内核中。大部分驱动的代码都分为两个部分,bottom/top。这里以 UART 设备的驱动为例。

bottom部分通常是Interrupt handler,当中断送到 CPU 并被接收,CPU 会调用相应的Interrupt handler,它并不运行在任何特定进程的context中,它只处理中断。但也因此它存在一些限制,因为进程的page table并不知道应该从哪个地址读写数据,也就无法直接从Interrtupt handler读写数据。

top部分是用户进程或内核其他部分调用的接口。对 UART 是read/write接口。这部分通常与用户的进程交互并进行数据读写。

通常,驱动中会有一些队列,或者说 buffer。topbottom部分的代码都会从队列中读写数据。这里的队列可以将并行运行的设备和 CPU 解耦开。

对设备的编程通常是通过memory mapped I/O完成的。设备地址出现在物理地址的特定区间,这由主板制造商决定。操作系统知道这些设备位于物理地址空间的具体位置,然后通过load/store指令对这些地址进行编程,实际就是读写设备的控制寄存器来操作设备实现相应的行为。

16550 是 QEMU 模拟的 UART 设备,QEMU 用这个模拟的设备与键盘和 Console 进行交互。图中表明了芯片拥有的寄存器。例如控制寄存器000,写它可以将数据写入寄存器中,读它可以读出寄存器中的内容。UART 可以通过串口发送数据 bit,线路另一侧的 UART 芯片可以将数据 bit 重新组合成字节。以及控制寄存器001,可以通过它控制 UART是否产生中断。

当通过load将数据写入Transmit Holding Register,UART 芯片会通过串口线将这个 Byte 送出,完成发送后 UART 会生成一个中断给内核,这时才能写入下一个数据。上图的 UART 芯片有一个容量16的 FIFO。


xv6中设置中断

我们讨论$ls

xv6中,Shell会输出提示符$。实际过程是:设备会将字符传输给 UART 的寄存器,UART 在发送完字符后产生一个中断,在 QEMU 中,模拟的线路的另一端会有另一个模拟的 UART 芯片,这个芯片连接到虚拟的 Console,它会将$显示在 console 上。

ls是用户输入的字符。键盘连接到 UART 的输入线路,当键盘上一个按键被按下,UART 芯片会将按键字符通过串口线发送到另一端的 UART 芯片。它会先将数据 bit 合并成一个 Byte,然后再产生一个中断,并告诉处理器有一个来自键盘的字符,之后Interrupt handler会处理来自 UART 的字符。

RISC-V 有许多与中断有关的寄存器:

  • SIE寄存器:这个寄存器中有一个 bit (E) 专门针对外部设备的中断;另有一个 bit (S) 专门针对软件中断和一个 bit (T) 针对定时器中断。软件中断可能由一个 CPU核触发给另一个 CPU 核。

  • SSTATUS寄存器:这个寄存器中有一个 bit 来打开或关闭中断。

每个CPU核都有独立的 SIE 和 SSTATUS 寄存器。SIE 单独控制特定的中断,SSTATUS 控制所有的中断。

  • SIP寄存器:处理器通过这个寄存器查看是什么类型的中断。

  • SCAUSE寄存器:表明当前状态的原因是中断。

  • STVEC寄存器:保存当trappage fault或中断发生时,CPU 运行的用户程序的程序计数器。

start函数先将所有的中断都设置在Supervisor mode,然后设置 SIE 寄存器来接收外部设备、软件和定时器中断,之后初始化定时器。

main函数中,console是第一个外设。

consoleinit中,先初始化锁,然后调用uartinit,这个函数会配置好 UART 芯片以供使用。

uartinit的流程是先关闭中断,然后设置波特率(串口线的传输速率),设置字符长度为8 bit,并重置 FIFO,最后打开中断。

运行完uartinit后,UART 就可以生成中断了。但还没有对 PLIC 编程,因此中断不能被 CPU 感知。在main函数中还要调用plicinit函数。

PLIC 与外设一样,也占用了一个 I/O 地址。这里设置了 PLIC 会接收哪些中断,进而将中断路由到 CPU。在上面设置了 UART 和 IO磁盘的中断。

plicinit由0号 CPU 运行。之后,每个 CPU 的核都需要调用plicinithart函数表明可以处理哪些外设中断。上面每个 CPU 核都可以处理来自 UART 和 VIRTIO 的中断,中断的优先级被设置为0。

目前,生成中断的外部设备和传递中断给 CPU 的 PLIC 都设置好了,但 CPU 还没有设置好接收中断,需要设置 SSTATUS 寄存器。在main的最后调用了scheduler函数。

scheduler函数主要是运行进程。但在实际运行进程前会执行intr_on函数设置 SSTATUS 寄存器来使 CPU 能接收中断。

以上就是中断的基本配置。


UART驱动的top部分

接下来介绍如何从 Shell 程序输出提示符$到 Console。首先是init.c中的main函数,这是系统启动后运行的第一个进程。

main首先尝试以读写模式打开console设备,如果失败,调用mknod来创建设备节点;然后用open打开确保获取到文件描述符,这里是第一个打开的文件,所以是文件描述符0;再调用dup创建 stdout 和 stderr,这里实际上通过复制文件描述符0,分别得到1、2,这样就绑定了标准输入、输出和错误。

dup复制给定的文件描述符并返回新的文件描述符,这两个描述符指向同一个文件表项。

Shell 首先打开文件描述符0、1、2,然后向文件描述符2打印提示符$

这里代码是直接调用了write系统调用。也可通过fprintf(2, "$ "),由 Shell 输出的每个字符都会触发一个write系统调用。

write系统调用最终会走到sys_write

函数先检查参数,然后调用filewrite

filewrite中首先会判断文件描述符的类型。mknod生成的文件描述符属于设备FD_DEVICE,对这种类型,会为特定设备执行设备相应的write函数。这里会调用console.c中的consolewrite函数。

先通过either_copyin将字符拷入,然后调用uartputc函数将字符写入给 UART 设备。可以认为这个函数是一个 UART 驱动的top部分。

uartputc会实际打印字符。UART 内部有一个32字符大小的 buffer 用来发送数据,并有一个为 consumer 提供的读指针和为 producer 提供的写指针,构建出一个环形的 buffer。

在例子里,Shell 是 producer,uartputc首先判断环形 buffer 是否已满,如果已满,则会sleep一段时间,把 CPU 出让给其他进程。这里提示符$是我们送出的第一个字符,因此代码会往下走,把字符送到 buffer 中,更新写指针,然后调用uartstart函数。

uartstart通知设备执行操作。首先检查当前设备是否空闲,如果空闲,就从 buffer 中读出数据,将数据写入到 THR 发送寄存器。这相当于告诉设备,有一个字节需要发送。一旦数据送到设备,系统调用就会返回,用户应用程序 Shell 就可以继续执行。这里由内核返回用户空间的机制与trap相同。


UART驱动的bottom部分

向 Console 输出字符过程中,如果发生了中断,由于之前已经在 SSTATUS 寄存器中打开了中断,这里会触发中断。假设键盘生成了一个中断并发向 PLIC,PLIC 会将中断路由给一个特定的 CPU 核,如果该核设置了 SIE寄存器的E bit(针对外部中断),那么处理过程如下:

  • 首先清除 SIE 寄存器的相应 bit 位。这样可以组织 CPU 核被其他中断打扰。

  • 然后设备 SEPC 寄存器为当前的程序计数器。

  • 保存当前的 mode。

  • 将 mode 设置为 Supervisor mode。

  • 将程序计数器的值设置为 STVEC 的值。

对于 Shell 的例子,STVEC的值为uservec的地址,uservec又会调用usertrap处理中断。

devintr中,首先通过 SCAUSE 寄存器判断中断是否来自外设,若是,则调用plic_claim获取中断。

plic_claim中,当前 CPU 核会告诉 PLIC 自己要处理中断,PLIC_SCLAIM会将中断号返回。对于 UART来说,返回的中断号为10。

devintr函数中,如果是 UART中断,会调用uartintr函数。它会从 UART 的接收寄存器中读取数据,然后将获取的数据传递给consoleintr函数。不过这里目前还没有通过键盘输入任何数据,所以 UART 的接收寄存器为空。

所以代码会直接运行到uartstart函数,这个函数会将 Shell 存储在 buffer 中的任意字符送出。这样驱动的top部分就和bottom部分就解耦开了。

只有一个 UART 设备,一个 buffer 只针对一个 UART 设备,而这个 buffer 会被所有 的 CPU 核共享。因此需要锁来确保多个 CPU 核上的程序串行的向 Console 打印输出。


Interrupt相关的并发

与中断相关的并发包括:

  • 设备与 CPU 是并行运行的。例如,当 UART 向 Console 发送字符的时候,CPU 会返回执行 Shell,而 Shell 可能会再执行一次系统调用,向 buffer 中写入另一个字符。这里的并行称为producer-consumer并行。

  • 中断会停止当前运行的程序。对于一些内核代码来说,如果不能在执行期间被中断,则需要内核临时关闭中断,来确保这段代码的原子性。

  • 驱动的 top 和 bottom 部分是并行运行的。例如,Shell 会在传输完提示符$之后会再调用write系统调用传输空格字符,代码会走到 UART 驱动的 top 部分,将空格写入到 buffer 中。但同时在另一个 CPU 核,可能会收到来自 UART 的中断,进而执行驱动的 bottom 部分,查看相同的 buffer。所以一个驱动的这两部分可以并行的在不同 CPU 上运行,通过lock来管理并行。

这里主要关注第一点。驱动中会有 buffer,在前面 UART的例子中,buffer 是32自己大小,且有两个指针,分别是读指针和写指针。如果两个指针相等,buffer 是空的。当 Shell 调用uatrputc函数时,会将字符写入写指针位置并将写指针加1。

producer可以一直写入数据,直到写指针+1等于读指针。这时 buffer 满了,代码会 sleep,暂时搁置 Shell 并运行其他进程。

Interrupt handleruartintr函数在这个场景下是consumer。每当

有一个中断,且读指针落后于写指针,uartintr就会从读指针读取一个字符再通过 UART 设备发送,并将读指针加1,直到二者相等。


UART读取键盘输入

Shell 会调用read从键盘中读取字符,在read系统调用的最底层,会调用fileread函数,这个函数中,如果读取的文件类型是设备,会调用相应设备的read函数。

在这个例子中的就是consoleread函数。

这里也有一个 buffer,包含128个字符。

在这个场景下,Shell是consumer,从 buffer 中读取数据;而键盘是producer,将数据写入到 buffer 中。

consoleread中可以看到,当读写指针一样时,进程会sleep。因此,当 Shell 打印 $后,如果键盘没有输入,Shell 进程会sleep,直到键盘有字符输入,这会被发送到主板上的 UART 芯片,产生中断后再被 PLIC 路由到某个 CPU 核,然后触发devintr函数,通过uartgetc获取相应字符,再传递给consoleintr函数。

默认情况下,字符会通过consputc输出到 console 上给用户看,然后被存放在 buffer 中。遇到换行符时,唤醒之前sleep的进程,再从 buffer 中将数据读出。

这样可以通过 buffer 将consumerproducer解耦,使它们可以按照自己的速度独立并行运行。


Interrupt的演进

polling和Interrupt

Last updated