操作系统 Lab2 kmt
2022-05-28 16:06:00

Lab2-kmt

花了整个五一三天假期,最后是听了答疑才知道怎么解决栈的数据竞争的....

痛苦的部分主要是多核 logging 和怎么用 qemu debug 的问题。搞定了这些技术上的难题,剩下就是老老实实写代码了。

设计

spinlock

去观摩了 xv6 的代码,发现不仅要自旋,还得关中断。并且关中断这事还得是嵌套的。

semaphore

每个信号量有一个等待队列 wait_tid,一个 value,还有一个 wakeup 表示需要唤醒多少个睡眠中的进程。

我的实现需要在 sem_signalsem_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_rendermemset 一段内存,然后这段内存恰好处在某两个结构体中间,结果就是改写了我的结构体信息。

这个 Bug 比较难抓到,每7、8次才能复现一次,并且每次导致出错的 memset 地址都是一样的(非常整齐,恰好是页面的整数倍)。一开始我以为是 pmm.c 的问题,分配的内存和设备地址重叠了。但我打印之后发现并不是这样。而且更神奇的是,我把 task_struct 的数量减少到 64 之后,这个 Bug 就再也没法触发了。。。