xv6 处理字符输入的过程

    当用户输入一个字符后,UART设备会向RISC-V请求抛出一个中断,进而调用xv6的陷阱处理函数。陷阱处理函数随后调用`devintr`函数。该函数通过RISC-V的`scause`寄存器获知中断来源于外部设备。然后,`devintr`会向名为PLIC的硬件单元询问中断的设备是什么。如果中断来自UART,则调用`uartint`函数进行处理。

    `uartint`函数读取UART硬件中所有等待输入的字符,并将它们发送给`consoleintr`。该函数不会等待输入,因为所有新输入都会产生新的中断。`consoleintr`的工作是将输入字符存放在`cons.buf`中,直到处理完整个行。`consoleintr`会对退格键和其他字符进行特殊处理。处理完整行后,`consoleintr`会唤醒当前等待的`consoleread`(如果存在)。

    一旦唤醒,`consoleread`会从`cons.buf`中读取一行,并将其拷贝到用户空间,然后返回到用户空间。

xv6 处理字符输出的过程

    对指向控制台的文件描述符进行的 `write` 系统调用最终会到达 `uartputc`。设备驱动程序维护了一个输出缓冲区(`uart_tx_buf`),使得 `uartputc` 函数不需要等待 UART 设备完成输出。相反,该函数将字符放入缓冲区,调用 `uartstart` 以使设备开始传输并返回。只有在缓冲区已满的情况下,`uartputc` 函数才会等待。

    每当 UART 传输完一个字节时,它都会抛出一个中断。随后,`uartintr` 调用 `uartstart`,该函数会检查设备是否确实完成了输出并将缓冲区中的下一个字符传输给输出设备。因此,如果进程向控制台输出了多个字符,第一个字节一般会被 `uartputc` 对 `uartstart` 的调用输出。当表明传输完成的中断被抛出后,剩下的被存入缓冲区的字符会被由 `uartstart` 对 `uartintr` 的调用输出。

    一个值得注意的通用模式是通过缓冲和中断实现的设备活动与进程活动的解耦。控制台驱动程序可以照常处理输入,即使当前没有进程在等待输入,且之后的读取仍然可以获取输入。同样,进程可以照常发送输出,而不需要等待设备。通过这种解耦,进程和设备 I/O 可以并发地执行,从而提高了系统性能。这一点在设备(或 UART)比较缓慢或设备需要得到系统即刻的注意时格外重要。这种想法有时被称为 *I/O 并发性*。

驱动中的并发

    你可能已经注意到 `consoleread` 和 `consoleintr` 中对 `acquire` 的调用。这些调用会请求一个锁,以保护控制台的数据结构不受并发访问的影响。并发可能会引起三种危险:首先,两个在不同 CPU 上运行的进程可能同时调用 `consoleread`;其次,即使 CPU 已经在 `consoleread` 中执行代码,硬件可能还要求 CPU 抛出一个控制台(实际上是 UART)中断;最后,设备可能在 `consoleread` 运行中使另一个 CPU 抛出控制台中断。这些危险可能会导致竞争条件或死锁。第六章将更深入地探讨这些问题,并介绍锁是如何定位这些问题的。

记时器中断

xv6 通过计时器中断来维护它的时钟,并通过 usertrapkerneltrap 中对 yield 的调用帮助它进行进程间切换。每个 RISC-V CPU 中的时钟硬件会发出计时器中断,xv6 对这些硬件进行编程,以使每个 CPU 周期性地产生中断。

    RISC-V 在机器模式中处理所有计时器中断,而不是在监管模式中处理。RISC-V 的机器模式执行时不启用分页机制,并使用单独的控制寄存器。因此,在机器模式中运行普通的 xv6 内核代码是不实际的。因此,xv6 处理计时器中断的方式与之前提到的陷阱机制完全独立。

     在 `main` 函数之前,运行在机器模式中的 `start.c` 负责准备接收计时器中断。其中的一部分工作是对 CLINT 硬件进行编程,以特定的延迟抛出中断。另一部分工作是设置一个类似于陷阱帧的 *scratch* 区域,以助于计时器中断处理器保存寄存器和 CLINT 寄存器的地址。最后, `start` 将 `mtvec` 设置为 `timevec` 以启用计时器中断。

    计时器中断可能发生于用户或内核代码执行的任何时刻。在执行关键操作时,内核没有任何禁用计时器中断的方法。因此,计时器中断处理器必须以不干扰被中断的内核代码的方式完成自己的工作。一种基本的策略是让处理器向 RISC-V 请求抛出一个“软件中断”,并立即返回。然后,RISC-V 使用一般的陷阱机制将软件中断传递给内核,同时允许内核禁用它们。处理由计时器中断产生的软件中断的代码位于 `devintr(kernel/trap.c:204)`。

    在机器模式中处理计时器中断的处理器是 `timervec(kernel/kernelvec.S:93)`。这段代码将一些寄存器保存到由 `start` 准备好的 *scratch* 区域,告知 CLINT 下次产生计时器中断的时间,向 RISC-V 请求抛出一个软件中断,恢复寄存器并返回。在计时器中断处理器中没有任何 C 语言代码。

现实

    Xv6 中的 UART 驱动通过读取 UART 的控制寄存器每次读取一个字节的数据,这种方式被称作*编程 I/O*,因为此时是软件在控制数据的读写的。编程 I/O 比较简单,但无法用于高速情境下的读写。