PREEMPT_ACTIVE标志在内核抢占中的作用

http://linuxperf.com/?p=38
2016/01/25 vmunix
Linux从2.6开始支持内核抢占,意味着即使进程运行在内核态也可以被抢占。为了支持内核抢占,代码中为每个进程的 thread_info 引入了 preempt_count 计数器,数值为0的时候表示可以内核抢占,每当进程持有内核锁的时候把 preempt_count 计数器加1,表示禁止内核抢占。此外还有一些其它禁止内核抢占的场景,也通过 preempt_counter 字段反应出来:preempt_counter 字段是32位的,除了抢占计数器之外还包括其他标志位,只要 preempt_counter 整体不为0,就不能进行内核抢占,这个设计一下子简化了对众多不能抢占的情况的检测:

  • Bit 0- 7: 就是上文中所讲的抢占计数器,表示显式禁用内核抢占的次数;
  • Bit 8-15: 软中断计数器,记录可延迟函数被禁用的次数;
  • Bit 16-27: 硬中断计数器,表示中断处理程序的嵌套数,irq_enter()递增它,irq_exit()递减它;
  • Bit 28: PREEMPT_ACTIVE 标志。

这篇笔记要讲的是 PREEMPT_ACTIVE 标志。PREEMPT_ACTIVE标志的本意是表明正在进行内核抢占,设置了之后preempt_counter就不再为0,从而达到禁止内核抢占的效果,使得执行抢占工作的代码不会被再抢占。它的一个重要用途是防止非Running状态的进程被抢占过程错误地从Run Queue中移除。这句话令人十分费解,已经不处于Running状态的进程本来就不应该留在Run Queue中,为什么要防止它被移除?我用了很长时间才琢磨出来,要回答这个问题,首先要理解为什么非Running状态的进程会被抢占?所谓抢占,就是从一个正在运行的进程手上把CPU抢过来,可是既然进程已经不是Running状态了,怎么会还在CPU上,还被抢占?
这是因为进程从Running变成非Running要经过几个步骤:在把自己放进Wait Queue、状态置成非Running之后,最后调用schedule()把自己从Run Queue中移除、并把CPU交给其他进程。设想一下,一个进程恰好在调用schedule()之前就被抢占了,此时它仍然还在CPU上运行。这就是为什么非Running状态的进程也会被抢占的原因。对这样的进程,抢占流程不能擅自将之从Run Queue中移除,因为它的切换过程没有完成,应该让它有机会自己回头接着做完。比如以下的代码,是一个典型的休眠过程:

for (;;) {
    prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE);
    if (condition)
        break;
    schedule();
}

如果在第2行被抢占,刚把进程状态设置为TASK_UNINTERRUPTIBLE,本来马上就要测试条件是否满足了,这时被抢占,而抢占过程必定包含调用schedule()的步骤,导致该进程被移出运行队列,失去了运行机会,随后的条件判断语句就无法执行了,假如此时condition条件是满足的,它本来会跳出for循环、而不会去调用schedule()进入休眠,然而却被抢占过程错误地调用schedule()导致它休眠了,也因此错过了那个条件判断语句,也许就永远没有被唤醒的机会了。正确的做法是:进程被抢占后还留在Run Queue中,下次还有机会继续运行,恢复运行后继续判断condition,如果条件不满足,在随后主动调用的schedule()中会被移出运行队列,这是不能由抢占代劳的。

下面我们详细看看PREEMPT_ACTIVE是如何帮助实现上述的正确做法的。在内核里,进程从运行态进入休眠态的最后一步是呼叫调度器schedule()——把自己从Run Queue中移除,把CPU交给其他进程,这在不支持内核抢占的时代没有问题,因为整个过程不会被打断,然而内核抢占的出现使情况变复杂了,现在从运行态进入休眠态的过程可能会被抢占所打断,而抢占过程中会调用schedule(),导致schedule()的调用提前发生,有可能形成race condition。为了避免这种情况,内核抢占过程中不能直接呼叫schedule()调度器,而是呼叫preempt_schedule(),再通过它来调用schedule(),preempt_schedule()会在调用schedule()之前设置PREEMPT_ACTIVE标志,调用之后再清除这个标志。而schedule()会检查这个标志,如果设置了PREEMPT_ACTIVE标志,意味着这是从抢占过程中进入schedule(),对于不是TASK_RUNNING(state != 0)的进程,就不会调用deactivate_task()把进程从Run Queue移除。源码如下:

asmlinkage void __sched schedule(void)
{
        struct task_struct *prev, *next;
...
 
        if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
                if (unlikely(signal_pending_state(prev->state, prev)))
                        prev->state = TASK_RUNNING;
                else   
                        deactivate_task(rq, prev, DEQUEUE_SLEEP);
                switch_count = &prev->nvcsw;
        }
...

这段代码的逻辑含义是这样的:
如果设置了PREEMPT_ACTIVE,说明该进程是由于内核抢占而被调离CPU的,这时不把它从Run Queue里移除;如果PREEMPT_ACTIVE没被设置(进程不是由于内核抢占而被调离),还要看一下它有没有未处理的信号,如果有的话,也不把它从Run Queue移除。
总之,只要不是主动呼叫schedule(),而是因被抢占而调离CPU的,进程就还在运行队列中,还有机会运行。

猜你喜欢

转载自blog.csdn.net/hbcbgcx/article/details/90339806