2012年8月4日星期六

关于linux内核抢占的一点思考

偶然看了一下rhel6.2系统内核中处理软中断的内核线程,很意外的产生了一点困
惑,这是ksoftirqd线程的主循环:

1    while (!kthread_should_stop()) {
2        preempt_disable();
3        if (!local_softirq_pending()) {
4            preempt_enable_no_resched();
5            schedule();
6            preempt_disable();
7        }
8
9        __set_current_state(TASK_RUNNING);
10
11        while (local_softirq_pending()) {
12            /* Preempt disable stops cpu going offline.
13               If already offline, we'll be on wrong CPU:
14               don't process */
15            if (cpu_is_offline((long)__bind_cpu))
16                goto wait_to_die;
17            do_softirq();
18            preempt_enable_no_resched();
19            cond_resched();
20            preempt_disable();
21            rcu_sched_qs((long)__bind_cpu);
22        }
23        preempt_enable();
24        set_current_state(TASK_INTERRUPTIBLE);
25    }

假设在23行执行完之后,有人试图唤醒这个线程,此时,线程还处于
TASK_RUNNING的状态,所以什么都不会发生。然后,在第24行,线程将自己设置
成了TASK_INTERRUPT状态,在执行第24行之后,线程被抢占了,那么,它在
TASK_INTERRUPT状态下被抢占,除非再次有人唤醒它,它岂不是再也返回不了了?
这样,不就丢失了一次唤醒吗?

这让我很困扰,于是,我自己写了个测试程序,启动一个内核线程,然后让它把
自己设置成TASK_INTERRUPTIBLE的状态,再进行几秒钟的忙循环。正常来说,执
行几秒钟的忙循环的话,肯定会被抢占了。然后我看它还能不能再被调度上来。
结果意外的发现rhel6的内核编译时是配置成不可抢占的(后来发现rhel5也这
样),我那个单核的虚拟机一执行忙循环就hang住。于是,重新编译了一个可抢
占的内核跑起来。结果发现,执行忙循环后,还是能被调度上来的。但如果我不
执行忙循环,而是调用schedule()函数,那么,除非被唤醒,否则就不能再被调
度上来了。

用systemtap跟踪了一下我的内核线程进入schedule()函数时的一些信息,结果
发现如果我主动调用schedule()函数,在进入schedule()函数时,thread_info
中的preempt_count的值是0x00000005,而如果在执行忙循环时被抢占调用
schedule函数时,preempt_count的值是0x10000005。这个最高位1在内核中被定
义为PREEMPT_ACTIVE。

原来,每当在抢占点上执行schedule函数时,这个标志会被设置,比如,从中断
返回时,调用:
retint_kernel
-> preempt_schedule_irq
   -> schedule

在preempt_schedule_irq函数中,就会调用这几行代码:

        add_preempt_count(PREEMPT_ACTIVE);
        local_irq_enable();
        schedule();
        local_irq_disable();
        sub_preempt_count(PREEMPT_ACTIVE);

设置PREEMPT_ACTIVE后,再调用schedule()函数,然后再清除PREEMPT_ACTIVE标
志。

而在schedule()函数中,有这样一段代码:

    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;
    }

prev就是当前要被调度下cpu的进程。prev->state为0表示TASK_RUNNING的状态。
如果进程的状态不是TASK_RUNNING并且PREEMPT_ACTIVE标志没有被设置,再判断进程是否有pending的信号,如果有,就把进程状态设置成TASK_RUNNING状态,
如果没有pending的信号,就把进程从RUNNING的进程队列里拿下来。

因此,逻辑关系是这样的:
如果PREEMPT_ACTIVE被设置了,说明进程是由于内核抢占被调度下CPU的,这时不
把它从RUNNING的队列里移除,如果进程不是由于内核抢占被调度下来的,看它
有没有未处理的信号,如果有的话,也不把它从RUNNING的队列里移除。只有再
上述两种情况都为假的情况下,进程才会被从RUNNING的队列里移除。

这就解释了为什么内核线程把自己设置成TASK_INTERRUPTIBLE状态之后,只要不
是主动调用schedule()函数,而是被抢占调度下cpu的,它就还会获得机会运行。

没有评论:

发表评论