重读 xv6(V)
xv6 驱动、锁
六 #
目前为止,我们讨论了 进程、调度、页表、trap 这些控制程序流的机制抽象,还没有谈论像 console、shell、文件 这些与外界进行 IO 交互的逻辑
printf #
源码文件:printf.c
printf 的含义不需要解释,这里只给出流程:
- 遍历给定的格式字符串
fmt - 扫描以
%开头的占位符,绑定变量 - 根据数据到字符的映射,调用
consputc()输出到 console
console #
源码文件:console.c
console 状态由以下变量描述:
- buffer 缓冲区,循环数组
- 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.c,sleeplock.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 判断中断源类型,流程如下:
- 检查中断来源
- 如果来自 PLIC,说明是设备中断,调用相应的驱动程序
- 如何来自 CPU 内部,说明是时钟中断,更新全局
tick
devintr能够区分中断是来自外部还是内部的,在 xv6 中外部设备只有uart与virtio_disk两种
我觉得驱动代码更多是与 QEMU 环境 进行交互的部分,既然 xv6 book 都没有展开讲,说明这些不是学习的重点
usertrap 通过 devintr 获取中断类型,决定下一步的处理
这一部分就写到这里,罗列了一些零碎的部分,并非学习重点