Chapter5 Interrupts and device drivers
driver
操作系统中管理一个特定设备的代码。
- 配置设备硬件;
- 告诉设备执行操作;
- 处理由此产生的中断;
- 与可能正在等待来自设备的I/O的处理进行互动;
设备驱动程序在两种情况下执行代码:
- 上半部分在进程的内核线程中运行。
- 通过系统调用,如读和写,希望设备执行I/O;
- 驱动程序要求硬件启动一个操作;
- 然后驱动代码等待操作完成;
- 设备完成操作并提出一个中断(陷阱);
- 内核陷阱代码处理这个陷阱并调用驱动程序的中断处理程序;
- 在中断时间执行的下半部分。
- 弄清楚什么操作已经完成;
- 唤醒相应的等待进程;
- 告诉硬件在任何等待的下一个操作上开始工作;
Console Input
console driver是一个设备驱动。通过UART串口接受输入的符号。UART硬件对于进程来说是一组memory-mapped寄存器,即RISC-V上有一些物理地址是直接和UART设备(如键盘和屏幕)相连的。也就是console driver是从UART中读写相关的数据,console直接交互的对象是UART,那么UART里面的各种寄存器的状态也都是由console来管理,包括读写,初始化等操作。UART的地址从0x10000000或UART0开始,每个UART控制寄存器的大小为1字节。
- console就是uart的device driver
- shell是一个程序,负责与kernel内的函数进行交互
xv6的main函数将调用consoleinit来初始化UART硬件,使得UART硬件在接收到字节或传输完成一个字节时发出中断。
xv6 shell程序通过user/init.c开启的文件描述符来从console读取字节(在while循环中调用getcmd,在其中调用gets,再调用readsystem call)。在kernel中调用consoleread,等待输入完毕之后的中断,然后将输入缓存在cons.buf中,将输入either_copyout到user space后返回用户进程。如果用户没有输入完整的一行,则读取进程将在sleepsystem call中等待。
当用户输入了一个字符后,UART硬件将产生一个中断,这个中断将触发xv6进入trap。trap handler将调用devintr来通过scause寄存器判断是外部设备触发了这个中断,然后硬件将调用PLIC来判断是哪个外部设备触发的这个中断,如果是UART触发的,devintr将调用uartintr。uartintr将读取从UART硬件中写入的字符然后将其传送给consoleintr,consoleintr将积累这些字符直到整行都已经被读取,然后将唤醒仍在sleep的consoleread。当consoleread被唤醒后,将这一行命令复制给user space然后返回。
UART控制寄存器:
LSR寄存器:line status register,用来指示输入的字节是否准备好被用户进程读取。RHR寄存器:receive holding register,用来放置可以被用户进程读取的字节。当RHR中的一个字节被读取时,UART硬件将其从内部的FIFO硬盘中删除,当FIFO中为空时,LSR寄存器被置0。THR寄存器:transmit holding register,当用户进程向THR写入一个字节时,UART将传输这个字节。
RISC-V对中断的支持:
SIE(supervisor interrupt enable) 寄存器用来控制中断,其中有一位是控制外部设备的中断(SEIE),一位控制suffer interrupt(一个CPU向另外一个CPU发出中断)(SSIE),一位控制定时器中断(STIE)。SSTATUS(supervisor status)寄存器,对某一个特定的CPU核控制是否接收寄存器,在kernel/riscv.h中的intr_on被设置。SIP(supervisor interrupt pending)寄存器,可以观察这个寄存器来判断有哪些中断在pending
Console Output
首先是consolewrite()→uartputc()→uartstart();其次是uartintr()→uartstart()。输出的字节将缓存在uart_tx_buf中,这样写入进程就不需要等待UART硬件完成字节的发送,只要当这个缓存区满了的情况下uartputc才会等待。当UART完成了一个字符的发送之后,将产生一个中断,uartintr将调用uartstart来判断设备是否确实已经完成发送,然后将下一个需要发送的字符发送给UART。因此让UART传送多个字符时,第一个字符由uartputc对uartstart的调用传送,后面的字符由uartintr对uartstart的调用进行传送。
I/O concurrency:设备缓冲和中断的解耦,从而让设备能够在没有进程等待读入的时候也能让console driver处理输入,等后面有进程需要读入的时候可以不需要等待。同时进程也可以不需要等待设备而直接写入字符到缓冲区。
Concurrency in drivers
Concurrency Problems:
在consoleread和consoleintr中调用了acquire来获取一个锁,从而保护当前的console driver,防止同时期其他进程对其的访问造成的干扰。
Timer Interrupts
xv6用计时器中断来在线程间切换,usertrap和kerneltrap中的yield也会导致这种进程切换。RISC-V要求定时器中断的handler放在machine mode而不是supervisor mode中,而machine mode下是没有paging的,同时有另外一套完全独立的控制寄存器,因此不能将计时器中断的handler放在trap机制中执行,因此,xv6对定时器中断的处理与上面的陷阱机制完全分开。
kernel/start.c(在main之前)运行在machine mode下,timerinit()在start.c中被调用:
- 用来配置CLINT(core-local interruptor)从而能够在一定延迟之后发送一个中断;
- 另一部分是建立一个类似于rapframe的scratch区域,以帮助定时器中断处理程序保存寄存器和CLINT寄存器的地址。
- 最后,start将mtvec设置为timervec,并启用定时器中断。
由于定时器中断可能在任意时间点发生,包括kernel在执行关键的操作时,无法强制关闭定时器中断,因此定时器中断的发生不能够影响这些被中断的操作。**解决方法是定时器中断handler让RISC-V CPU产生一个"software interrupt"然后立即返回,**software interrupt以正常的trap机制被送到kernel中,可以被kernel禁用。
timervec是一组汇编指令,告知CLINT产生下一次定时器中断的时间,让RISC-V产生一个software interrupt,恢复寄存器并返回到trap.c中,判断which_dev==2为定时器中断后调用yield()。
Real world
计时器中断将会通过调用yield进行强制的线程切换从而使CPU能够在各个内核线程之间均匀分配时间。
由于中断非常耗时,因此可以用一些技巧来减少中断。
- 用一个中断来处理很多一段时间内的事件。
- 彻底禁止设备的中断,让CPU定时去检查这些设备是否有任务需要处理,这种技巧叫做polling。
中断初始化:
1.start.c:start()对中断相关的寄存器初始化。设置Sipervisor mode,设置SIE寄存器接受External,软件和定时器中断,之后初始化定时器。
2.main.c:main()函数中第一个consoleinit()函数,以方便对uart进行初始化保证其可用,另一方面将sys_write和sys_read函数对console的读写绑定到consoleread和consolewrite上,原则上讲,uart初始化后,UART就可以生成中断了,但是因为我们还没有对PLIC进行编程,所以中断不能被CPU所感知。
3.main.c:main()函数中的plicinit()函数。该函数主要设置PLIC可以接受哪些中断,进而将中断路由到CPU。第一行设置了UART的中断,使得UART的中断能够被路由并进而被CPU感知,第二行设置PLIC接收来自IO磁盘的中断。
4.main.c:main()中的plicinithart()函数。之前的plicinit()函数由0号CPU调用,现在的plicinithart()函数则需要每个CPU都调用,表明该CPU对于哪些外设中断感兴趣。具体而言,每个CPU的核都表明自己对来自于UART和VIRTIO的中断感兴趣。并且因为我们不考虑中断的优先级,所以我们将优先级阈值设置为0。
5.main.c:main()中的scheduler()函数。到目前为止,我们有了生成中断的外部设备,我们有了PLIC可以传递中断到单个的CPU。但是CPU自己还没有设置好接收中断,因为我们还没有设置好SSTATUS寄存器。在main函数的最后,程序调用了scheduler()函数。intr_on()函数设置SSTATUS寄存器,打开中断标志位。