OSLabs中断处理栈帧问题分析
(2020年05月01日)
OSLabs中为什么不把进程/任务和CPU核心绑定就会出错呢?到底是哪里出错了呢?下面小编就给大家带来神奇的锁核问题解决方案,让我们一起来看看吧(误)
注:本页内容以2019年春季学期《操作系统》课程代码为准,其中的代码无法使用gcc9
直接编译,且2020年课程代码进行了升级。
CTE代码分析
当CPU收到中断信号时,首先会跳转到在am/src/x86/qemu/cte_trap.S
中的trap
处,此时会保存上下文(压入栈中作为函数参数)并调用irq_handle
函数。
irq_handle
函数定义在am/src/x86/qemu/cte.c
中,接受一个TrapFrame
陷阱帧类型的参数(即刚才的汇编代码中压入的数据)。
- 函数首先会创建一个
_Context
类型的上下文数据对象,这个对象保存在栈上。 - 接着会根据IRQ编号设置一个
_Event
类型的事件对象,这个对象也是保存在栈上。 - 然后,调用
__cb_irq
函数,通过中断处理程序获得一个将要恢复到的上下文ret_ctx
,这个指针也是在栈上。 - 最后,将
ret_ctx
指向的数据压入到栈中,然后恢复上下文并从中断中返回(iret
)。
竞争情况
去年想了很久都没能找到究竟在哪里发生了竞争:
- 中断处理程序里上了一把大锁,不可能有多个核心同时进入临界区;
- 在决定运行下一个任务的时候,任务对应的上下文对象的控制权会转移给当前CPU。
最为关键的上下文数据被锁保护着,且只能转移所有权给一个CPU核心。我一直以为问题出现在这个决定下一个task的过程中,但经过学弟提点之后才发现问题不在这里。
中断处理程序的逻辑大概是这样的:
_Context *os_trap(_Event ev, _Context *ctx) {
_Context *ret = ctx;
spinlock_acquire();
for (handler in handlers) {
_Context *next = handler(ev, ctx);
if (next != NULL) {
ret = next;
}
}
spinlock_release();
return ret;
}
看起来很正确的代码,然而竞争情况就出现在了最不起眼的return
语句上。
- 核心A运行任务1时发生中断,进入中断处理程序,上锁;
- 核心A获得了下一个将要运行的任务的上下文,然后解开自旋锁;
- 核心A还没来得及执行
return
,就恰好被外部中断打断; - 核心B进入中断处理程序,又恰好获得了任务1的上下文;
- 核心B解开自旋锁并执行
return
,从任务1的上下文恢复; - 核心A在中断中恢复,但此时核心A的上下文已被破坏。
这里发生的问题就是:在os_trap
函数中,核心的栈上保存了CTE函数中的内容(ctx
、ev
、ret_ctx
等)。如果没有return
就被其他核心抢占,且其他核心准备切换到当前核心刚刚保存的任务,从中断返回时就会把当前核心的栈设置为它的栈,存储这些内容的栈空间就可能被修改了。后来当前核心从中断返回时,就可能读到错误的内容。
解决方案
必须要在没有核心还会访问内容之后才能放弃对共享资源的独占(RCU)。
对于核心在中断处理时的上下文,在os_trap
释放锁后仍然需要读取内容,必须持续保持占有;而从中断处理程序返回后核心就切换到别的栈上了,此时可以放弃独占。因此可以在同一颗核心第次中断处理时放弃对第次中断处理时的栈帧的独占。