中断与设备驱动程序
什么是中断
中断对应的场景很简单,就是硬件想要得到操作系统的关注。
操作系统需要做的是,保存当前的工作,处理中断,处理完成之后再恢复之前的工作。这里的保存和恢复工作,与我们之前看到的系统调用过程非常相似。所以系统调用,page fault,中断,都使用相同的机制。
- 中断与系统调用的区别
- asynchronous(异步性):当硬件生成中断时,Interrupt handler与当前运行的进程在CPU上没有任何关联。而系统调用会发生在运行进程的context下。
- concurrency(并发):CPU和设备之间是真正的并行的。
- program device:设备需要被编程。
- PLIC
所有的设备都连接到处理器上,处理器上是通过Platform Level Interrupt Control,简称PLIC来处理设备中断。PLIC会路由这些中断,PLIC会将中断路由到某一个CPU的核。如果所有的CPU核都正在处理中断,PLIC会保留中断直到有一个CPU核可以用来处理中断。
什么是设备驱动
通常来说,管理设备的代码称为驱动,所有的驱动都在内核中。我们今天要看的是UART设备的驱动,代码在uart.c文件中。
大部分驱动都分为两个部分,bottom 和 top。
bottom部分
通常是Interrupt handler。中断处理程序并不运行在任何特定进程的上下文 中,它只是处理中断。top部分
是用户进程,或者内核的其他部分调用的接口。例如read
和write
通常情况下,驱动中会有一些队列(或者说buffer),top部分的代码会从队列中读写数据,而Interrupt handler(bottom部分)同时也会向队列中读写数据。这里的队列可以将并行运行的设备和CPU解耦开来。
- memory mapped I/O
I/O设备的寄存器被映射到系统内存地址空间的一段地址范围内,程序可以通过读写内存地址的方式来进行对I/O设备的控制和访问。
在xv6中设置中断
Console input
当用户输入一个字符后,UART硬件将产生一个中断,这个中断将触发xv6进入trap,随后调用devintr
来通过scause
寄存器判断是外部设备触发了这个中断,然后硬件将调用PLIC判断是哪个外部设备触发了这个外部中断,如果是UART触发的,devintr
将调用uartintr
。uartintr
将读取从UART硬件中写入的字符然后将其传送给consoleintr
,consoleintr
将积累这些字符直到整行都已经被读取,然后将唤醒仍在sleep的consoleread
。当consoleread
被唤醒后,将这一行命令复制给user space然后返回。
Console output
对console上的文件描述符进行write
system call,最终到达kernel/uart.c的uartputc
函数。输出的字节将缓存在uart_tx_buf
中,这样写入进程就不需要等待UART硬件完成字节的发送,只要当这个缓存区满了的情况下uartputc
才会等待。当UART完成了一个字符的发送之后,将产生一个中断,uartintr
将调用uartstart
来判断设备是否确实已经完成发送,然后将下一个需要发送的字符发送给UART。因此让UART传送多个字符时,第一个字符由uartputc
对uartstart
的调用传送,后面的字符由uartintr
对uartstart
的调用进行传送。
UART驱动的top部分
当XV6启动时,Shell会输出提示符“$ ”,如果我们在键盘上输入ls,最终可以看到“$ ls”。我们接下来通过研究Console是如何显示出“$ ls”,来看一下设备中断是如何工作的。
首先,系统启动后运行第一个进程init,这个进程会创建一个Console设备。然后再进行两次dup后,文件描述符0,1,2都指向了这个Console设备。随后这个进程会fork,然后子进程进入shell。
在shell的getcmd
函数中:
1 |
|
执行write系统调用,sys_write
函数又会调用file.c中的filewrite
函数。
这个filewrite
函数判断文件的类型,(这里还会用到argfd
函数,用一个struct file*
的类型获取fd的文件描述符指向的文件)。filewrite
函数发现这个文件类型是属于一个设备后,就会为这个特定的设备执行相应的write
函数。
1 |
|
因为设备是Console,所以会调用console.c中的consolewrite
函数。
consolewrite是UART驱动的top部分
consolewrite
会调用uartputc
函数,首先把数据存在一个缓冲区里uart_tx_buf
中。
再然后会调用uartstart
函数,通知UART设备执行操作。取出数据放入THR发送寄存器。
一旦数据送到了设备,系统调用会返回,用户应用程序Shell就可以继续执行。与此同时,UART设备会将数据送出。
UART连接了两个设备,一个是键盘,另一个是显示设备,也就是Console。
然后呢。。。会发生中断。
UART驱动的bottom部分
trap.c的devintr
函数中,首先会通过SCAUSE
寄存器判断当前中断是否是来自于外设的中断。如果是的话,再调用plic_claim函数来获取中断。如果是UART中断,那么会调用uartintr
函数。
1 |
|
由于我们没有在键盘上敲下任何一个键(这里纯粹说一下”$”是如何被处理的),所以会直接执行uartsatrt函数。
- 小疑问:为什么interrupt和write都会调用了uartstart函数?
首先,如果是write多个字节的话,第一次通过uartputc调用uartstart,把第一个字节发送出去。第一个字节发送完成后,会产生中断,紧接着把余下的字节都发送出去。
A general pattern to note is the decoupling of device activity from process activity via buffering and interrupts.
Interrupt handler,也就是uartintr函数,在这个场景下是consumer,每当有一个中断,并且读指针落后于写指针,uartintr函数就会从读指针中读取一个字符再通过UART设备发送,并且将读指针加1。当读指针追上写指针,也就是两个指针相等的时候,buffer为空,这时就不用做任何操作。
UART读取键盘输入
类似的,shell会调用read
从键盘读取字符。再会调用fileread
函数,如果文件类型是设备,在这里是console
设备,所以会调用consoleread
函数。这里也有一个Buffer。
大体流程如下:
假设用户通过键盘输入了“l”,这会导致“l”被发送到主板上的UART芯片,产生中断之后再被PLIC路由到某个CPU核,之后会触发devintr
函数,devintr
可以发现这是一个UART中断,然后通过uartgetc
函数获取到相应的字符,之后再将字符传递给consoleintr
函数。
默认情况下,字符会通过consputc
,输出到console上给用户查看。之后,字符被存放在buffer中。在遇到换行符的时候,唤醒之前sleep的进程,也就是Shell,再从buffer中将数据读出。
所以这里也是通过buffer将consumer和producer之间解耦,这样它们才能按照自己的速度,独立的并行运行。如果某一个运行的过快了,那么buffer要么是满的要么是空的,consumer和producer其中一个会sleep并等待另一个追上来。