学习和研究中前行,并在分享中提升自己

欢迎订阅阿里内推邮件



linux complete fair schedule 调度器研究:进程切换的触发和执行

阅读次数: 363| 时间:2018年2月19日 22:10 | 标签:linux

linux cfs 调度器研究:进程切换的触发和执行

上一篇主要介绍了cfs的一些基本的核心思想,今天想写一下进程切换触发和执行触发的时机。为什么我要把进程切换触发和执行分成两个部分了?在回答这个问题之前,我们先来说一下进程切换的两种场景。通常进程切换有两种情景:自愿(Voluntary)和强制(Involuntary)。自愿切换,通常是进程在等待某种资源,比如IO等;而强制切换通常是指当前进程还在运行被迫让出cpu,这种场景一般被称为抢占。

自愿切换

通常进程在主动sleep或者等待io返回都会主动让出cpu,触发重新高度。下面一段代码就是典型的自愿切换的场景

DEFINE_WAIT(wq);
for(;;) {
        prepare_to_wait(&mddev->sb_wait, &wq, TASK_UNINTERRUPTIBLE);
                if (atomic_read(&mddev->pending_writes)==0)
                        break;
                schedule();
        }    
        finish_wait(&mddev->sb_wait, &wq);

这段代码的含义是说当前进程放入到等待队列,如果条件不满足的话,则主动触发调度让出cpu。

强制切换(Involuntary)

强制切换分为两个部分触发切换和执行切换。之所以分两个部分,是因为触发切换时kernel只是设置了进程TIF_NEED_RESCHED这个flag,而此时并没有进程切换的实际动作。而是在随后的某些特定的时机内核会检查这个flag,最终调用schedule这个函数执行进程切换。下面我们看一下,会触发进程切换的时机。

切换触发时机

  • 周期时钟中断

时钟中断处理函数会调用scheduler_tick(),这个函数里面会检查当前cpu上目前正在执行的进程是否时间片已经用完,如果用完则需要触发调度。

  void scheduler_tick(void)
{
        int cpu = smp_processor_id();
        struct rq *rq = cpu_rq(cpu);
        struct task_struct *curr = rq->curr;

        sched_clock_tick();

        raw_spin_lock(&rq->lock);
        update_rq_clock(rq);
        update_cpu_load_active(rq);
        curr->sched_class->task_tick(rq, curr, 0); //检查当前cpu上的进程是否需要重新调度
        raw_spin_unlock(&rq->lock);

        perf_event_task_tick();

   #ifdef CONFIG_SMP
        rq->idle_balance = idle_cpu(cpu);
        trigger_load_balance(rq, cpu);
    #endif
        rq_last_tick_reset(rq);
}
  • 睡眠进程被唤醒的时候 当睡眠进程被唤醒时会通过check_preempt_curr这个函数来判断是否要抢占当前cpu上正在运行的进程,一般情况下唤醒的进程都会抢占当前正在运行的进程

  • 创建新进程的时候 当新进程被fork出来的时候,如果新进程的优先级高于当前cpu上正在运行的进程(比如在cfs 调度器设置了sched child runs first )。相应的调度器就会调用sched_fork(),它再通过调度类的 task_fork方法触发强制切换,抢占当前进程:

int sched_fork(unsigned long clone_flags, struct task_struct *p)
{
        ...
        if (p->sched_class->task_fork)
                p->sched_class->task_fork(p);
        ...
}
  • smp 场景下load_balance的时候
load_balance()
{
        ...
        move_tasks();
        ...
        resched_cpu();
        ...
}
  • 修改进程nice值的时候 通过set_user_nice修改进程nice值,如果该进程优化先级高于当前正在运行的进程,则会触发进程切换
void set_user_nice(struct task_struct *p, long nice)
{
    ...
    if (on_rq) {
    enqueue_task(rq, p, 0);
                /*   
                 * If the task increased its priority or is running and
                 * lowered its priority, then reschedule its CPU:
                 */
    if (delta < 0 || (delta > 0 && task_running(rq, p))) 
                        resched_task(rq->curr);
    }    
    ...
}

进程切换执行时机

之前一直有个误解,以为周期性的时钟中断的中断响应函数中就会执行切换,其实不是这样的。触发时只是设置了TIF_NEED_RESCHED标志告诉调度器需要进行进程重新调度,但是真正执行调度还要等内核代码发现这个标志才行,目前内核代码只在固定的几个点上检查TIF_NEED_RESCHED标志。这种强制进程切换,也叫做抢占。

通常抢占如果发生在用户态,叫做用户态抢占;如果发生在内核态,叫做内核态抢占

  • 用户态发生抢占的时机一:中断返回用户态的时候
retint_careful:
        CFI_RESTORE_STATE
        bt    $TIF_NEED_RESCHED,%edx
        jnc   retint_signal
        TRACE_IRQS_ON
        ENABLE_INTERRUPTS(CLBR_NONE)
        pushq_cfi %rdi
        SCHEDULE_USER  //调用schedule
        popq_cfi %rdi
        GET_THREAD_INFO(%rcx)
        DISABLE_INTERRUPTS(CLBR_NONE)
        TRACE_IRQS_OFF
        jmp retint_check                
  • 用户态发生抢占的时机二:系统调用返回用户态时
sysret_careful:
        bt $TIF_NEED_RESCHED,%edx
        jnc sysret_signal
        TRACE_IRQS_ON
        ENABLE_INTERRUPTS(CLBR_NONE)
        pushq_cfi %rdi
        SCHEDULE_USER
        popq_cfi %rdi
        jmp sysret_check
  • 内核态抢占 通常情况下内核是否能抢占取决于编译内核时候的选项
1. CONFIG_PREEMPT_NONE=y 不允许内核抢占。
2. CONFIG_PREEMPT_VOLUNTARY=y 容许内核代码调用cond_resched()主动让出CPU。    
3. CONFIG_PREEMPT=y 允许完全内核抢占。

总结

进程切换的多少也会影响整体性能,如果某个服务进程自愿切换的比较多,那么说明进程要经常等待的资源,这些资源比如锁等,这时候就需要重新设计一下相关代码,否则整体吞吐量上不去;如果强制切换的次数比较多,那说明服务器cpu资源已经达到瓶颈,需要增加cpu资源了。