思考一下,假设你正在用金山文档或者google docs编辑文字,当你敲下一个字符,屏幕上就会显示一个字符。操作系统做了哪些工作来支持这一过程?为了实现过程,在开机时操作系统又需要做什么工作来准备即将到来的中断?
Basic concepts
driver → the code in an operating system that manages a particular device
- it configures the device hardware
- tells the device to perform operations
- handles the resulting interrupts
- interacts with processes that may be waiting for I/O from the device.
Device drivers execute code in two contexts:
- top half that runs in a process’s kernel thread — 自顶向下
- bottom half that executes at interrupt time. — 自下而上
- UART(Universal Asynchronous Receiver-Transmitter)
- 一些重要的寄存器:
Console input
console driver 是一种driver,负责与uart硬件相沟通
UART与输入输出设备(如键盘屏幕)相连,也就是console driver是从UART中读写相关的数据,console直接交互的对象是UART,UART中的各种寄存器的状态也是由console来管理,包括读写‘初始化等等
- console driver的工作原理:
- The UART hardware that the driver talks to is a 16550 chip emulated by QEMU
- The UART hardware appears to software as a set of memory-mapped control registers.
- UART硬件的初始化
- UART何时产生中断
- shell如何读取console中的内容?
- 用户输入字符到console读取到该字符的过程:
Code
- consoleinit()
- consoleread()
- consoleintr()
- devintr()
- uartintr()
Console output
- 缓冲区
- 系统调用write()是如何输出到终端的?
Code
- uartputc()
- uartstart()
- uartputc_sync()
PLIC
- 作用
- 处理中断的流程
Device Driver
bottom部分:主要是Interrupt handler。当一个进程送到了CPU,并且CPU设置接收中断,那么CPU会调用Interrupt handler处理中断
top部分:主要是用户进程,或者是内核其他调用的接口。对UART来说,这里有read/wirte接口,可以被更高层代码调用
top:
read → fileread →consoleread → buffer空,就阻塞,否则读取数据
bottom:
kerneltrap/usertrap → deintr → uartintr → 循环uartgetc和consoleintr,满了之后,唤醒consoleread,如果空就调用uartstart
top:
printf → for循环putc每次都是一个字符 → uartintr → write → sys_write → filewrite → consolewrite → for循环uartputc → uartputc将字符输出到output buffer,满了就阻塞,否则 → uartstart → while循环写入THR寄存器,发现空了就返回
bottom:
kerneltrap/usertrap → devintr → uartintr → uartstart write到寄存器中
xv6的中断初始化过程
- start()
- main()
xv6中shell程序输出$的top部分
init.c:main():该函数由main.c:userinit()创建,也就是xv6的第一个用户进程,该函数首先通过mknod()创建了一个代表console的设备,type为FD_DEVICE,major为CONSOLE,然后通过open函数打开该文件,这是第一个打开的文件,文件描述符为0。接着通过dup系统调用创建stdout和stderr,分别为1和2,所以文件描述符0,1,2都代表console,之后通过fork+exec创建子进程,即执行sh.c:main()sh.c:main():该函数主要通过getcmd()函数来读取输入,而getcmd首先会调用fprintf()来向console写入一个字符:fprintf()→putc()→write()→syswrite()→filewrite()file.c:consolewrite():该函数会首先判断文件的类型,f→type == FD_DEVICE,紧接着向对应的设备调用write函数,即consolewirite()console.c:consolewrite():这里先通过either_copyin()将字符串拷入,之后调用uartputc()。uartputc()将字符写入给UART设备。uartputc()会实际的打印字符uart.c:uartputc():在该函数中,有一个buffer用来发送数据,和其读写指针共同构成一个循环队列。队列满时会让进程sleepuart.c:uartstart():uartstart()通知设备执行操作。如果buffer没有问题,就将字符写入THR,一旦数据送到设备,系统调用就会返回,shell就会继续执行。从内核返回机制与trap一样- UART会将数据输出到屏幕,并再产生一个中断,接下来:
xv6中shell程序输出的bottom部分
trap.c:usertrap():进入devintr(),首先判断是否是一个supervisor external interrupt,是的话就调用plic_claim()获取中断号,对UART来说,中断号是10,进而进入uartintr()uart.c:uartintr():该函数通过uartgetc()读取寄存器中的字符,这时没有字符,返回-1,直接进入uartstart()uart.c:uartstart():该函数将shell存储在buffer中的任意字符送出,实际上是在还有一个空格要输出。write系统调用可以在UART发送提示符的同时并发地将空格写入buffer,所以在UART的中断触发时,可以发现在buffer中还有一个空格字符,之后会将这个字符送出
- xv6中UART从键盘获取输入的top部分
中断相关内存布局
UART0: 这个宏定义了一个 UART (Universal Asynchronous Receiver-Transmitter) 外设的物理地址。UART 是用于串行通信的硬件设备,通常用于与外部设备(如终端或串行通信设备)进行通信。UART0_IRQ: 定义了 UART0 外设的中断号。VIRTIO0: 定义了 virtio 驱动的物理地址,virtio 是一种用于虚拟化环境的设备驱动标准。VIRTIO0_IRQ: 定义了 virtio 驱动的中断号。CLINT: 定义了 CLINT(Core Local Interruptor)的物理地址,CLINT 包含了计时器等信息。CLINT_MTIMECMP(hartid): 定义了 CLINT 中特定 Hart(硬件线程)的 MTIMECMP 寄存器的地址偏移量,用于设置时钟中断触发时间。CLINT_MTIME: 定义了 CLINT 中的 MTIME 寄存器的地址偏移量,表示自系统启动以来的时钟周期数。PLIC: 定义了 PLIC(Platform-Level Interrupt Controller)的物理地址,PLIC 用于管理外部中断。PLIC_PRIORITY,PLIC_PENDING: 定义了 PLIC 中优先级和中断挂起寄存器的地址偏移量。PLIC_MENABLE(hart),PLIC_SENABLE(hart): 定义了使能特定 Hart 的中断的寄存器地址偏移量。PLIC_MPRIORITY(hart),PLIC_SPRIORITY(hart): 定义了特定 Hart 的中断优先级寄存器的地址偏移量。PLIC_MCLAIM(hart),PLIC_SCLAIM(hart): 定义了特定 Hart 的中断声明寄存器的地址偏移量。