重读 xv6(V)

xv6 驱动、锁

#

目前为止,我们讨论了 进程、调度、页表、trap 这些控制程序流的机制抽象,还没有谈论像 console、shell、文件 这些与外界进行 IO 交互的逻辑

printf #

源码文件:printf.c

printf 的含义不需要解释,这里只给出流程:

  1. 遍历给定的格式字符串 fmt
  2. 扫描以 % 开头的占位符,绑定变量
  3. 根据数据到字符的映射,调用 consputc() 输出到 console

console #

源码文件:console.c

console 状态由以下变量描述:

  1. buffer 缓冲区,循环数组
  2. read、write、edit 下标

consoleread/consolewrite 实现系统与终端的字符传递,将 buffer 的内容复制到用户空间

consoleintr 则是处理用户输入带来的中断事件,模拟 buffer 的状态机变化

xv6 提供了 文件描述符 这一抽象,使得 console 可以被抽象为设备/文件接口,我们就是通过 read/write 系统调用与用户进行交互的

值得一提的是,console 在用户没有完成输入时,会调用 sleep 让出 CPU,等待用户继续写入字符

uart #

源码文件:uart.c

真正负责打印字符的是 uartputc 函数,uart 这里可以理解为与键盘屏幕交互的硬件接口

我认为这里的讨论不是重点了,我们只需要知道 uart 通过 寄存器内容 得知交互行为,传输字符到 console 的缓冲区即可


总之,以上代码构成与控制台交互的整个逻辑,consoleinit 设置 console 作为设备抽象的读写函数,并调用 uartinit 完成硬件的初始化配置,而 printfinit 只是设置 pr

#

驱动、中断、进程、空间分配,总是会出现多个进程访问同一块数据的情况,这就是 锁(lock)的应用场景

#

源码文件:spinlock.csleeplock.c

关于自旋锁、休眠锁的实现,我觉得没有多讨论的必要了

大粒度锁可以保护临界区,但减少了并发性,我们可以通过细化锁来提高效率

kalloc 链表为例,多个进程同时申请内存时,会在锁上面产生冲突

此时我们可以选择将 pid 哈希映射到多个子链表,为每个链表分别维护锁,这样不同的进程就可以在两个不同的地址同时运行 kalloc ,当哈希不平衡时,我们也可以夺取其他线程的锁,或者重新分配资源

此外,中断与锁的交互是 os 层面特有的问题,上方的应用层意识不到中断的存在

在编写内核程序时,我们需要时刻考虑随机中断是否存在死锁风险,如果有,就提前关闭中断

在这里补充线程的内容,xv6 没有显式支持用户多线程,但是 内核程序、scheduler 都是通过 trap 中断 而不是 swtch 机制进行上下文切换的,本质上只有页表发生了变化

我认为可以在 thread 结构体里维护类似 trapframe 的结构,实现用户线程间的切换
usertrap 的区别在于不需要切换 %satp 页表,意味着所有线程共享同一块虚拟空间,进程资源也是共享的
那么这里又可以用 lock 保护线程临界区

要实现这一套逻辑,需要仿照进程重做一个粒度更小的 fork 体系与调度器,还需要修改 clockintr 的后续处理 不过我又想到,可以由内核线程对用户线程进行调度,在 trap 时切换线程,这样设计也是可以的

以上是临时想到的,没有经过验证

设备中断 #

PLIC(Platform-Level Interrupt Controller,平台级中断控制器)是 RISC-V 架构中负责管理外设中断的标准化硬件组件,用于协调外部设备(如定时器、UART、GPIO 等)产生的中断请求,并将其路由到相应的处理器核心进行处理。

硬件知识不多讨论,这里我想借机讲一下 trap.c/devintr

xv6 将外部设备都抽象为 device,中断时通过 %scause 判断中断源类型,流程如下:

  1. 检查中断来源
  2. 如果来自 PLIC,说明是设备中断,调用相应的驱动程序
  3. 如何来自 CPU 内部,说明是时钟中断,更新全局 tick

devintr 能够区分中断是来自外部还是内部的,在 xv6 中外部设备只有 uartvirtio_disk 两种
我觉得驱动代码更多是与 QEMU 环境 进行交互的部分,既然 xv6 book 都没有展开讲,说明这些不是学习的重点

usertrap 通过 devintr 获取中断类型,决定下一步的处理


这一部分就写到这里,罗列了一些零碎的部分,并非学习重点