学习思考
😘Interrupt
00 分钟
2023-8-8
2023-8-9
type
status
date
slug
summary
tags
category
icon
password
Property
Aug 9, 2023 02:59 PM
思考一下,假设你正在用金山文档或者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 — 自顶向下
    • via sstem calls such as read and write that want the device to perform I/O
    • read: sys_read()fileread()
    • write: sys_write()filewrite()
    • device ash the hardware to satr an operation(e.g. ask the disk to read a block);
    • read: consoleread()either_copyout()
    • write: consolewrite()uartputc()uartstart()WriteReg()
    • then the driver code waits for operation to complete
      the device completes the operayion and raises an interrupt(trap)
      kernel trap code handles this trap and calls the driver’s interrupt handler;
  • bottom half that executes at interrupt time. — 自下而上
    • figure out what operation has completed
      • read: devintr()uartintr()consoleintr()
      • write: devintr()uartintr()uartstart()
    • wake up the corresponding waiting process;
    • tell the hardware to start work on any waiting next operation
  • UART(Universal Asynchronous Receiver-Transmitter)
    • 可以理解为是一组寄存器
      是一种用于串行通信的硬件设备,用于在计算机系统内部或与外部设备之间传输数据。在 xv6 操作系统中,UART 是一个重要的组件,用于实现与外部设备(如终端、串口设备等)的通信。
      1. UART 硬件:在计算机系统中,UART 是一种通用的串行通信设备,它用于将并行数据转换为串行数据以进行传输,或将串行数据转换为并行数据以供计算机处理。UART 通常具有发送(Transmitter)和接收(Receiver)两个部分,用于在计算机和外部设备之间进行双向通信。
      1. xv6 中的 UART:xv6 操作系统实现了对 UART 的支持,以便在操作系统内部进行串行通信。它使用 UART 进行输入/输出操作,允许用户与操作系统进行交互,或者通过串口与其他设备进行通信。
      1. UART 在操作系统中的作用:在 xv6 中,UART 可以用于实现终端(Terminal)功能,使用户能够在操作系统运行时与系统交互。用户可以通过终端发送命令、接收输出等。此外,UART 还可以用于调试和监控操作系统的运行状态,例如输出调试信息或错误消息。
      1. UART 驱动程序:xv6 包含了针对 UART 设备的驱动程序,这些驱动程序负责管理与 UART 的通信。它们提供了操作 UART 的接口,允许操作系统通过读取和写入操作与 UART 进行交互。
      总之,xv6 操作系统中的 UART 是一个重要的组件,用于实现操作系统与外部设备之间的串行通信。它使操作系统能够与用户进行交互,同时也可以在调试和监控操作系统时发挥作用。
  • 一些重要的寄存器:
    • RHR中保持着UART接收的输入,等待内核将其内容取走;
      THR保持着内核的输入,等待UART将其发送。对于内核的read或write指令,将访问对应的RHR或THR控制寄存器。
      LSR包含一些位,LSR_RX_READY指示RHR中是否有输入字符,等待内核将其读出;一旦被读出,UART就将其从内部的FIFO缓冲区中移除,直到缓冲区为空时置位LSR_TX_IDLE。

Console input

