中断与设备驱动程序

什么是中断

中断对应的场景很简单,就是硬件想要得到操作系统的关注。
操作系统需要做的是,保存当前的工作,处理中断,处理完成之后再恢复之前的工作。这里的保存和恢复工作,与我们之前看到的系统调用过程非常相似。所以系统调用,page fault,中断,都使用相同的机制。

  • 中断与系统调用的区别
  1. asynchronous(异步性):当硬件生成中断时,Interrupt handler与当前运行的进程在CPU上没有任何关联。而系统调用会发生在运行进程的context下。
  2. concurrency(并发):CPU和设备之间是真正的并行的。
  3. program device:设备需要被编程。
  • PLIC

所有的设备都连接到处理器上,处理器上是通过Platform Level Interrupt Control,简称PLIC来处理设备中断。PLIC会路由这些中断,PLIC会将中断路由到某一个CPU的核。如果所有的CPU核都正在处理中断,PLIC会保留中断直到有一个CPU核可以用来处理中断。

什么是设备驱动

通常来说,管理设备的代码称为驱动,所有的驱动都在内核中。我们今天要看的是UART设备的驱动,代码在uart.c文件中。

大部分驱动都分为两个部分,bottom 和 top。

  • bottom部分
    通常是Interrupt handler。中断处理程序并不运行在任何特定进程的上下文 中,它只是处理中断。

  • top部分
    是用户进程,或者内核的其他部分调用的接口。例如 readwrite

通常情况下,驱动中会有一些队列(或者说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将调用uartintruartintr将读取从UART硬件中写入的字符然后将其传送给consoleintrconsoleintr将积累这些字符直到整行都已经被读取,然后将唤醒仍在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传送多个字符时,第一个字符由uartputcuartstart的调用传送,后面的字符由uartintruartstart的调用进行传送。

UART驱动的top部分

当XV6启动时,Shell会输出提示符“$ ”,如果我们在键盘上输入ls,最终可以看到“$ ls”。我们接下来通过研究Console是如何显示出“$ ls”,来看一下设备中断是如何工作的。

首先,系统启动后运行第一个进程init,这个进程会创建一个Console设备。然后再进行两次dup后,文件描述符0,1,2都指向了这个Console设备。随后这个进程会fork,然后子进程进入shell。

在shell的getcmd函数中:

1
fprintf(2,"$ ");

执行write系统调用,sys_write函数又会调用file.c中的filewrite函数。
这个filewrite函数判断文件的类型,(这里还会用到argfd函数,用一个struct file*的类型获取fd的文件描述符指向的文件)。
filewrite函数发现这个文件类型是属于一个设备后,就会为这个特定的设备执行相应的write函数。

1
ret = devsw[f->major].write(1,addr,n);

因为设备是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
2
3
4
5
6
7
8
9
10
uartintr(void){
while(1){
int c = uartgetc();
if(c == -1) break;
consoleintr();
}
acquire(&uart_tx_lock);
uartstart();
release(&uart_tx_lock);
}

由于我们没有在键盘上敲下任何一个键(这里纯粹说一下”$”是如何被处理的),所以会直接执行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并等待另一个追上来。


中断与设备驱动程序
http://example.com/2024/02/29/操作系统/xv6-labs/中断设备驱动程序/
作者
LiuZhaocheng
发布于
2024年2月29日
许可协议