Lab2-kmt
花了整个五一三天假期,最后是听了答疑才知道怎么解决栈的数据竞争的....
痛苦的部分主要是多核 logging 和怎么用 qemu debug 的问题。搞定了这些技术上的难题,剩下就是老老实实写代码了。
设计
spinlock
去观摩了 xv6 的代码,发现不仅要自旋,还得关中断。并且关中断这事还得是嵌套的。
semaphore
每个信号量有一个等待队列 wait_tid
,一个
value
,还有一个 wakeup
表示需要唤醒多少个睡眠中的进程。
我的实现需要在 sem_signal
和 sem_wait
中修改全局的任务链表。由于 os_trap()
中途也可以
sem_signal
,所以需要保证对链表读写的互斥。同时由于
os_trap()
的第一个操作必须是保存上下文、最后必须是切换,因此需要保证这两个操作对进程状态的修改是符合
save_context
后和 switch_task
前的语义的。
在调试过程中遇到过一个印象深刻的Bug
一开始我认为只能调度 RUNNABLE
的任务,但实际上可以调度所有非 RUNNING
的任务。注意到等待信号量进入睡眠后需要一次切换让出 CPU,这就是一种从
SLEEP
调度的情况。
栈的数据竞争
一开始的上下文切换是通过记录栈上的指针完成的,即每个任务记录一个上下文指针,指向栈上由 AM 的 cte 保存的上下文。
于是就可以观察到,某些时候 os_trap()
会返回到空的
%rip
。
后面每个任务的结构体里都单独拷贝一份上下文,这样就会在多核时出现经典的
triple fault。然后STFW发现可以用 -d exec
来打印 trace,用
-d cpu-reset
来打印寄存器的值。然后就可以发现每次都是一个线程的 %rip
跑飞了,triple fault 就恰好是三次越界指令访问。并且可以发现每次都是在
cpu_current()
调用后返回到了错误地址,意味着栈被改写了。
然后我去翻了聊天记录,发现有同学问了一样的问题,但是没有看懂他的解决方案。于是中午去听了答疑,知道了怎么延迟任务T的调度来确保T的栈不会被两个 CPU 同时操作。感觉这个想法还是很厉害的。
但是这样做会出现新的问题:如果用smp=2跑3个任务,那么就会出现问题。CPU[0]从
idle[0]->print,而 CPU[1] 此时无法从 idle[1]
跳到任何任务(一个正在运行,另一个由于栈切换必须等到 CPU[0] 下一次
os_trap()
才能调度,但是 yield()
的语义是让出
CPU[1],因此会被我的 assert
抓到)
解决方案也很简单。我开了2倍smp的 idle 任务,用于保证每个 CPU 至少可以切换到另一个 idle 上。这样虽然不太优雅,但也还能跑起来。
tty的神秘Bug
一开始我开了 128 个 task_struct
,然后在跑
dev
的时候滚键盘就会出现某个任务的结构体被修改了的情况。通过
assert
和断点找到了是 tty_render
会
memset
一段内存,然后这段内存恰好处在某两个结构体中间,结果就是改写了我的结构体信息。
这个 Bug 比较难抓到,每7、8次才能复现一次,并且每次导致出错的
memset
地址都是一样的(非常整齐,恰好是页面的整数倍)。一开始我以为是
pmm.c
的问题,分配的内存和设备地址重叠了。但我打印之后发现并不是这样。而且更神奇的是,我把
task_struct
的数量减少到 64 之后,这个 Bug
就再也没法触发了。。。