注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

Tsecer的回音岛

Tsecer的博客

 
 
 

日志

 
 

可抢占内核中抢占时机分析  

2012-04-21 14:28:27|  分类: Linux内核 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
一、可抢占内核
为了更好的支持实时嵌入式系统,linux系统在内核中添加了内核态抢占,这也就说当中断/异常到来时,内核态同样可以发生抢占,这个功能对于一些相对比较耗时的系统调用来说是有意义的,例如select系统调用。这个内核态抢占和用户态线程抢占一样,对被抢占线程来说是不可见的,但是鉴于内核的特殊性,线程可以通过preempt_disable来禁止抢占。
在我之前的一篇关于多进程select同一个文件情况的讨论中,说明了select函数如何防止唤醒丢失的问题,主要的思想就是在进行任何的poll操作之前,先把自己设置为TASK_INTERRUPTIBLE状态,这样在遍地撒网的过程中如果有一个文件就绪,同样会将线程设置为可运行状态,从而保证线程可以被唤醒。这个操作可能比较耗时,所以函数的实现中在每执行一次poll函数之后就执行一个cond_resched()函数来主动判断是否需要重新调度,这样对于没有中断到来的时候也可以释放控制权,例如SMP系统中。
二、抢占来源
最为常见的就是定时器,我们看一下最为常见的sleep函数:
sys_nanosleep--->>hrtimer_nanosleep--->>>do_nanosleep---->>>hrtimer_init_sleeper
其中初始化定时器的唤醒操作为hrtimer_wakeup,该函数比较简单
static enum hrtimer_restart hrtimer_wakeup(struct hrtimer *timer)
{
    struct hrtimer_sleeper *t =
        container_of(timer, struct hrtimer_sleeper, timer);
    struct task_struct *task = t->task;

    t->task = NULL;
    if (task)
        wake_up_process(task);

    return HRTIMER_NORESTART;
}
int fastcall wake_up_process(struct task_struct *p)
{
    return try_to_wake_up(p, TASK_STOPPED | TASK_TRACED |
                 TASK_INTERRUPTIBLE | TASK_UNINTERRUPTIBLE, 0);注意这个标志为0,表示唤醒时可以抢占
}
try_to_wake_up
    if (!sync || cpu != this_cpu) {
        if (TASK_PREEMPTS_CURR(p, rq))
            resched_task(rq->curr);注意,这里设置的需要调度标志是当前运行任务而不是被唤醒任务
    }
static inline void resched_task(struct task_struct *p)
{
    assert_spin_locked(&task_rq(p)->lock);
    set_tsk_need_resched(p);
}
static inline void set_tsk_need_resched(struct task_struct *tsk)
{
    set_tsk_thread_flag(tsk,TIF_NEED_RESCHED);
}
三、从中断返回
linux-2.6.21\arch\i386\kernel\entry.S
ret_from_intr:
    GET_THREAD_INFO(%ebp)
check_userspace:
    movl PT_EFLAGS(%esp), %eax    # mix EFLAGS and CS
    movb PT_CS(%esp), %al
    andl $(VM_MASK | SEGMENT_RPL_MASK), %eax
    cmpl $USER_RPL, %eax
    jb resume_kernel        # not returning to v8086 or userspace
…………
#ifdef CONFIG_PREEMPT
ENTRY(resume_kernel)
    DISABLE_INTERRUPTS(CLBR_ANY)
    cmpl $0,TI_preempt_count(%ebp)    # non-zero preempt_count ? 首先判断是否主动禁止了抢占,select没有禁止,所以继续执行
    jnz restore_nocheck
need_resched:
    movl TI_flags(%ebp), %ecx    # need_resched set ?
    testb $_TIF_NEED_RESCHED, %cl 这个TIF_NEED_RESCHED在之前已经设置过,所以满足,继续执行
    jz restore_all
    testl $IF_MASK,PT_EFLAGS(%esp)    # interrupts off (exception path) ?
    jz restore_all
    call preempt_schedule_irq  调用preempt_schedule_irq函数,此处发生抢占,对被抢占线程透明
    jmp need_resched
END(resume_kernel)
#endif
四、抢占后调度
preempt_schedule_irq函数主要代码
    add_preempt_count(PREEMPT_ACTIVE);
    local_irq_enable();
    schedule();
    local_irq_disable();
    sub_preempt_count(PREEMPT_ACTIVE);
在asmlinkage void __sched schedule(void)函数中
if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {对于被抢占调度,该if条件并不满足
        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);
        }
    }
