驱动是操作系统中管理特定设备的代码,他有如下功能:1、配置设备相关的硬件,2、告诉设备需要怎样执行,3、处理设备产生的中断,4、与等待设备I/O的进程进行交互。驱动程序的代码写起来可能很棘手,因为驱动程序与它所管理的设备会同时执行。此外,驱动程序编写人员必须了解设备的硬件接口,但硬件接口可能是很复杂的,而且文档不够完善。
需要操作系统关注的设备通常可以被配置为产生中断,这是trap的一种类型。内核trap处理代码可以知道设备何时引发了中断,并调用驱动的中断处理程序;在xv6中,这个处理发生在devintr(kernel/trap.c:177)中。
许多设备驱动程序在两个context中执行代码:上半部分(top half)在进程的内核线程中运行,下半部分(bottom half)在中断时执行。上半部分是通过系统调用,如希望执行I/O的 read和write。这段代码可能会要求硬件开始一个操作(比如要求磁盘读取一个块);然后代码等待操作完成。最终设备完成操作并引发一个中断。驱动程序的中断处理程序,作为下半部分,推算出什么操作已经完成,如果合适的话,唤醒一个等待该操作的进程,并告诉硬件执行下一个操作。

5.1 Code: Console input

控制台驱动(kernel/console.c)是驱动结构的一个简单说明。控制台驱动通过连接到RISC-V上的UART串行端口硬件,接受输入的字符。控制台驱动程序每次累计一行输入,处理特殊的输入字符,如退格键和control-u。用户进程,如shell,使用read系统调用从控制台获取输入行。

5.2 Code: Console output

有一个通用模式需要注意,设备活动和进程活动需要解耦,这将通过缓冲和中断来实现。控制台驱动程序可以处理输入,即使没有进程等待读取它;随后的读取将看到输入。同样,进程可以发送输出字节,而不必等待设备。这种解耦可以通过允许进程与设备I/O并发执行来提高性能,当设备速度很慢(如UART)或需要立即关注(如打印键入的字节)时,这种解耦尤为重要。这个idea有时被称为I/O并发(I/O concurrency)

5.3 Concurrency in drivers

你可能已经注意到在consolereadconsoleintr中会调用acquireacquire调用会获取一个锁,保护控制台驱动的数据结构不被并发访问。这里有三个并发风险:不同CPU上的两个进程可能会同时调用consoleread;硬件可能会在一个CPU正在执行consoleread时,向该CPU抛出一个控制台(实际上是UART)中断;硬件可能会在consoleread执行时向另一个CPU抛出一个控制台中断。这些危险可能导致竞争条件或死锁。第6章探讨锁如何在这些情况下提供帮助。
需要关注驱动并发安全的另一个原因是,一个进程可能正在等待来自设备的输入,但是此时该进程已经没有在运行(被切换),输入的中断信号可能会到达。因此,中断处理程序不允许知道被中断的进程或代码。例如,一个中断处理程序不能安全地用当前进程的页表调用copyout。中断处理程序通常只做相对较少的工作(例如,只是将输入数据复制到缓冲区),并唤醒上半部分代码来做剩下的工作。

5.4 Timer interrupts

Xv6使用定时器中断来维护它的时钟,并使它能够在受计算限制的进程之间切换;usertrapkerneltrap中的yield调用会导致这种切换。每个RISC-V CPU的时钟硬件都会抛出时钟中断,定时器中断来自连接到每个RISC-V CPU的时钟硬件。Xv6对这个时钟硬件进行编程,使其周期性地中断相应的CPU。

RISC-V要求在机器模式下处理定时器中断,而不是监督者模式。RISCV机器模式执行时没有分页,并且有一套单独的控制寄存器,因此在机器模式下运行普通的xv6内核代码是不切实际的。因此,xv6对定时器中断的处理完全独立于上面谈到的trap机制。

在main执行之前的start.c中的代码是在机器模式下执行的,设置了接收定时器中断(kernel/start.c:62)。一部分工作是对CLINT硬件(core-local interruptor)进行编程,使其每隔一定时间产生一次中断。另一部分是设置一个类似于trapframescratch区域,帮助定时器中断处理程序保存寄存器和CLINT寄存器的地址。最后,startmtvec设置为timervec,并启用定时器中断。