console driver 是一种driver,负责与uart硬件相沟通
UART与输入输出设备(如键盘屏幕)相连,也就是console driver是从UART中读写相关的数据,console直接交互的对象是UART,UART中的各种寄存器的状态也是由console来管理,包括读写‘初始化等等
  • console driver的工作原理:
      1. The console driver accepts characters typed by a human, via the UART serial-port hardware attached to RISC-V
      1. The console driver accumulates a line of input at a time, processing special input characters such as backspace and control-u
      1. User processed, such as the shell, use the read system call to fetch lines of input from the console.
      1. When you type input to xv6 in QEMU, your key strokes are delivered to xv6 by way of QEMU’s simulated UART hardware.
  • The UART hardware that the driver talks to is a 16550 chip emulated by QEMU
    • on a real computer, a 16550 would manage an RS232 serial link connecting to a terminal or other computer. When running QEMU, it’s connected to your keyboard and display.
  • The UART hardware appears to software as a set of memory-mapped control registers.
    • There are some physical addresses that RISC-V hardware connects to the UART device, so that loads and stores interact with the device hardware rather than RAM.
    • The memory-mapped addresses for the UART start at 0x10000000, or UART0 (kernel/memlayout.h:21).
    • There are a handful of UART control registers, each the width of a byte.
  • UART硬件的初始化
    • 调用consoleinit里的uartinit()来初始化UART硬件,写入相关的控制寄存器,配置好传输的波特率,重置FIFO缓冲区,最后开启接收中断receive interrupt发送完成中断transmit complete interrupt保证能够产生两种interrupts
      Xv6’s main calls consoleinit (kernel/console.c:182) to initialize the UART hardware.
      1. Configures the UART to generate a receive interrupt when the UART receives each byte of input
      1. A transmit complete interrupt each time the UART finishes sending a byte of output.
  • UART何时产生中断
    • 接收到一个字符和发送完成一个字符的时候
  • shell如何读取console中的内容?
      1. Calls to the system call make their way through the kernel to consoleread
      1. consoleread() waitsfor input to arrive (via interrupts) and (after a whole line has arrived) returns to the user process
      1. if the user hasn’t typed a full line yet, any read processes will wait in the sleep call
  • 用户输入字符到console读取到该字符的过程:
      1. When the user types a character, the UART hardware asks the RISC-V to raise an interrupt, which activate xv6’s trap handler
      1. The trap handler calls devintr()(kernel/trap.c:177), which looks at the RISC-V scause register to discover that the interrupt is from an external device.
      1. Then it asks a hardware unit called the PLIC to tell it which device interrupted(kernel/trap.c:86). If it was the UART, devintr() calls uartintr().
      1. uarintr() 一个一个读取字符,以便通过consoleintr()函数缓存到buff中,另一边通过consputc()回显屏幕,直到换行符等特殊符号到达,然后通过wakeup()函数激活已经在consoleread()函数中sleep的进程。 uartintr (kernel/uart.c:180) reads any waiting input characters from the UART hardware and hands them to consoleintr (kernel/console.c:136); it doesn’t wait for characters, since future input will raise a new interrupt. The job of consoleintr is to accumulate input characters in cons.buf until a whole line arrives. consoleintr treats backspace and a few other characters specially. When a newline arrives, consoleintr wakes up a waiting consoleread (if there is one).
      1. Once woken, consoleread will observe a full line in cons.buf, copy it to user space, and return (via the system call machinery) to user space.
        1. 总结一下: 用户输入字符 → UART中RHR接收字符 → UART发出中断 → 内核接收中断,陷入 → trap handler发现是外部中断,来自UART → 调用uartintr → 发现RHR中有字符可读 → 调用consoleintr → 将输出字符缓冲到cons.buf → 读到’\n’或者’ctrl+D’ → 唤醒consoleread → 读出一整行,拷贝到用户空间

Code

  • consoleinit()
    • xv6在main进行了控制台的初始化,调用consoleinit()来完成,然后consoleinit()又调用uartinit()初始化UART(kernel/uart.c),如下所示。之后,每当UART收到一个字节的输入时,就会产生receive interrupt;每当UART完成一个字节的发送时,就会产生transmit complete interrupt。
  • consoleread()
    • xv6的shell可以主动地从控制台读取输入,shell发出read的系统调用请求后,最终将导向到控制台驱动程序的top half,并执行consoleread()(kernel/console.c)。consoleread()主要从缓冲区cons.buf中读取整行的输入到用户空间中,如果cons.buf中没有字符可读,或者读完了缓冲区中的所有字符但该行仍未结束,shell自己会睡眠,并等待条件满足时才会被唤醒。
  • consoleintr()
    • 如果有输入字符到达控制台,控制台将会产生中断,因此跳转到控制台的中断处理程序consoleintr(),它会将输入字符缓冲到cons.buf中,然后返回。直到cons.buf里累积了一整行的输入,consoleintr才会唤醒一个用户进程的(比如shell)consoleread,然后consoleread将缓冲在cons.buf中的一整行输入,拷贝到用户空间,然后返回到用户进程。
  • devintr()
    • 用户通过键盘输入一个字符,到控制台接收到这个字符,在这之间我们通过UART进行传输。
      当用户键下键盘输入一个字符,且UART的RHR接收到时,UART就会向CPU发出中断(receive interrupt),在内核中执行trap handler。发现是设备中断后,trap handler会跳转到devintr(),如下所示。在devintr中,通过检查scause中的值,发现该设备中断来自于外部设备,然后由PLIC(管理着所有的外部设备中断)告诉CPU,是哪个设备产生中断。最后发现是UART之后,devintr就会跳转到uartintr(),即UART的中断处理程序,进行相关处理。
  • uartintr()
    • uartintr(kernel/uart.c)如下所示,它首先尝试从其控制寄存器RHR中读出一个字符(我们说过RHR保持着UART接收的输入),如果有,直接将该字符交给控制台驱动程序的bottom half,即consoleintr来处理该字符(实际上在UART的中断处理程序中,又调用了控制台的中断处理程序,但这不是通过中断实现的,并不是中断嵌套),然后consoleintr做的工作正如前面我们所说明的那样。但如果RHR中没有字符可读,uartintr并不会阻塞地等待那些尚未到达RHR的字符,但也不会因此错过它们,因为之后新的字符被用户键入时,UART还会发出后续中断
      处理完RHR中保持的字符后,UART接着调用uartstart,在该函数中,检查UART的缓冲区里是否有需要UART发送的数据,如果有,并且THR为空,就将该字符写入到THR中,UART就会发送该字节,在下一小节我们将更仔细地说明发送的部分。

