Interrupts

  • 中断与系统调用区别
    • asynchronous,异步,与当前运行在 CPU 的进程无关
    • concurrency,CPU 和产生中断的设备并行运行
    • program device,需要关注外部设备

中断处理——硬件

  • PLIC 会通知当前有一个待处理的中断
  • 其中一个 CPU 核会 Claim 接收中断,这样 PLIC 就不会把中断发给其他的 CPU 处理
  • CPU 核处理完中断之后,CPU 会通知 PLIC
  • PLIC 将不再保存中断的信息

中断处理——软件

使用驱动管理设备

  • bottom:通常是 Interrupt handler。当一个中断送到了 CPU,并且 CPU 设置接收这个中断,CPU 会调用相应的 Interrupt handler。Interrupt handler 并不运行在任何特定进程的 context 中,它只是处理中断。

  • top:是用户进程,或者内核的其他部分调用的接口。对于 UART 来说,这里有 read/write 接口,这些接口可以被更高层级的代码调用。

  • 驱动中会有一些队列(或者说 buffer),top 部分的代码会从队列中读写数据,而 Interrupt handler(bottom 部分)同时也会向队列中读写数据。这里的队列可以将并行运行的设备和 CPU 解耦开来

  • 我们通过 load 将数据写入到 Transmit Holding Register 中,之后 UART 芯片会通过串口线将这个 Byte 送出。当完成了发送,UART 会生成一个中断给内核,这个时候才能再次写入下一个数据。
  • 所以内核和设备之间需要遵守一些协议才能确保一切工作正常。上图中的 UART 芯片会有一个容量是 16 的 FIFO,但是你还是要小心,因为如果阻塞了 16 个 Byte 之后再次写入还是会造成数据覆盖

键入字符中断过程

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

中断相关寄存器

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

  • SSTATUS:有一个 bit 来打开或者关闭中断。每一个 CPU 核都有独立的 SIE 和 SSTATUS 寄存器,除了通过 SIE 寄存器来单独控制特定的中断,还可以通过 SSTATUS 寄存器中的一个 bit 来控制所有的中断。

  • SIP(Supervisor Interrupt Pending)寄存器。当发生中断时,处理器可以通过查看这个寄存器知道当前是什么类型的中断。

  • SCAUSE 寄存器,这个寄存器我们之前看过很多次。它会表明当前状态的原因。

  • STVEC 寄存器,它会保存当 trap,page fault 或者中断发生时,CPU 运行的用户程序的程序计数器,这样才能在稍后恢复程序的运行。

控制台初始化

  • uartinit 函数,关闭中断,设置波特率,设置字符长度为 8bit,重置 FIFO,最后重新打开中断

  • 需要让中断被 CPU 感知,需要调用 plicinit 函数

    • PLIC 与外设一样,也占用了一个 I/O 地址(0xC000_0000
  • plicinithart 函数。

    • plicinit 是由 0 号 CPU 运行,之后,每个 CPU 的核都需要调用 plicinithart 函数表明对于哪些外设中断感兴趣。

    • 每个 CPU 的核都表明自己对来自于 UART 和 VIRTIO 的中断感兴趣。因为我们忽略中断的优先级,所以我们将优先级设置为 0

  • 设置 SSTATUS 寄存器,接收中断

    • scheduler 函数中开中断

uart driver top

  • consolewrite()

    • either_copyin 将字符拷入,之后调用 uartputc 函数。uartputc 函数将字符写入给 UART 设备,所以你可以认为 consolewrite 是一个 UART 驱动的 top 部分
  • 在 UART 的内部会有一个 buffer 用来发送数据,buffer 的大小是 32 个字符。同时还有一个为 consumer 提供的读指针和为 producer 提供的写指针,来构建一个环形的 buffer

  • uartstart()

    • 首先是检查当前设备是否空闲,如果空闲的话,我们会从 buffer 中读出数据,然后将数据写入到 THR(Transmission Holding Register)发送寄存器。这里相当于告诉设备,我这里有一个字节需要你来发送。

uart driver bottom

  • plic_claim 函数位于 plic.c 文件中。在这个函数中,当前 CPU 核会告知 PLIC,自己要处理中断,PLIC_SCLAIM 会将中断号返回,对于 UART 来说,返回的中断号是 10。

  • 位于 uart.c 文件的 uartintr 函数,会从 UART 的接受寄存器中读取数据,之后将获取到的数据传递给 consoleintr 函数。没有通过键盘输入任何数据,所以 UART 的接受寄存器为空,调用 uartstart 将 buffer 中的字符送出

中断并发

  • 设备和 CPU 并行,当 UART 向 Console 发送字符的时候,CPU 会返回执行 Shell,而 Shell 可能会再执行一次系统调用,向 buffer 中写入另一个字符,这些都是在并行的执行。这里的并行称为 producer-consumer 并行。
  • 中断停止运行当前程序,如果不能在执行期间被中断,这时内核需要临时关闭中断,来确保这段代码的原子性
  • 驱动的 top 和 bottom 部分是并行运行的,所以一个驱动的 top 和 bottom 部分可以并行的在不同的 CPU 上运行。这里我们通过 lock 来管理并行。因为这里有共享的数据,我们想要 buffer 在一个时间只被一个 CPU 核所操作

producer-consumer 并发

  • producer 可以一直写入数据,直到写指针 + 1 等于读指针

  • uartintr 函数是 consumer,每当有一个中断,并且读指针落后于写指针,uartintr 函数就会从读指针中读取一个字符再通过 UART 设备发送,并且将读指针加 1