定时器中断可能发生在用户或内核代码执行的任何时候;内核没有办法在关键操作中禁用定时器中断。因此,定时器中断处理程序必须以保证不干扰被中断的内核代码的方式进行工作。基本策略是处理程序要求RISC-V引发一个软件中断并立即返回。RISC-V用普通的trap机制将软件中断传递给内核,并允许内核禁用它们。处理定时器中断产生的软件中断的代码可以在devintr(kernel/trap.c:204)中看到。

机器模式的定时器中断处理程序是timervec(kernel/kernelvec.S:93)。它在start准备的scratch区域保存一些寄存器,告诉CLINT何时产生下一个定时器中断,使RISC-V产生一个软件中断,恢复寄存器,然后返回。在定时器中断处理程序中没有C代码。

5.5 Real world

Xv6允许在内核和用户程序执行时使用设备和定时器中断。定时器中断可以强制从定时器中断处理程序进行线程切换(调用yield),即使是在内核中执行。如果内核线程有时在不返回用户空间的情况下花费大量时间进行计算,那么在内核线程之间公平地对CPU进行时间切片(time-slice)的能力是很有用的。然而,内核代码需要注意它可能会被暂停(由于定时器中断),然后在不同的CPU上恢复,这是xv6中一些复杂性的根源(参见第6.6节)。如果设备和定时器中断只发生在执行用户代码时,内核可以变得更简单一些。

完全支持一台典型计算机上的所有设备是非常困难的,因为有很多设备,这些设备有很多功能,而且设备和驱动程序之间的协议可能很复杂,文档也不完善。在许多操作系统中,驱动程序占用的代码比核心内核还多。

UART驱动器通过读取UART控制寄存器,一次读取一个字节的数据;这种模式被称为编程I/O(programmed I/O),因为软件在控制数据移动。编程I/O简单,但速度太慢,无法在高数据速率下使用。需要高速移动大量数据的设备通常使用直接内存访问__(direct memory access, DMA)。DMA设备硬件直接将传入数据写入RAM,并从RAM中读取传出数据。现代磁盘和网络设备都使用DMA。DMA设备的驱动程序会在RAM中准备数据,然后使用对控制寄存器的一次写入来告诉设备处理准备好的数据。

当设备在不可预知的时间需要关注时,中断是很有用的,而且不会太频繁。但中断对CPU的开销很大。因此,高速设备,如网络和磁盘控制器,使用了减少对中断需求的技巧。其中一个技巧是对整批传入或传出的请求提出一个单一的中断。另一个技巧是让驱动程序完全禁用中断,并定期检查设备是否需要关注。这种技术称为轮询(polling。如果设备执行操作的速度非常快,轮询是有意义的,但如果设备大部分时间处于空闲状态,则会浪费CPU时间。一些驱动程序会根据当前设备的负载情况,在轮询和中断之间动态切换。

UART驱动首先将输入的数据复制到内核的缓冲区,然后再复制到用户空间。这在低数据速率下是有意义的,但对于那些快速生成或消耗数据的设备来说,这样的双重拷贝会大大降低性能。一些操作系统能够直接在用户空间缓冲区和设备硬件之间移动数据,通常使用DMA。

正如第1章中提到的,控制台对应用程序来说是一个常规的文件,应用程序使用read和write系统调用来读取输入和写入输出。应用程序可能希望控制无法通过标准文件系统调用表达的设备的各个方面(例如,在控制台驱动程序中启用/禁用行缓冲)。对于这种情况,Unix操作系统支持ioctl系统调用。

计算机的某些使用要求系统必须在有限的时间内响应。例如,在安全关键系统(safety-critical systems)中,错过最后期限(deadline)可能会导致灾难。Xv6不适合硬实时设置。硬实时操作系统往往是以允许分析确定最坏情况响应时间的方式与应用程序链接的库。Xv6也不适合软实时应用程序,当偶尔错过最后期限是可以接受的,因为xv6的调度器太简单了,而且它有长时间禁用中断的内核代码路径。