Linux 的 schedule()
函数主要完成现场切换任务。下面的主要是与文章所讨论问题相关的代码段
// Linux 2.6.11.12
// file: sched.c
void schedule(void) {
// ...
spin_lock_irq(&rq->lock);
// ...
prepare_arch_switch(rq, next);
context_switch(rq, prev, next);
finish_task_switch(prev);
// ...
}
这里锁住rq->lock
后,并没有将其打开,而是在prev task
下一次被调度后,才执行finish_task_switch(prev)
解锁rq->lock
。
那么,现在假定有三个进程A,B 和 C。A首先执行 schedule()
,A将rq->lock
锁住后,调用context_switch()
切换至B。再然后,B开始调用schedule()
,B也想获取rq->lock
,然而,如果照此逻辑,rq->lock
已经被A锁住了,所以,死锁也就发生了。
但是,其实我们都知道,按照上面例子推断,那内核开始运行后,马上就会出现死锁,这似乎与直觉相违。于是,自然而然的想法便是,在调用switch_to()
切换至B进程前,rq->lock
已经被释放了。如果你同意上面A、B、C的例子,你也应该会同意我这么一个推断的,对吧?
于是,接下来我把焦点集中到了那个prepare_arch_switch()
/*
* file: sched.c
* Default context-switch locking:
*/
#ifndef prepare_arch_switch
# define prepare_arch_switch(rq, next) do { } while (0)
# define finish_arch_switch(rq, next) spin_unlock_irq(&(rq)->lock)
#endif
// file: include/asm-ia64/system.h
#define prepare_arch_switch(rq, next) \
do { \
spin_lock(&(next)->switch_lock); \ spin_unlock(&(rq)->lock); \ } while (0) #define finish_arch_switch(rq, prev) spin_unlock_irq(&(prev)->switch_lock)
事情貌似有了一点点转机,ia64里,prepare_arch_switch()
确实是把rq->lock
释放了,同时,锁上了next->switch_lock
。振奋人心,我们似乎应该庆祝一下,我们的猜测是对的。
注:
finish_task_switch(prev)
使用finish_arch_switch()
宏进行解锁
噢,等等,我们的老朋友i386并没有特别定义prepare_arch_switch()
,他使用的是默认的实现。也就是说,在切换进程时,他没有释放rq->lock
。好吧,天一下子塌了下来,我们最后的救命稻草就这样消失了。
无助的我,只能上stackoverflow求助大神了。抱着满肚子的疑惑吃过了午饭,满怀期待地再次打开浏览器,结果,还是没有人回答……
好吧,我承认,我快疯了。无助的我,再次打开编辑器去看另一个版本的代码,希望能够搜寻到在switch_to()
前释放rq->lock
的一些蛛丝马迹。
现在,我可以确切告诉你,我没有找到在switch_to()
前释放rq->lock
的代码,而且,至少在Linux 2.6,应该是永远也不会找到的。rq->lock
的的确确没有被释放就切换到另一个进程了!!
下面是 Linux 2.6.32.68 里 context_switch()
中的一段注释
/*
* Since the runqueue lock will be released by the next
* task (which is an invalid locking op but in the case
* of the scheduler it's an obvious special-case), so we
* do an early lockdep release here:
*/
不知道你看到这段注释的时候是不是和我一样的激动,但这就是我们苦苦搜寻的答案了。虽然有悖常识,但他的确运行良好。下面分三种情况,以默认实现为例,我们仔细看看,这里面,内核程序员到底施了什么魔法。
1. B 是一个新进程
这里”新进程”的意思是,他刚刚被fork出来,他还没有运行什么代码。对于这个新进程,__switch_to()
将返回到ret_from_fork()
,ret_from_fork()
里最终将调用finish_task_switch()
!
如果你没有忘记,在最上面给出的schedule()
代码里,在prev task
从switch_to()
返回后(再次被调度),他将调用finish_task_switch()
释放对应的锁。
现在,rq->lock
被打开了。
2. B 是一个旧进程
同样的,这里的“旧”,指的是B曾经运行过。此时,B 将从 switch_to()
函数返回。接着,他会接着运行schedule()
剩下的代码。其中,就包括那finish_task_switch()
。rq->lock
已经由prev task
锁住了,所以,我们可以安全地对它进行解锁操作。
好了,让我们释放rq->lock
吧!
注:A 再次运行时,与情况 2 相同
现在,虽然最初的问题已经解决了,但我觉得,还有另一个问题需要考虑:为什么我们要费尽心思,去写这么精巧且违反直觉的代码呢?为什么我们不能在切换至第二个进程前,直接释放锁呢?(好吧,看起来像是两问题)
答案是,不能。
不知道你有没有注意到,上面锁住rq->lock
的同时,也禁止了本地中断。如果我们释放锁,就意味着我们可能会中断。而在进程B真正运行前,内核中进程的数据结构都将处于不一致的状态。此时,如果被中断,后果估计不会是我们所愿意看到的。
而在ia64里,虽然我们确确实实解开了rq->lock
,但在解锁前,我们也锁住了next->switch_lock
。由此我们也可以判断,在第1个理由里我所宣称的防止内核数据结构被破坏,具体的,就是next
的task descriptor。至于这里使用next->switch_lock
而不是rq->lock
的原因,源码中其实已有说明,由于与这里我们要讨论的问题无关,便不再赘述。