Chapter5
Interrupts and device drivers
驱动程序driver
是操作系统中管理特定设备的代码。它配置设备硬件,指示设备执行操作,处理产生的中断,并与可能正在等待设备I/O的进程交互。
需要操作系统关注的设备通常被配置为可以生成中断,这是trap
的一种。kernel trap
处理代码会识别设备何时引发了中断,并调用驱动程序的中断处理程序。xv6
中,这种调度发生在devintr()
函数。
在设备驱动程序中,代码通常分为两个部分执行:一个是在进程的内核线程中运行的Top Half,另一个是在中断时执行的Bottom Half。顶部部分通过系统调用来调用,这些系统调用希望设备执行I/O操作,然后等待操作完成并引发中断。底部部分为中断处理程序,确定已经完成的操作,如果合适则唤醒等待的进程,并告诉硬件开始处理等待着的下一个操作。
Code:Console input
代码:控制台输入
控制台驱动程序console.c
是驱动程序结构的一个示例。它通过连接到 RISC-V 的 UART 串口硬件接受输入的字符。控制台驱动程序一次累积一行输入,用consoleintr
处理特殊输入字符。用户进程使用read
系统调用从控制台获取输入行,驱动程序通过 QEMU 模拟的 16550芯片与 UART 硬件通信。
UART
UART 硬件对软件来说表现为一组内存映射的控制寄存器。RISC-V 将一些物理地址映射到 UART 设备,因此 load 和 store 会直接与设备硬件交互,而非 RAM 。
UART 的内存映射地址从0x10000000
开始。其中有一些控制寄存器,内存映射地址计算为#define Reg(reg) ((volatile unsigned char *)(UART0 + (reg)))
。
举例来说,LSR 寄存器包含指示是否有输入字符等待读取的位,这些字符从 RHR 寄存器读取,每读取一个字符,硬件会将其从等待字符的内部 FIFO 中删除,并在 FIFO 为空时清除 LSR 中的这个位。
xv6
的main
调用kernel/console.c/consoleinit()
来初始化 UART 硬件。UART 在接收到每个输入字节时生成接收中断,在完成发送每个输出字节时生成发送中断。
控制台输入
xv6 shell
通过文件描述符从控制台读取数据,read
系统调用通过内核会到达consoleread
。它等待输入通过中断到达并从缓冲区cons.buf
读取字符,将输入复制到用户空间,并在整行到达后返回用户进程。
当用户输入一个字符时,UART 硬件请求 RISC-V 引发一个中断,激活trap handler
程序,它调用devintr
,查看 scause 寄存器,发现中断来自外部设备,然后请求 PLIC[1] 硬件单元得知哪个设备引发中断。如果是 UART,decintr
调用uartintr
。
这个函数读取正在等待的输入字符并传递给consoleintr
。
consoleintr
的任务是将输入字符累积到cons.buf
中,直到一整行到达。这个函数里面有一下退格键和其他字符的特别处理,当换行符到达,它会唤醒等待的consoleread
。
consoleread
在检验cons.buf
有一整行数据后,会将其复制到用户空间,并通过系统调用机制返回用户空间。
Code:Console output
代码:控制台输出
对连接到控制台的文件描述符进行write
系统调用最终会到达uartputc
。
设备驱动程序维护一个输出缓冲区uart_tx_buf
,使得write
进程不必等待 UART 完成发送。
uartputc
将每个字符加到缓冲区,然后调用uartstart
启动设备传输。
每次 UART 完成发送一个字节,它会生成一个中断,uartintr
调用uartstart
,检查设备是否真的已经完成发送,并将下一个缓冲区的字符交给设备。
如果进程向控制台写入多个字节,通常第一个字节由uartputc
调用uartstart
发送,其余的缓冲区字节在传输完成,触发中断时由uartintr
调用uartstart
发送。
通过缓冲区和中断,实现了设备活动和进程活动的解耦。二者可以并发来提高性能,这种思想也被称为I/O concurrency。
Concurrency in drivers
驱动程序中的并发性
concoleread
和consoleintr
中都有acquire
语句,这些调用获取了一个锁,用于保护控制台驱动程序的数据结构免受并发访问。这里有三种并发危险:不同 CPU 上的两个进
程可能同时调用consoleread
;硬件可能在一个 CPU 已经在执行consoleread
时要求该 CPU 传递一个控制台(实际上是 UART)中断;以及硬件可能在consoleread
执行时在另一个 CPU 上传递控制台中断。
并发性在驱动程序中需要注意的另一个例子是,一个进程正在等待设备输入,但输入到达的中断信号可能在不同的进程运行时到达。因此,中断处理程序不允许考虑当前进程的状态,必须在独立于当前进程的上下文运行。因此中断处理程序通常只做较少的工作,并唤醒top half来完成其他工作。
Timer interrupts
定时器中断
xv6
使用定时器中断来维护其时钟,并使其能在计算密集型进程间切换。切换在usertrap
和kerneltrap
中用yield
实现。xv6
对连接到RISC-V CPU 的时钟硬件编程,使其定期中断每个 CPU。
定时器中断必须在machine mode
下处理。这种模式在没有分页的情况下执行,使用一组独立的控制寄存器。因此定时器中断的处理机制与其他的trap
处理机制完全独立。
不是太懂,代码中是直接把所有异常和中断都委托给了 supervisor mode。
在start.c/timerinit()
中配置时钟中断。部分配置是对 CLINT (Core Local Interruptor) 硬件进行编程。
CLINT 通过比较stimecmp
和time
寄存器来生成中断,当time >= stimecmp
时触发。
另外部分是保存寄存器,在kernelvec.S
中。
定时器中断可以在任何时候发送,内核无法禁用。因此,定时器中断处理程序必须保证不会干扰被中断的内核代码。解决方案是让处理程序请求 RISC-V 引发一个 software interrupt
并立即返回,RISC-V通过正常的trap
机制将software
传递给内核,并允许内核禁用它们。
Real world
xv6
允许在执行内核代码和用户程序时发送设备和定时器中断。定时器中断强制从处理程序中进行线程切换。
UART 驱动程序通过读取控制寄存器一次检索一字节的数据,这种模式称为programmed I/O,但这种模式速度太慢。需要高速传输大量数据的设备通常使用直接内存访问(DMA),DMA 设备硬件直接将传入数据写入 RAM,并从中读取传出数据。其驱动程序会写入控制寄存器告诉设备处理准备好的数据。
中断的 CPU 开销很高,因此高速设备会用一些技巧来减少中断的需求。一个技巧是为整个批量的传入或传出请求引发单个中断。另一个技巧是驱动程序完全禁用中断,并定期检查设备以查看是否需要关注。这种技术称为polling
。有一些驱动会在 polling 和 interrupts 之间动态切换。
Last updated