前面的 !(preempt_count() & PREEMPT_ACTIVE)对于抢占来说是不满足的,所以不会执行接下来的deactivate_task函数。我们想一下如果没有这个判断会怎样?下面是select内核实现
linux-2.6.21\fs\select.c
int do_select(int n, fd_set_bits *fds, s64 *timeout)

    for (;;) {
        unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
        long __timeout;

        set_current_state(TASK_INTERRUPTIBLE);这 个地方时一个提前亮,早于所有poll操作前设置线程为TASK_INTERRUPTIBLE,如果在所有的poll之后设置,那么可能在poll的过程 中之前已经挂载的某个poll会把线程设置为RUNNING态,着个 状态会被最后设置的TASK_INTERRUPTIBLE冲掉,从而产生唤醒丢失
……
                    if (f_op && f_op->poll)
                        mask = (*f_op->poll)(file, retval ? NULL : wait);循环调用所有文件的poll接口
……
                cond_resched(); 主动判断是否需要重新执行一次调度
        __timeout = schedule_timeout(__timeout);
}
如果没有这个判断,假设select函数在set_current_state(TASK_INTERRUPTIBLE)执行之后被抢占,进而被从可运行队列中摘除,那么执行select的任务还没有被挂接到任何一个唤醒队列上就被剥夺了调度权,也就意味着它永远没有机会再次获得调度了,除非主动唤醒,例如向它发送信号或者任务定时器到时等。
所以,抢占之后被抢占的任务即使它的状态已经不再是RUNNING状态,它依然会被放在可运行任务队列中,可以被调度器选择到,这里也说明了抢占调度的一个重要特征
五、cond_resched主动检测
对于一些比较耗时的系统调用,它通常会在合适的时机自觉主动的来检测是否需要重新调度,例如在select中每次执行
这一点对于任务主动通过cond_resched调用同样适用
do_select-->>>cond_resched--->>>__cond_resched--->>
static void __cond_resched(void)
{
#ifdef CONFIG_DEBUG_SPINLOCK_SLEEP
    __might_sleep(__FILE__, __LINE__);
#endif
    /*
     * The BKS might be reacquired before we have dropped
     * PREEMPT_ACTIVE, which could trigger a second
     * cond_resched() call.
     */
    do {
        add_preempt_count(PREEMPT_ACTIVE);
        schedule();
        sub_preempt_count(PREEMPT_ACTIVE);
    } while (need_resched());
}
六、从preempt_disable到preempt_enable转换时隐式调用
#define preempt_enable() \
do { \
    preempt_enable_no_resched(); \
    barrier(); \
    preempt_check_resched(); \
} while (0)

#define preempt_check_resched() \
do { \
    if (unlikely(test_thread_flag(TIF_NEED_RESCHED))) \
        preempt_schedule(); \
} while (0)
asmlinkage void __sched preempt_schedule(void)
need_resched:
    add_preempt_count(PREEMPT_ACTIVE);
    schedule();
    sub_preempt_count(PREEMPT_ACTIVE);
由于很多的解锁操作都会附带执行这个preempt_enable调用,所以这个地方的抢占执行还是比较多的,例如spin_unlock_irq。
七、简单总结
1、唤醒操作
所有的唤醒操作最终都是通过try_to_wake_up函数来实现的,例如__WAITQUEUE_INITIALIZER中使用的default_wake_function、定时器hrtimer_init_sleeper中使用的hrtimer_wakeup--->>wake_up_process都是如此。而在try_to_wake_up最后
    if (!sync || cpu != this_cpu) {
        if (TASK_PREEMPTS_CURR(p, rq))
            resched_task(rq->curr);此处并没有直接调用schedule接口,而只是设置了标志位,所以其他地方需要检测这个标志并进行可能的调度
    }
2、抢占时不从可运行队列函数
在前面说的三种抢占模式中,在调用schedule函数之前都会设置PREEMPT_ACTIVE标志,而在schedule函数中同样会判断该标志位,如果置位则不会把线程从可运行队列中删除,这一点是为了被抢占者的代码,否则需要更多的机制来保证任务状态的一致性。
3、可运行任务的选择
在schedule函数中,查找下一个可运行任务的流程为   
idx = sched_find_first_bit(array->bitmap);
    queue = array->queue + idx;
    next = list_entry(queue->next, struct task_struct, run_list);
事实上,调度器只在run_list上选择下一个运行线程,所以任务最重是否可运行事实上都是通过对自己在run_list的入队和出队实现的:
__activate_task---->>>enqueue_task-
list_add_tail(&p->run_list, array->queue + p->prio);
deactivate_task---->>>dequeue_task
    list_del(&p->run_list);
  评论这张
 
阅读(2511)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017