XV6通过定时器中断来维护时钟以及进行进程切换;在usertrap和kerneltrap中的yield函数会执行进程切换。定时器中断会由RISC-V CPU内部的时钟硬件产生。XV6对此时钟硬件进行编程以定期中断每个CPU。
RISC-V要求定时器中断必须在机器模式下执行,而不是在监管模式下执行。RISC-V的机器模式在无分页环境下执行,并且具有一系列单独的控制寄存器,因此在机器模式下运行普通的 xv6 内核代码是不切实际的。所有XV6的定时器中断处理程序是和陷阱机制完全分开的。
start.c中的代码执行于机器模式中,main函数之前,在timerinit函数中对定时器中断进行了设置:对CLINT硬件编程使其在一定时间后产生一次中断;设置scratch区域(类似于trapframe),帮助定时器中断处理程序保存寄存器和CLINT寄存器的地址。最后函数会设置mtvec为timervec函数地址并开启定时器中断。
定时器中断会在任何时候发生,内核在执行关键操作时也无法禁用定时器中断。因此定时器中断处理程序必须保证不会干扰被中断的内核代码执行。处理程序最基本的策略就是产生一个软件中断之后立即返回。产生的软件中断就可以通过通用的陷阱机制进行处理,并且可以进行关闭。软件中断的处理程序在devintr函数中。
机器模式的时钟中断向量为timervec,该函数保存了三个寄存器在start函数准备的scratch区域中,通知CLINT下一个中断的时刻,通过csrw sip, a1(a1为2)指令触发一个软件中断,最后恢复寄存器并返回。
真实操作系统XV6运行设备和时钟中断在内核执行时产生。定时器中断在中断处理程序中强制线程切换,即使是在内核态执行中。这个功能可以使得内核线程公平地获取CPU时间片,尤其是当内核线程耗费大量时间进行计算而不返回用户态。但是,这使得内核代码需要考虑到其可能会被暂停并在一段时间后再另一个CPU上恢复,而这给XV6带来了一定的复杂性。如果设备中断和定时器中断只在用户代码执行时运行触发,内核可以变得更加简单。
在许多操作系统中,驱动程序的代码量远远大于内核本身。要支持所有设备在计算机上运行是十分繁杂的工作:有大量设备需要支持,设备有很多特性,设备间的协议十分复杂并且缺少文档。
UART设备通过读取控制寄存器一次接收一个字节数据,这种模式称为程序I/O(programmed I/O),因为数据移动由软件驱动。这种方式十分简单但是在高速设备上是十分缓慢的。高速设备通常通过DMA方式来进行数据传输。DMA设备硬件可以直接对内存进行读写,现代硬盘和网络设备就是通过这种方式进行的。DMA设备驱动会在内存中准备数据,之后通过一次控制寄存器的写入告诉设备对准备好的数据进行处理。
当设备需要在无法预知但不太频繁的时间上需要进行处理时,中断是有效的。但是中断有很大的CPU开销。因此高速设备会使用一些技巧来减少中断次数。一个技巧就是对一整批的输入或输出请求发起一次中断。另一个是驱动完全禁用中断,转为定时查询设备是否需要处理,这种技术被称为轮询(polling)。如果设备操作执行非常频繁,那么轮询是有意义的,反之如果设备大部分时间都是空闲的,那么轮询会浪费CPU时间。一些驱动会根据设备负载自动切换轮询和中断。
UART驱动先拷贝输入数据到内核缓冲区,之后再拷贝到用户空间。这在低数据传输率的情况下是有效的,但是对于高速设备,两次拷贝会显著地降低性能。一些操作系统可以直接将数据在用户态缓冲区和设备硬件之间移动,通常是通过DMA。