张天昀的个人博客

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函数中的内容(ctxevret_ctx等)。如果没有return就被其他核心抢占,且其他核心准备切换到当前核心刚刚保存的任务,从中断返回时就会把当前核心的栈设置为它的栈,存储这些内容的栈空间就可能被修改了。后来当前核心从中断返回时,就可能读到错误的内容。

解决方案

必须要在没有核心还会访问内容之后才能放弃对共享资源的独占(RCU)。

对于核心在中断处理时的上下文,在os_trap释放锁后仍然需要读取内容,必须持续保持占有;而从中断处理程序返回后核心就切换到别的栈上了,此时可以放弃独占。因此可以在同一颗核心第i+1i+1次中断处理时放弃对第ii次中断处理时的栈帧的独占。