Console output

  • 缓冲区
    • 由UART负责维护
  • 系统调用write()是如何输出到终端的?
    • consolewrite()uartputc()uartstart()
      unartintr()uartstart()
       
      用户进程需要产生一些输出到控制台上,以便通过屏幕显示给用户。所以,用户进程,例如shell,通过系统调用write,往某个发送缓冲区里写入一些字符;我们通过UART来传输这些字符,所以我们应该往UART的发送缓冲区里写入它们,然后用户进程就可以返回;UART会在适当的时候,将一个字符写入控制寄存器THR;最后,UART将字符成功地发送到了控制台,控制台呈现输出给用户。

Code

  • uartputc()
    • 用户进程请求的write系统调用,最终将导向到UART驱动程序的top half,并执行uartputc()(kernel/uart.c),如下所示。对于用户进程来说,只需要通过uartputc向发送缓冲区中写入一个字符,并且调用uartstart(),而uartstart无论如何都会很快就返回,后面我们将看到这点;如果发送缓冲区已满,驱动程序的决定是暂时将该进程挂起,让其睡眠,什么时候唤醒呢?同样也是在uartstart中。
  • uartstart()
    • uartstart()尝试发送一个位于发送缓冲区中的字符(按FIFO的方式),如果发送缓冲区为空,或者控制寄存器THR还持有着字符(这代表着对于上一个字符,UART的发送已经就绪,只是还没发送出去),那么uartstart将会直接返回
      我们可以看到,无论该字符能不能马上被UART发送出去,uartstart都会很快就返回,因此我们可以认为uartstart几乎是无阻塞的。再返回到上一层,在uartputc中,我们要么会调用uartstart,要么让当前进程挂起并睡眠,因此uartputc也是很快就返回的。所以,UART暴露给用户程序或内核的uartputc接口是异步的,用户进程的write可以使用这种接口,因为用户进程可以很快地返回或被挂起,从而很快地进行后续工作或者让出CPU资源。
  • uartputc_sync()
    • uartputc也提供了同步,或者说阻塞的版本,uartputc_sync。该版本的接口,用于满足那些需要马上响应的需求,因此CPU就阻塞在某处,直到THR中的字符被发送,然后就把需要发送的新字符写入THR。你也可以看到,该字符不会写入发送缓冲区中。事实上,内核的printf就使用这个同步的版本,因为内核打印的消息比较重要,我们希望能尽快地显示给用户。

PLIC

  • 作用
    • 处理设备中断或者分发中断,PLIC会管理来自外设的中断,比如说对其优先级进行编程等等
  • 处理中断的流程
    • PLIC会通知当前有一个待处理的中断
      其中一个CPU核会调用plic_claim()Claim接收中断,这样PLIC就不会把中断交给其他CPU处理
      该CPU处理完中断之后,CPU会通过plic_complete()通知PLIC
      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()
    • 函数中对中断相关寄存器的初始化。这里将所有的中断都设置在Supervisor mode,然后设置SIE寄存器来接收External,软件和定时器中断,之后初始化定时器
  • main()
    • consoleinit()
      uartinit()
      此时xv6还未初始化PLIC,所以CPU不能对中断进行感知
      trapinit()
      trapinithart()
      plicinit()
      // set up interrupt controller
      设置PLIC可以接受那些设备的中断
      第一行使能UART的中断
      第二行设置PLIC接受I/O磁盘的中断
      plicinithart()
      // ask PLIC for device interrupts
      需要每个CPU都调用,每个核都表示自己对UART和VIRTIO感兴趣
      暂时将中断优先级都设置为0

