在实现了 xv6 的用户态协程后, 知道了协作式调度的底层原理,双方通过主动让出的方式释放执行权,并在让出的时候保存上下文寄存器,修改为目标协程的寄存器, 每个协程也都有自己的执行栈, 在 xv6 中保存在静态区的固定 4K 的栈
以下是在学习调度过程中遇到的一些问题和找到的解答
问题 1: go 的调度也是这样实现的吗?
go 在 1.14 之前都是协作式的调度,难怪被叫做协程,后来才改为了基于信号的抢占式调度
在 1.14 之前是通过在代码中插入调度检查点来做到主动让出的, 像死循环这样的代码也会在循环内插入检查点来实现,但是性能损耗很高
在 1.14 后实现的抢占式调度,是基于信号的, 信号就类似内核里的硬件中断,可以强行把执行权转交给某个中断处理程序
问题2: 在信号处理程序里要如何拿到对应协程的寄存器值
到这里看起来好像很简单,也就和内核类似,保存寄存器,还原寄存器. 但是还有一个地方不明白的是, 在信号处理程序里要如何拿到对应协程的寄存器值,毕竟当前的寄存器值已经是信号处理程序的值了
查阅信号相关的系统调用也没找到和寄存器相关的参数 , 如果系统调用提供了修改和获取的参数那就最好了 , 但是没有
后来查阅资料得知, linux 在进入信号处理程序后, 会在栈中保存被中断的程序的寄存器值,sigcontext , 并且在还原的时候会从这里取, 这样就解决了寄存器获取和修改的问题
问题 3:为什么没有在信号处理程序里做调度?
这个问题在查看 gophercon 的一个视频后得到了解答: 在信号处理程序中处理调度会引发很多问题 , 于是作者把调度移到了正常状态下的程序运行
问题 4:为什么只能在安全状态下运行调度
因为一个指针操作可能由多个机器指令构成, 被抢占的时候可能只执行了部分指令 , 此时指针的状态难以判断处是否可以回收 , 因此 go 的做法是在抢占的时候还要判断是否能安全抢占 , 像处在垃圾回收时就是不安全状态
参考资料
- c 实现的用户态抢占式调度 关键词: linux 信号 修改寄存器
- 内核信号处理 & CPU8个通用寄存器
- go 语言原本 - 抢占式调度 关键词: go pushcall
- go 语言原本 - 信号 关键词: go pushcall
- linux 0.11 信号源码阅读 关键词: linux 信号 源码
- 抢占式调度实现的作者的视频讲解 GopherCon 2020: Austin Clements - Pardon the Interruption: Loop Preemption in Go 1.14 关键词: go preempt
- GopherCon 2018: Kavya Joshi - The Scheduler Saga
- GopherCon 2021: Madhav Jivrajani - Queues, Fairness, and The Go Scheduler