linux内核抢占的几个细节

邮件列表每天都能让我学到新东西,感谢他!有朋友问PREEMPT_ACTIVE有什么用,我给出了最简单的回答,就是避免被抢占的进程被无情的赶出运行队列。这个回答显然不能让那位朋友满意…

进程一旦调用了schedule,如果再次被调度运行,那么有下面几种可能:1.状态为TASK_RUNNING,处于运行队列,那么它肯定有机会再运行;2.处于睡眠队列,那么一旦条件满足被唤醒,那么它就会运行。那么如果一个进程被抢占的话,而且它不在运行队列,那么怎么再让它运行呢?答案是它不能运行了。为了避免这种情况,就必须避免处于非TASK_RUNNING的进程被抢占的进程不被赶出运行队列,也就是下面的代码,schedule的代码:

if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {

switch_count = &prev->nvcsw;

if (unlikely((prev->state & TASK_INTERRUPTIBLE) && unlikely(signal_pending(prev))))

prev->state = TASK_RUNNING;

else {

if (prev->state == TASK_UNINTERRUPTIBLE)

rq->nr_uninterruptible++;

deactivate_task(prev, rq);

}

也许有人会问,怎么会有不是TASK_RUNNING的进程而且被抢占的,这个问题实在难以回答,可是记住,进程状态和其所在的队列没有关系,设置进程状态和抢占总是有可能有间隙的。我们看看下面的代码:

for (;;) { /

1: prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE); /

2: if (condition) /

3: break; /

4: schedule(); /

}

如果在1中被抢占,恰恰在设置完进程为TASK_UNINTERRUPTIBLE的时候被抢占,本来马上就要测试条件是否满足了,结果又被加入睡眠队列去睡眠了,如果没有PREEMPT_ACTIVE,那么在schedule中就会被移出运行队列,如果只有这一次唤醒机会,那么就永远唤不醒这个进程了,如果本次从schedule回来条件不满足,那么在下面的schedue中就会被移出运行队列,这不是抢占的职责,如果非要怎么做就会出错,在dequeue_task中由array->queue已经为空了,在第二次真正出队的时候就会由于空指针引用而出错(这其实不会发生,因为只要从schedue回来,进程的状态肯定是TASK_RUNNING,仅仅是一个例子)。因此必须保证在将进程从运行队列移除的时候,它必须在运行队列,否则移个鸟啊!实际上PREEMPT_ACTIVE的作用就是防止将处于非TASK_RUNNING状态的进程并且没有在任何睡眠队列的进程移出运行队列,总之必须保证进程在一个队列中或者可以被唤醒,被抢占的进程是不能被唤醒的,如果它还不在运行队列中,那么它将永远不能再运行了。那么PREEMPT_ACTIVE是怎么保证被抢占的进程不会被移除运行队列呢?就是在preempt_schedule实现的:

asmlinkage void __sched preempt_schedule(void)

{

struct thread_info *ti = current_thread_info();

if (likely(ti->preempt_count || irqs_disabled()))

return;

do {

add_preempt_count(PREEMPT_ACTIVE); //设置PREEMPT_ACTIVE位,一直到下面的sub_preempt_count(PREEMPT_ACTIVE),这期间不能再抢占这个进程,不过再抢占也没有意义,如果非要抢占,出了下面的sub_preempt_count(PREEMPT_ACTIVE)也不迟

schedule();

sub_preempt_count(PREEMPT_ACTIVE); //抢占完毕后清除之

barrier();

} while (unlikely(test_thread_flag(TIF_NEED_RESCHED)));

}

除了这里之外,在早一些的内核中从中断返回内核空间时如果要抢占,在entery.S中也会加上这个这个PREEMPT_ACTIVE。现在还有一个问题,就是为何wait_event要用那种实现方式呢?为何需要一个循环呢?我的回答就是:这种情况下进程之所以能被唤醒就是因为它加入了一个睡眠队列,如果如你所说在schedule之后直接判断condition的话是不安全的,因为唤醒不一定是因为条件满足了,万一两个进程同时被唤醒那很可能有一个进程条件不能满足,如果正好此时进程被抢占,那么这个进程就没有机会加入睡眠队列了,也就没有机会被唤醒了,虽然PREEMPT_ACTIVE保证了这个进程不出运行队列,但是却失去了程序的本意,程序的本意是通过唤醒运行队列来使进程运行,而此时却成了完全依据优先级了,即使条件满足因为这个进程不在睡眠队列也不会被唤醒,系统就乱掉了。

其实很简单,必须在将进程加入到睡眠队列以后再判断条件,因为这样可以不漏掉唤醒通知,如果反过来的话,就是先判断再加入睡眠队列,如果在加入之前其它进程唤醒了这个睡眠队列,那么这个进程就会漏掉这次唤醒,之所以会有一个循环是因为可能不止一个进程被唤醒,那么就会出现竞争,这个循环就是为了竞争而设置的,这个循环保证了每个出了这个循环的进程都能安全带着结果为真的条件。

另外,说到TASK_RUNNING这个状态,又有人问了,为何在缺页中要把进程状态设置为TASK_RUNNING,难道缺页前不是TASK_RUNNING吗?大部分情况下应该是,可是linux内核不敢保证,之所以在handle_mm_fault中将进程状态设置为TASK_RUNNING是为了保证在缺页处理中如果睡眠,那么进程可以被唤醒,举个例子,在select中,当进程被设置为非TASK_RUNNING之后还会copy_from_user,而这却可能引起缺页。如果不把进程状态设置为TASK_RUNNING,那么万一在page fault中schedule了,那么这个进程就会被赶出运行队列,就再也回不来了,为了预防之,措施是:在任何调用schedule的地方分辨状态,然后设置进程状态,比如前面说的用PREEMPT_ACTIVE来预防,另外就是像handle_mm_fault中做到的一样,尽量使进程在TASK_RUNNABE状态下进入schedule。不过我想是不是现在这个应该去掉了,即使在缺页中不把进程设置为运行态,如果非要调度,也在之前设为运行台了。

ACTIVE_PREEMPT的作用:防止已经处于非运行态的进程还没有加入睡眠队列的时候就被抢占然后剔除出运行队列。这样就永远也回不来了,虽然这种情况很少见,一般都是先将进程放到睡眠队列再设置状态。

人只要不失去方向,就不会失去自己

linux内核抢占的几个细节

相关文章:

你感兴趣的文章:

标签云: