【MIT-OS6.S081笔记5】Chapter5阅读摘要:Interrupts and device drivers

  1. 需要操作系统处理的设备通常可以配置为生成中断,这是一种陷阱。内核陷阱处理代码会识别设备何时发出了中断,并调用驱动程序的中断处理程序;在xv6中,这种调度发生在devintrkernel/trap.c:177)中。驱动代码首先在进程的内核线程执行(进入设备输入输出按系统调用),等待操作完成以后发出中断,然后执行中断处理程序,然后唤醒等待的进程,通知硬件处理之后的下一项操作。
  2. 控制台驱动程序(console.c)是驱动程序结构的一个简单示例。控制台驱动程序通过连接到RISC-V的UART串行端口硬件接收人工输入的字符。控制台驱动程序每次累积一行输入,处理退格键和control-u等特殊输入字符。用户进程(如shell)使用read系统调用来从控制台获取输入行。当你在QEMU中向xv6输入内容时,你的按键会通过QEMU的模拟UART硬件传递给xv6。诸如UART的硬件在软件看来是一组内存映射的控制寄存器,RISC-V硬件会连接到UART设备的一些物理地址,加载和存储操作会和设备硬件而非RAM交互。UART的内存映射地址起始于0x10000000,即UART0kernel/memlayout.h:21)。UART有几个控制寄存器,每个都是一个字节的宽度。它们相对于UART0的偏移量在(kernel/uart.c:22)中定义。例如,LSR寄存器包含一些位,这些位用于指示是否有输入字符等待软件读取。这些字符(如果有的话)可以从RHR寄存器中读取。每读取一个字符,UART硬件就会将其从等待字符的内部FIFO中删除,当FIFO为空时,会清除LSR中的"就绪"位。UART的发送硬件在很大程度上独立于接收硬件;如果软件向THR写入一个字节,UART就会发送该字节。
  3. xv6的main函数会调用consoleinitkernel/console.c:184)来初始化UART硬件。这段代码对UART进行配置,使其在收到每个输入字节时产生接收中断,并在每次完成一个输出字节的发送时产生发送完成中断(kernel/uart.c:53)。
  4. xv6 shell 通过 init.cuser/init.c:19)打开的文件描述符从控制台读取内容。对 read 系统调用的请求会穿过内核到达 consolereadkernel/console.c:82)。consoleread 会等待输入到达(通过中断)并缓冲到 cons.buf 中,将输入复制到用户空间,然后(在一整行输入到达后)返回给用户进程。如果用户尚未输入完整的一行,所有正在读取的进程都会在 sleep 调用(kernel/console.c:98)中等待(第 7 章会解释 sleep 的详细信息)。
  5. 当用户输入一个字符时,UART硬件会请求RISC-V触发一个中断,这会激活xv6的陷阱处理程序。陷阱处理程序会调用devintrkernel/trap.c:177),该函数通过查看RISC-V的scause寄存器来确定中断来自外部设备。然后,它会请求一个名为PLIC的硬件单元告知是哪个设备触发了中断(kernel/trap.c:186)。如果是UART,devintr就会调用uartintruartintrkernel/uart.c:180)从UART硬件读取所有等待输入的字符,并将它们传递给consoleintrkernel/console.c:138);它不会等待字符,因为后续输入会引发新的中断。consoleintr的任务是将输入字符累积到cons.buf中,直到一整行输入到达。consoleintr会对退格键和其他一些字符进行特殊处理。当换行符到达时,consoleintr会唤醒正在等待的consoleread(如果有的话)。一旦被唤醒,consoleread会观察cons.buf中的一整行,将其复制到用户空间,并(通过系统调用机制)返回用户空间。
  6. 对连接到控制台的文件描述符执行write系统调用,最终会到达uartputckernel/uart.c:87)。设备驱动程序维护着一个输出缓冲区(uart_tx_buf),这样写入进程就不必等待UART完成发送;相反,uartputc会将每个字符添加到缓冲区,调用uartstart来启动设备传输(如果尚未启动),然后返回。uartputc需要等待的唯一情况是缓冲区已满。每次UART发送完一个字节,它就会产生一个中断。uartintr会调用uartstart。它会检查设备是否确实已完成发送,并将下一个缓冲的输出字符交给设备。因此,如果一个进程向控制台写入多个字节,通常第一个字节会通过uartputcuartstart的调用来发送,而其余缓冲的字节则会在传输完成中断到达时,通过uartintruartstart的调用来发送。
  7. 需要注意的一个通用模式是通过缓冲和中断将设备活动与进程活动解耦。控制台驱动程序即使在没有进程等待读取输入时,也能处理输入;后续的读取操作将会看到该输入。同样,进程可以发送输出而无需等待设备。这种解耦能够通过允许进程与设备I/O并发执行来提高性能,并且在设备速度较慢(如通用异步收发传输器)或需要立即处理(如回显输入的字符)时,显得尤为重要。这一理念有时被称为I/O并发。
  8. 你可能已经注意到consoleread和consoleintr中都有获取锁的调用。这些调用会获取一个锁,该锁用于保护控制台驱动程序的数据结构免受并发访问。在驱动程序中,并发处理需要谨慎对待的一个方面是:中断处理程序通常只做相对较少的工作(比如,仅仅将输入数据复制到缓冲区),然后唤醒上半部分代码来完成剩下的工作。
  9. xv6使用定时器中断来维护其时钟,并使其能够在计算密集型进程之间进行切换;usertrapkerneltrap中的yield调用会引发这种切换。定时器中断来自连接到每个RISC-V CPU的时钟硬件。xv6对这种时钟硬件进行编程,使其定期中断每个CPU。
  10. RISC-V要求定时器中断在机器模式下处理,而非监管者模式。RISC-V的机器模式在执行时不启用分页,并且拥有独立的控制寄存器集,因此在机器模式下运行普通的xv6内核代码并不现实。因此,xv6对定时器中断的处理与上文所述的陷阱机制完全分开。 在main函数之前,start.c中以机器模式执行的代码会进行设置以接收定时器中断(kernel/start.c:57)。其中一部分工作是对CLINT硬件(核心本地中断器)进行编程,使其在特定延迟后产生中断。另一部分工作是设置一个临时区域,类似于trapframe(陷阱帧)用于帮助计时器中断处理程序保存寄存器和CLINT寄存器的地址。最后,start函数将mtvec设置为timervec并启用计时器中断。
  11. 当用户代码或内核代码正在执行时,定时器中断可能在任何时刻发生;内核无法在关键操作期间禁用定时器中断。因此,定时器中断处理程序必须以确保不会干扰被中断的内核代码的方式完成其工作。基本策略是让处理程序请求RISC-V引发"软件中断",然后立即返回。RISC-V通过普通的陷阱机制将软件中断传递给内核,并允许内核禁用它们。处理由定时器中断生成的软件中断的代码可以在devintr(kernel/trap.c:204)中找到。机器模式定时器中断向量是timerveckernel/kernelvec.S:93)。它在start准备的临时区域中保存一些寄存器,告知CLINT何时生成下一个定时器中断,请求RISC-V触发软件中断,恢复寄存器,然后返回。定时器中断处理程序中没有C代码。
  12. xv6允许在执行内核程序以及用户程序时发生设备中断和计时器中断。即便是在执行内核程序时,计时器中断也会从计时器中断处理程序强制进行线程切换(调用yield)。如果内核线程有时会花费大量时间进行计算而不返回用户空间,那么在这些内核线程之间公平地对CPU进行时间分片的能力就很有用。不过,内核代码需要注意自己可能会被暂停(由于计时器中断),之后在另一个CPU上恢复运行,这一点是xv6中一些复杂性的来源。如果设备中断和计时器中断只在执行用户代码时发生,内核可能会变得稍微简单一些。要全面支持一台普通计算机上的所有设备,工作量非常大,因为设备数量众多,这些设备又具备许多功能,而且设备与驱动程序之间的协议可能既复杂又缺乏完善的文档说明。在许多操作系统中,驱动程序所占的代码量比核心内核还要多。
  13. UART驱动程序通过读取UART控制寄存器,每次获取一个字节的数据;这种模式被称为程序控制I/O,因为是由软件驱动数据移动的。程序控制I/O虽然简单,但速度太慢,无法在高数据速率下使用。需要高速传输大量数据的设备通常会使用直接内存访问(DMA)。DMA设备硬件会直接将传入的数据写入RAM,并从RAM中读取传出的数据。现代的磁盘和网络设备都采用DMA。DMA设备的驱动程序会在RAM中准备好数据,然后通过对控制寄存器执行一次写入操作,告知设备去处理已准备好的数据。
  14. 当设备需要在不可预测的时间获得关注,且这种情况不常发生时,中断是有意义的。但中断会带来较高的CPU开销。因此,像网络和磁盘控制器这类高速设备,会采用一些技巧来减少对中断的需求。其中一个技巧是为一整批传入或传出的请求触发一次中断。另一个技巧是驱动程序完全禁用中断,并定期检查设备是否需要关注,这种技术称为轮询。如果设备执行操作的速度非常快,轮询是合理的,但如果设备大部分时间处于空闲状态,轮询就会浪费CPU时间。有些驱动程序会在轮询和中断之间动态切换,这取决于当前设备负载。
  15. UART驱动程序会先将传入的数据复制到内核中的缓冲区,然后再复制到用户空间。这在数据传输速率较低时是合理的,但对于那些生成或消耗数据速度极快的设备,这种双重复制会显著降低性能。有些操作系统能够直接在用户空间缓冲区和设备硬件之间传输数据,这通常借助DMA来实现。
相关推荐
让流萤再次伟大20 小时前
MIT 6.S081 Lab 1. Xv6 and Unix utilities
操作系统
大模型铲屎官1 天前
【操作系统-Day 46】文件系统核心探秘:深入理解连续分配与链式分配的实现与优劣
人工智能·python·深度学习·大模型·操作系统·文件系统·计算机组成原理
大模型铲屎官1 天前
【操作系统-Day 47】揭秘Linux文件系统基石:图解索引分配(inode)与多级索引
linux·运维·服务器·人工智能·python·操作系统·计算机组成原理
一个平凡而乐于分享的小比特1 天前
操作系统中的“千年虫”
操作系统·千年虫
Hello_MyDream2 天前
继承和线程
操作系统
序属秋秋秋2 天前
《Linux系统编程之进程控制》【进程替换】
linux·c语言·c++·操作系统·进程·系统编程·进程替换
lcreek2 天前
Linux虚拟文件系统(VFS)核心架构解析
linux·操作系统
胡萝卜3.02 天前
程序构建核心解析:从预处理到链接的完整指南
运维·服务器·c++·人工智能·操作系统·编译原理·系统编成
♛识尔如昼♛3 天前
操作系统(1)第一章- 操作系统的概念和功能
操作系统