xv6中shell程序输出$的top部分

  1. 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()
  1. sh.c:main():该函数主要通过getcmd()函数来读取输入,而getcmd首先会调用fprintf()来向console写入一个字符:fprintf()putc()write()syswrite()filewrite()
  1. file.c:consolewrite():该函数会首先判断文件的类型,f→type == FD_DEVICE,紧接着向对应的设备调用write函数,即consolewirite()
  1. console.c:consolewrite():这里先通过either_copyin()将字符串拷入,之后调用uartputc()uartputc()将字符写入给UART设备。uartputc()会实际的打印字符
  1. uart.c:uartputc():在该函数中,有一个buffer用来发送数据,和其读写指针共同构成一个循环队列。队列满时会让进程sleep
  1. uart.c:uartstart()uartstart()通知设备执行操作。如果buffer没有问题,就将字符写入THR,一旦数据送到设备,系统调用就会返回,shell就会继续执行。从内核返回机制与trap一样
  1. UART会将数据输出到屏幕,并再产生一个中断,接下来:

xv6中shell程序输出的bottom部分

  1. trap.c:usertrap():进入devintr(),首先判断是否是一个supervisor external interrupt,是的话就调用plic_claim()获取中断号,对UART来说,中断号是10,进而进入uartintr()
  1. uart.c:uartintr():该函数通过uartgetc()读取寄存器中的字符,这时没有字符,返回-1,直接进入uartstart()
  1. uart.c:uartstart():该函数将shell存储在buffer中的任意字符送出,实际上是在还有一个空格要输出。write系统调用可以在UART发送提示符的同时并发地将空格写入buffer,所以在UART的中断触发时,可以发现在buffer中还有一个空格字符,之后会将这个字符送出
  • xv6中UART从键盘获取输入的top部分

中断相关内存布局

  1. UART0: 这个宏定义了一个 UART (Universal Asynchronous Receiver-Transmitter) 外设的物理地址。UART 是用于串行通信的硬件设备,通常用于与外部设备(如终端或串行通信设备)进行通信。
  1. UART0_IRQ: 定义了 UART0 外设的中断号。
  1. VIRTIO0: 定义了 virtio 驱动的物理地址,virtio 是一种用于虚拟化环境的设备驱动标准。
  1. VIRTIO0_IRQ: 定义了 virtio 驱动的中断号。
  1. CLINT: 定义了 CLINT(Core Local Interruptor)的物理地址,CLINT 包含了计时器等信息。
  1. CLINT_MTIMECMP(hartid): 定义了 CLINT 中特定 Hart(硬件线程)的 MTIMECMP 寄存器的地址偏移量,用于设置时钟中断触发时间。
  1. CLINT_MTIME: 定义了 CLINT 中的 MTIME 寄存器的地址偏移量,表示自系统启动以来的时钟周期数。
  1. PLIC: 定义了 PLIC(Platform-Level Interrupt Controller)的物理地址,PLIC 用于管理外部中断。
  1. PLIC_PRIORITY, PLIC_PENDING: 定义了 PLIC 中优先级和中断挂起寄存器的地址偏移量。
  1. PLIC_MENABLE(hart), PLIC_SENABLE(hart): 定义了使能特定 Hart 的中断的寄存器地址偏移量。
  1. PLIC_MPRIORITY(hart), PLIC_SPRIORITY(hart): 定义了特定 Hart 的中断优先级寄存器的地址偏移量。
  1. PLIC_MCLAIM(hart), PLIC_SCLAIM(hart): 定义了特定 Hart 的中断声明寄存器的地址偏移量。
      1. sysfile.c:sys_read():read系统调用中,调用了fileread()函数,该函数首先判断文件类型,接着向调用自己的read函数(consoleread)
      1. console.c:consoleread()
      1. console.c:consoleintr():默认情况下字符会通过consputc回显到屏幕上。之后字符存放在buffer中,遇到换行符或者ctrlD时唤醒之前sleep的进程,从buffer中读出字符
      1. 通过buffer将top和bottom解耦,可以并发的进行

参考

  • xv6book

评论
Loading...