ASM:Linux进程调度:CFS调度器的设计框架(2)
3、调度函数schedule()分析
当kernel/sched.c:sched_tick()执行完,并且时钟中断返回时,就会调用kernel/sched.c:schedule()完成进程切换。我们也可以显示调用schedule(),例如在前面“Linux进程管理“的介绍中,进程销毁的do_exit()最后就直接调用schedule(),以切换到下一个进程。
schedule()是内核和其他部分用于调用进程调度器的入口,选择哪个进程可以运行,何时将其投入运行。schedule通常都和一个具体的调度类相关联,例如对CFS会关联到调度类fair_sched_class,对实时调度会关到rt_sched_class,也就是说,它会找到一个最高优先级的调度类,后者有自己的可运行队列,然后问后者谁才是下一个该运行的进程。从kernel/sched.c:sched_init()中我们可以看到有一行为"current->sched_class = &fair_sched_class",可见它初始时使用的是CFS的调度类。schedule函数唯一重要的事情是,它会调用pick_next_task。schedule()代码如下:
1 asmlinkage void __sched schedule(void) 2 { 3 struct task_struct *prev, *next; 4 unsigned long *switch_count; 5 struct rq *rq; 6 int cpu; 7 8 need_resched: 9 preempt_disable(); 10 cpu = smp_processor_id(); /* 获取当前cpu */ 11 rq = cpu_rq(cpu); /* 得到指定cpu的rq */ 12 rcu_sched_qs(cpu); 13 prev = rq->curr; /* 当前的运行进程 */ 14 switch_count = &prev->nivcsw; /* 进程切换计数 */ 15 16 release_kernel_lock(prev); 17 need_resched_nonpreemptible: 18 19 schedule_debug(prev); 20 21 if (sched_feat(HRTICK)) 22 hrtick_clear(rq); 23 24 spin_lock_irq(&rq->lock); 25 update_rq_clock(rq); /* 更新rq的clock属性 */ 26 clear_tsk_need_resched(prev); /* 清楚prev进程的调度位 */ 27 28 if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) { 29 if (unlikely(signal_pending_state(prev->state, prev))) 30 prev->state = TASK_RUNNING; 31 else /* 从运行队列中删除prev进程,根据调度类的不同实现不同, 32 对CFS,deactivate_task调用dequeue_task_fair完成进程删除 */ 33 deactivate_task(rq, prev, 1); 34 switch_count = &prev->nvcsw; 35 } 36 37 pre_schedule(rq, prev); /* 现只对实时进程有用 */ 38 39 if (unlikely(!rq->nr_running)) 40 idle_balance(cpu, rq); 41 42 put_prev_task(rq, prev); /* 将切换出去进程插到队尾 */ 43 next = pick_next_task(rq); /* 选择下一个要运行的进程 */ 44 45 if (likely(prev != next)) { 46 sched_info_switch(prev, next); /* 更新进程的相关调度信息 */ 47 perf_event_task_sched_out(prev, next, cpu); 48 49 rq->nr_switches++; /* 切换次数记录 */ 50 rq->curr = next; 51 ++*switch_count; 52 /* 进程上下文切换 */ 53 context_switch(rq, prev, next); /* unlocks the rq */ 54 /* 55 * the context switch might have flipped the stack from under 56 * us, hence refresh the local variables. 57 */ 58 cpu = smp_processor_id(); 59 rq = cpu_rq(cpu); 60 } else 61 spin_unlock_irq(&rq->lock); 62 63 post_schedule(rq); /* 对于实时进程有用到 */ 64 65 if (unlikely(reacquire_kernel_lock(current) < 0)) 66 goto need_resched_nonpreemptible; 67 68 preempt_enable_no_resched(); 69 if (need_resched()) 70 goto need_resched; 71 } 72 EXPORT_SYMBOL(schedule);
(1)清除调度位:如果之前设置了need_resched标志,则需要重新调度进程。先获取cpu和rq,当前进程成为prev进程,清除它的调度位。cpu_rq()函数在sched.c中定义为一个宏:
#define cpu_rq(cpu) (&per_cpu(runqueues, (cpu)))
该函数通过向上加偏移的方式得到rq,这里可以看出runqueues为一个rq结构的数组,cpu为数组下标。
(2)删除切换出去的进程:调用deactivate_task从运行队列中删除prev进程。根据调度类的不同实现不同,对CFS,deactivate_task调用dequeue_task_fair完成进程删除。deactive_task()如下:
1 static void deactivate_task(struct rq *rq, struct task_struct *p, int sleep) 2 { 3 if (task_contributes_to_load(p)) 4 rq->nr_uninterruptible++; 5 6 dequeue_task(rq, p, sleep); /* 具体操作 */ 7 dec_nr_running(rq); /* rq中当前进程的运行数减一 */ 8 } 9 10 static void dequeue_task(struct rq *rq, struct task_struct *p, int sleep) 11 { 12 if (sleep) { /* 如果sleep不为0,更新调度实体se中的相关变量 */ 13 if (p->se.last_wakeup) { 14 update_avg(&p->se.avg_overlap, 15 p->se.sum_exec_runtime - p->se.last_wakeup); 16 p->se.last_wakeup = 0; 17 } else { 18 update_avg(&p->se.avg_wakeup, 19 sysctl_sched_wakeup_granularity); 20 } 21 } 22 /* 更新进程的sched_info数据结构中相关属性 */ 23 sched_info_dequeued(p); 24 /* 调用具体调度类的函数从它的运行队列中删除 */ 25 p->sched_class->dequeue_task(rq, p, sleep); 26 p->se.on_rq = 0; 27 }
对CFS,sched_class会关联到kernel/sched_fair.c中的fair_sched_class,因此最终是调用dequeue_task_fair从运行队列中删除切换出去的进程。dequeue_task_fair在前面已分析过。
(3)将切换出去进程插到队尾:调用put_prev_task(),将当前进程,也就是被切换出去的进程重新插入到各自的运行队列中,对于CFS算法插入到红黑树的合适位置上,对于实时调度插入到同一个优先级队列的链表尾部。
(4)选择下一个要运行的进程:调用pick_next_task(),从运行队列中选择下一个要运行的进程。对CFS,执行的是pick_next_task_fair。它会选择红黑树最左叶子节点的进程(在所有进程中它的vruntime值最小)。pick_next_task()如下:
1 static inline struct task_struct * 2 pick_next_task(struct rq *rq) 3 { 4 const struct sched_class *class; 5 struct task_struct *p; 6 7 /* 8 * Optimization: we know that if all tasks are in 9 * the fair class we can call that function directly: 10 */ 11 if (likely(rq->nr_running == rq->cfs.nr_running)) { 12 p = fair_sched_class.pick_next_task(rq); 13 if (likely(p)) 14 return p; 15 } 16 17 class = sched_class_highest; 18 for ( ; ; ) { /* 对每一个调度类 */ 19 /* 调用该调度类中的函数,找出下一个task */ 20 p = class->pick_next_task(rq); 21 if (p) 22 return p; 23 /* 24 * Will never be NULL as the idle class always 25 * returns a non-NULL p: 26 */ 27 class = class->next; /* 访问下一个调度类 */ 28 } 29 }
该函数以优先级为序,从高到低,依次检查每个调度类并且从高优先级的调度类中,选择最高优先级的进程。前面的优化是说如果所有的进程都在CFS调度类中,则可以直接调用fair_sched_class中的pick_next_task_fair函数找出下一个要运行的进程。这个函数前面分析过。
(5)调度信息更新:调用kernel/sched_stats.h中的sched_info_switch(),以更新切换出去和进来进程以及对应rq的相关变量。该函数最终是调用__sched_info_switch()完成工作。如下:
1 static inline void 2 __sched_info_switch(struct task_struct *prev, struct task_struct *next) 3 { 4 struct rq *rq = task_rq(prev); 5 6 /* 7 * prev now departs the cpu. It's not interesting to record 8 * stats about how efficient we were at scheduling the idle 9 * process, however. 10 */ 11 if (prev != rq->idle) /* 如果被切换出去的进程不是idle进程 */ 12 sched_info_depart(prev); /* 更新prev进程和他对应rq的相关变量 */ 13 14 if (next != rq->idle) /* 如果切换进来的进程不是idle进程 */ 15 sched_info_arrive(next); /* 更新next进程和对应队列的相关变量 */ 16 } 17 18 static inline void sched_info_depart(struct task_struct *t) 19 { 20 /* 计算进程在rq中运行的时间长度 */ 21 unsigned long long delta = task_rq(t)->clock - 22 t->sched_info.last_arrival; 23 /* 更新RunQueue中的Task所得到CPU执行时间的累加值 */ 24 rq_sched_info_depart(task_rq(t), delta); 25 26 /* 如果被切换出去进程的状态是运行状态 27 那么将进程sched_info.last_queued设置为rq的clock 28 last_queued为最后一次排队等待运行的时间 */ 29 if (t->state == TASK_RUNNING) 30 sched_info_queued(t); 31 } 32 33 static void sched_info_arrive(struct task_struct *t) 34 { 35 unsigned long long now = task_rq(t)->clock, delta = 0; 36 37 if (t->sched_info.last_queued) /* 如果被切换进来前在运行进程中排队 */ 38 delta = now - t->sched_info.last_queued; /* 计算排队等待的时间长度 */ 39 sched_info_reset_dequeued(t); /* 因为进程将被切换进来运行,设定last_queued为0 */ 40 t->sched_info.run_delay += delta; /* 更新进程在运行队列里面等待的时间 */ 41 t->sched_info.last_arrival = now; /* 更新最后一次运行的时间 */ 42 t->sched_info.pcount++; /* cpu上运行的次数加一 */ 43 /* 更新rq中rq_sched_info中的对应的变量 */ 44 rq_sched_info_arrive(task_rq(t), delta); 45 }
(6)上下文切换:调用kernel/sched.c中的context_switch()来完成进程上下文的切换,包括切换到新的内存页、寄存器状态和栈,以及切换后的清理工作(例如如果是因为进程退出而导致进程调度,则需要释放退出进程的进程描述符PCB)。schedule()的大部分核心工作都在这个函数中完成。如下:
1 static inline void 2 context_switch(struct rq *rq, struct task_struct *prev, 3 struct task_struct *next) 4 { 5 struct mm_struct *mm, *oldmm; 6 7 prepare_task_switch(rq, prev, next); 8 trace_sched_switch(rq, prev, next); 9 mm = next->mm; 10 oldmm = prev->active_mm; 11 /* 12 * For paravirt, this is coupled with an exit in switch_to to 13 * combine the page table reload and the switch backend into 14 * one hypercall. 15 */ 16 arch_start_context_switch(prev); 17 18 if (unlikely(!mm)) { /* 如果被切换进来的进程的mm为空 */ 19 next->active_mm = oldmm; /* 将共享切换出去进程的active_mm */ 20 atomic_inc(&oldmm->mm_count); /* 有一个进程共享,所有引用计数加一 */ 21 /* 将per cpu变量cpu_tlbstate状态设为LAZY */ 22 enter_lazy_tlb(oldmm, next); 23 } else /* 如果mm不为空,那么进行mm切换 */ 24 switch_mm(oldmm, mm, next); 25 26 if (unlikely(!prev->mm)) { /* 如果切换出去的mm为空,从上面 27 可以看出本进程的active_mm为共享先前切换出去的进程 28 的active_mm,所有需要在这里置空 */ 29 prev->active_mm = NULL; 30 rq->prev_mm = oldmm; /* 更新rq的前一个mm结构 */ 31 } 32 /* 33 * Since the runqueue lock will be released by the next 34 * task (which is an invalid locking op but in the case 35 * of the scheduler it's an obvious special-case), so we 36 * do an early lockdep release here: 37 */ 38 #ifndef __ARCH_WANT_UNLOCKED_CTXSW 39 spin_release(&rq->lock.dep_map, 1, _THIS_IP_); 40 #endif 41 42 /* 这里切换寄存器状态和栈 */ 43 switch_to(prev, next, prev); 44 45 barrier(); 46 /* 47 * this_rq must be evaluated again because prev may have moved 48 * CPUs since it called schedule(), thus the 'rq' on its stack 49 * frame will be invalid. 50 */ 51 finish_task_switch(this_rq(), prev); 52 }
该函数调用switch_mm()切换进程的mm,调用switch_to()切换进程的寄存器状态和栈,调用finish_task_switch()完成切换后的清理工作。
switch_mm()的实现与体系结构相关,对x86,在arch/x86/include/asm/mmu_context.h中,如下:
1 static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next, 2 struct task_struct *tsk) 3 { 4 unsigned cpu = smp_processor_id(); 5 6 if (likely(prev != next)) { 7 #ifdef CONFIG_SMP 8 /* 设置per cpu变量tlb */ 9 percpu_write(cpu_tlbstate.state, TLBSTATE_OK); 10 percpu_write(cpu_tlbstate.active_mm, next); 11 #endif 12 /* 将要被调度运行进程拥有的内存描述结构 13 的CPU掩码中当前处理器号对应的位码设置为1 */ 14 cpumask_set_cpu(cpu, mm_cpumask(next)); 15 16 /* Re-load page tables */ 17 load_cr3(next->pgd); /* 将切换进来进程的pgd load到cr3寄存器 */ 18 19 /* 将被替换进程使用的内存描述结构的CPU 20 掩码中当前处理器号对应的位码清0 */ 21 cpumask_clear_cpu(cpu, mm_cpumask(prev)); 22 23 /* 24 * load the LDT, if the LDT is different: 25 */ 26 if (unlikely(prev->context.ldt != next->context.ldt)) 27 load_LDT_nolock(&next->context); 28 } 29 #ifdef CONFIG_SMP 30 else { /* 如果切换的两个进程相同 */ 31 percpu_write(cpu_tlbstate.state, TLBSTATE_OK); 32 BUG_ON(percpu_read(cpu_tlbstate.active_mm) != next); 33 34 if (!cpumask_test_and_set_cpu(cpu, mm_cpumask(next))) { 35 /* We were in lazy tlb mode and leave_mm disabled 36 * tlb flush IPI delivery. We must reload CR3 37 * to make sure to use no freed page tables. 38 */ 39 load_cr3(next->pgd); 40 load_LDT_nolock(&next->context); 41 } 42 } 43 #endif 44 }
该函数主要工作包括设置per cpu变量tlb、设置新进程的mm的CPU掩码位、重新加载页表、清除原来进程的mm的CPU掩码位。
switch_to()用于切换进程的寄存器状态和栈,实现也与体系结构有关,对x86,在arch/x86/include/asm/system.h中。该函数被定义为一个宏,如下:
1 #define switch_to(prev, next, last) \ 2 do { \ 3 /* \ 4 * Context-switching clobbers all registers, so we clobber \ 5 * them explicitly, via unused output variables. \ 6 * (EAX and EBP is not listed because EBP is saved/restored \ 7 * explicitly for wchan access and EAX is the return value of \ 8 * __switch_to()) \ 9 */ \ 10 unsigned long ebx, ecx, edx, esi, edi; \ 11 \ 12 asm volatile("pushfl\n\t" /* save flags */ \ 13 "pushl %%ebp\n\t" /* save EBP */ \ 14 "movl %%esp,%[prev_sp]\n\t" /* save ESP */ \ 15 "movl %[next_sp],%%esp\n\t" /* restore ESP */ \ 16 "movl $1f,%[prev_ip]\n\t" /* save EIP */ \ 17 /* 将next_ip入栈,下面用jmp跳转,这样 18 返回到标号1时就切换过来了 */ 19 "pushl %[next_ip]\n\t" /* restore EIP */ \ 20 __switch_canary \ 21 "jmp __switch_to\n" /* 跳转到C函数__switch_to,执行完后返回到下面的标号1处 */ \ 22 "1:\t" \ 23 "popl %%ebp\n\t" /* restore EBP */ \ 24 "popfl\n" /* restore flags */ \ 25 \ 26 /* output parameters */ \ 27 : [prev_sp] "=m" (prev->thread.sp), \ 28 [prev_ip] "=m" (prev->thread.ip), \ 29 "=a" (last), \ 30 \ 31 /* clobbered output registers: */ \ 32 "=b" (ebx), "=c" (ecx), "=d" (edx), \ 33 "=S" (esi), "=D" (edi) \ 34 \ 35 __switch_canary_oparam \ 36 \ 37 /* input parameters: */ \ 38 : [next_sp] "m" (next->thread.sp), \ 39 [next_ip] "m" (next->thread.ip), \ 40 \ 41 /* regparm parameters for __switch_to(): */ \ 42 [prev] "a" (prev), \ 43 [next] "d" (next) \ 44 \ 45 __switch_canary_iparam \ 46 \ 47 : /* reloaded segment registers */ \ 48 "memory"); \ 49 } while (0)
该函数用汇编代码保持各种寄存器的值,然后通过jmp调用C函数__switch_to,完成寄存器状态和栈的切换。对32位系统,__switch_to()函数在arch/x86/kernel/process_32.c中,如下:
1 __notrace_funcgraph struct task_struct * 2 __switch_to(struct task_struct *prev_p, struct task_struct *next_p) 3 { 4 struct thread_struct *prev = &prev_p->thread, 5 *next = &next_p->thread; 6 int cpu = smp_processor_id(); 7 /* init_tss为一个per cpu变量 */ 8 struct tss_struct *tss = &per_cpu(init_tss, cpu); 9 bool preload_fpu; 10 11 /* never put a printk in __switch_to... printk() calls wake_up*() indirectly */ 12 13 /* 14 * If the task has used fpu the last 5 timeslices, just do a full 15 * restore of the math state immediately to avoid the trap; the 16 * chances of needing FPU soon are obviously high now 17 */ 18 preload_fpu = tsk_used_math(next_p) && next_p->fpu_counter > 5; 19 20 __unlazy_fpu(prev_p); /* 保存FPU寄存器 */ 21 22 /* we're going to use this soon, after a few expensive things */ 23 if (preload_fpu) 24 prefetch(next->xstate); 25 26 /* 27 * 重新载入esp0:把next_p->thread.esp0装入对应于本地cpu的tss的esp0 28 * 字段;任何由sysenter汇编指令产生的从用户态 29 * 到内核态的特权级转换将把这个地址拷贝到esp寄存器中 30 */ 31 load_sp0(tss, next); 32 33 /* 34 * Save away %gs. No need to save %fs, as it was saved on the 35 * stack on entry. No need to save %es and %ds, as those are 36 * always kernel segments while inside the kernel. Doing this 37 * before setting the new TLS descriptors avoids the situation 38 * where we temporarily have non-reloadable segments in %fs 39 * and %gs. This could be an issue if the NMI handler ever 40 * used %fs or %gs (it does not today), or if the kernel is 41 * running inside of a hypervisor layer. 42 */ 43 lazy_save_gs(prev->gs); 44 45 /* 46 * 装载每个线程的线程局部存储描述符:把next进程使用的线程局部存储(TLS)段 47 * 装入本地CPU的全局描述符表;三个段选择符保存在进程描述符 48 * 内的tls_array数组中 49 */ 50 load_TLS(next, cpu); 51 52 /* 53 * Restore IOPL if needed. In normal use, the flags restore 54 * in the switch assembly will handle this. But if the kernel 55 * is running virtualized at a non-zero CPL, the popf will 56 * not restore flags, so it must be done in a separate step. 57 */ 58 if (get_kernel_rpl() && unlikely(prev->iopl != next->iopl)) 59 set_iopl_mask(next->iopl); 60 61 /* 62 * Now maybe handle debug registers and/or IO bitmaps 63 */ 64 if (unlikely(task_thread_info(prev_p)->flags & _TIF_WORK_CTXSW_PREV || 65 task_thread_info(next_p)->flags & _TIF_WORK_CTXSW_NEXT)) 66 __switch_to_xtra(prev_p, next_p, tss); 67 68 /* If we're going to preload the fpu context, make sure clts 69 is run while we're batching the cpu state updates. */ 70 if (preload_fpu) 71 clts(); 72 73 /* 74 * Leave lazy mode, flushing any hypercalls made here. 75 * This must be done before restoring TLS segments so 76 * the GDT and LDT are properly updated, and must be 77 * done before math_state_restore, so the TS bit is up 78 * to date. 79 */ 80 arch_end_context_switch(next_p); 81 82 if (preload_fpu) 83 __math_state_restore(); /* 恢复FPU寄存器 */ 84 85 /* 86 * Restore %gs if needed (which is common) 87 */ 88 if (prev->gs | next->gs) 89 lazy_load_gs(next->gs); 90 91 percpu_write(current_task, next_p); 92 93 return prev_p; 94 }
主要工作包括保存FPU寄存器、重新装载esp0、装载每个线程的TLS段、恢复FPU寄存器等。__unlazy_fpu()函数在arch/x86/include/asm/i387.h中,如下:
static inline void __unlazy_fpu(struct task_struct *tsk) { /* 如果进程使用了FPU/MMX/SSE或SSE2指令 */ if (task_thread_info(tsk)->status & TS_USEDFPU) { __save_init_fpu(tsk); /* 保存相关的硬件上下文 */ stts(); } else tsk->fpu_counter = 0; } /* * These must be called with preempt disabled */ static inline void __save_init_fpu(struct task_struct *tsk) { /* 如果CPU使用SSE/SSE2扩展 */ if (task_thread_info(tsk)->status & TS_XSAVE) { struct xsave_struct *xstate = &tsk->thread.xstate->xsave; struct i387_fxsave_struct *fx = &tsk->thread.xstate->fxsave; xsave(tsk); /* * xsave header may indicate the init state of the FP. */ if (!(xstate->xsave_hdr.xstate_bv & XSTATE_FP)) goto end; if (unlikely(fx->swd & X87_FSW_ES)) asm volatile("fnclex"); /* * we can do a simple return here or be paranoid :) */ goto clear_state; } /* Use more nops than strictly needed in case the compiler varies code */ alternative_input( "fnsave %[fx] ;fwait;" GENERIC_NOP8 GENERIC_NOP4, "fxsave %[fx]\n" "bt $7,%[fsw] ; jnc 1f ; fnclex\n1:", X86_FEATURE_FXSR, [fx] "m" (tsk->thread.xstate->fxsave), [fsw] "m" (tsk->thread.xstate->fxsave.swd) : "memory"); clear_state: /* AMD K7/K8 CPUs don't save/restore FDP/FIP/FOP unless an exception is pending. Clear the x87 state here by setting it to fixed values. safe_address is a random variable that should be in L1 */ alternative_input( GENERIC_NOP8 GENERIC_NOP2, "emms\n\t" /* clear stack tags */ "fildl %[addr]", /* set F?P to defined value */ X86_FEATURE_FXSAVE_LEAK, [addr] "m" (safe_address)); end: task_thread_info(tsk)->status &= ~TS_USEDFPU; /* 重置TS_USEDFPU标志 */ }
包含在thread_info描述符的status字段中的TS_USEDFPU标志,表示进程在当前执行的过程中是否使用过FPU/MMU/XMM寄存器。在__unlazy_fpu中可以看到,如果进程执行过程中使用了FPU/MMX/SSE或SSE2指令,则内核必须保存相关的硬件上下文,这由__save_init_fpu()完成,它会调用xsave()保存硬件相关的状态信息,注意保存完之后要重置TS_USEDFPU标志。
回到context_switch(),在完成mm、寄存器和栈的切换之后,context_swith最后调用kernel/sched.c中的finish_task_switch()完成进程切换后的一些清理工作。例如,如果是因为进程退出(成为TASK_DEAD状态)而导致进程调度,则需要释放退出进程的PCB,由finish_task_switch()调用put_task_struct()完成这项工作。put_task_struct()在include/linux/sched.h中,它直接调用kernel/fork.c中的__put_task_struct(),清理工作的调用链为__put_task_struct()--->free_task()--->free_task_struct()。在fork.c中,free_task_struct()工作直接由kmem_cache_free()函数完成,如下:
# define free_task_struct(tsk) kmem_cache_free(task_struct_cachep, (tsk))
context_switch执行完后,schedule()的工作基本就完成了。至此,进程调度完成,新的进程被调入CPU运行。
从以上讨论看出,CFS对以前的调度器进行了很大改动。用红黑树代替优先级数组;用完全公平的策略代替动态优先级策略;引入了模块管理器;它修改了原来Linux2.6.0调度器模块70%的代码。结构更简单灵活,算法适应性更高。相比于RSDL,虽然都基于完全公平的原理,但是它们的实现完全不同。相比之下,CFS更加清晰简单,有更好的扩展性。
CFS还有一个重要特点,即调度粒度小。CFS之前的调度器中,除了进程调用了某些阻塞函数而主动参与调度之外,每个进程都只有在用完了时间片或者属于自己的时间配额之后才被抢占。而CFS则在每次tick都进行检查,如果当前进程不再处于红黑树的左边,就被抢占。在高负载的服务器上,通过调整调度粒度能够获得更好的调度性能。
进程调度的完整流程(使用CFS算法)总结如下:
1 fork() 2 --->kernel/sched_fair.c:enqueue_task_fair() 新进程最后进入红黑树队列 3 4 kernel/sched.c:sched_tick() 被时钟tick中断直接调用 5 --->sched_class->task_tick()==>kernel/sched_fair.c:task_tick_fair() 6 --->kernel/sched_fair.c:entity_tick() 处理tick中断 7 --->update_curr() 更新当前进程的运行时统计信息 8 --->__update_curr() 更新进程的vruntime 9 --->calc_delta_fair() 计算负载权重值 10 --->kernel/sched.c:calc_delta_mine() 修正delta值 11 --->check_preempt_tick() 检测是否需要重新调度 12 --->kernel/sched.c:resched_task() 设置need_resched标志 13 --->include/linux/sched.h:set_tsk_need_resched(p) 完成设置工作 14 kernel/sched.c:schedule() 中断返回时调用,完成进程切换 15 --->include/linux/sched.h:clear_tsk_need_resched() 清除调度位 16 --->kernel/sched.c:deactivate_task() 删除切换出去的进程(pre进程) 17 --->dequeue_task() 18 --->kernel/sched_fair.c:dequeue_task_fair() 从红黑树中删除pre进程 19 --->dequeue_entity() 20 --->__dequeue_entity() 21 --->lib/rbtree.c:rb_erase() 删除pre进程 22 --->dec_nr_running() 递减nr_running 23 --->kernel/sched.c:put_prev_task() 将切换出去的进程插入到队尾 24 --->kernel/sched_fair.c:put_prev_task_fair() 25 --->put_prev_entity() 26 --->__enqueue_entity() 27 --->搜索红黑树,找到插入位置并插入 28 --->缓存最左边的节点进程 29 --->kernel/sched.c:pick_next_task() 选择下一个要运行的进程 30 --->kernel/sched_fair.c:pick_next_task_fair() 31 --->pick_next_entity() 32 --->__pick_next_entity() 33 --->include/linux/rbtree.h:rb_entry(left,...) 返回红黑树最左边的节点进程 34 --->include/linux/kernel.h:container_of() 35 --->kernel/sched_stats.h:sched_info_switch() 更新调度信息(rq相关变量) 36 --->sched_info_depart() 37 --->sched_info_arrive() 38 --->kernel/sched.c:context_switch() 切换进程上下文 39 --->arch/x86/include/asm/mmu_context.h:switch_mm() 切换内存页 40 --->设置新进程的CPU掩码位,重新加载页表等 41 --->arch/x86/include/asm/system.h:switch_to() 切换寄存器状态和栈 42 --->arch/x86/kernel/process_32.c:__switch_to() 43 --->arch/x86/include/asm/i387.h:__unlazy_fpu() 保存FPU寄存器 44 --->__save_init_fpu() 若使用了FPU/MMX/SSE或SSE2指令则保存相关硬件上下文 45 --->xsave() 46 --->arch/x86/include/asm/paravirt.h:load_sp0() 重新载入esp0 47 --->arch/x86/include/asm/paravirt.h:load_TLS() 加载线程的TLS段 48 --->__math_state_restore() 恢复FPU寄存器 49 --->kernel/sched.c:finish_task_switch() 完成切换后的清理工作 50 --->include/linux/sched.h:put_task_struct() 如果原来进程死亡(而不是运行超时) 51 需要释放它的PCB 52 --->kernel/fork.c:__put_task_struct() 53 --->free_task() 54 -->free_task_struct() 55 --->kmem_cache_free() 释放原来进程的PCB
下面是基本的执行流程图:
4、实时调度算法
linux内核中提供了两种实时调度策略:SCHED_FIFO和SCHED_RR,其中RR是带有时间片的FIFO。这两种调度算法实现的都是静态优先级。内核不为实时进程计算动态优先级。这能保证给定优先级别的实时进程总能抢占优先级比他低得进程。linux的实时调度算法提供了一种软实时工作方式。实时优先级范围从0到MAX_RT_PRIO减一。默认情况下,MAX_RT_PRIO为100(定义在include/linux/sched.h中),所以默认的实时优先级范围是从0到99。SCHED_NORMAL级进程的nice值共享了这个取值空间,它的取值范围是从MAX_RT_PRIO到MAX_RT_PRIO+40。也就是说,在默认情况下,nice值从-20到19直接对应的是从100到139的优先级范围,这就是普通进程的静态优先级范围。在实时调度策略下。schedule()函数的运行会关联到实时调度类rt_sched_class。
(1)数据结构
1)实时优先级队列rt_prio_array:在kernel/sched.c中,是一组链表,每个优先级对应一个链表。还维护一个由101 bit组成的bitmap,其中实时进程优先级为0-99,占100 bit,再加1 bit的定界符。当某个优先级别上有进程被插入列表时,相应的比特位就被置位。 通常用sched_find_first_bit()函数查询该bitmap,它返回当前被置位的最高优先级的数组下标。由于使用位图,查找一个任务来执行所需要的时间并不依赖于活动任务的个数,而是依赖于优先级的数量。可见实时调度是一个O(1)调度策略。
1 struct rt_prio_array {
2 DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1); /* 包含1 bit的定界符 */
3 struct list_head queue[MAX_RT_PRIO];
4 };
这里用include/linux/types.h中的DECLARE_BITMAP宏来定义指定长度的位图,用include/linux/list.h中的struct list_head来为100个优先级定义各自的双链表。在实时调度中,运行进程根据优先级放到对应的队列里面,对于相同的优先级的进程后面来的进程放到同一优先级队列的队尾。对于FIFO/RR调度,各自的进程需要设置相关的属性。进程运行时,要根据task中的这些属性判断和设置,放弃cpu的时机(运行完或是时间片用完)。
2)实时运行队列rt_rq:在kernel/sched.c中,用于组织实时调度的相关信息。
1 struct sched_rt_entity { 2 struct list_head run_list; 3 unsigned long timeout; 4 unsigned int time_slice; 5 int nr_cpus_allowed; 6 7 struct sched_rt_entity *back; 8 #ifdef CONFIG_RT_GROUP_SCHED 9 struct sched_rt_entity *parent; 10 /* rq on which this entity is (to be) queued: */ 11 struct rt_rq *rt_rq; 12 /* rq "owned" by this entity/group: */ 13 struct rt_rq *my_q; 14 #endif 15 }; 16 4)实时调度类rt_sched_class:在kernel/sched_rt.c中。 17 static const struct sched_class rt_sched_class = { 18 .next = &fair_sched_class, 19 .enqueue_task = enqueue_task_rt, 20 .dequeue_task = dequeue_task_rt, 21 .yield_task = yield_task_rt, 22 23 .check_preempt_curr = check_preempt_curr_rt, 24 25 .pick_next_task = pick_next_task_rt, 26 .put_prev_task = put_prev_task_rt, 27 28 #ifdef CONFIG_SMP 29 .select_task_rq = select_task_rq_rt, 30 31 .load_balance = load_balance_rt, 32 .move_one_task = move_one_task_rt, 33 .set_cpus_allowed = set_cpus_allowed_rt, 34 .rq_online = rq_online_rt, 35 .rq_offline = rq_offline_rt, 36 .pre_schedule = pre_schedule_rt, 37 .post_schedule = post_schedule_rt, 38 .task_woken = task_woken_rt, 39 .switched_from = switched_from_rt, 40 #endif 41 42 .set_curr_task = set_curr_task_rt, 43 .task_tick = task_tick_rt, 44 45 .get_rr_interval = get_rr_interval_rt, 46 47 .prio_changed = prio_changed_rt, 48 .switched_to = switched_to_rt, 49 };
(2)实时调度的主要操作:实时调度的操作在kernel/sched_rt.c中实现。
1)进程插入enqueue_task_rt:更新调度信息,调用enqueue_rt_entity()-->__enqueue_rt_entity(),将调度实体插入到相应优先级队列的末尾。如下:
1 static void 2 enqueue_task_rt(struct rq *rq, struct task_struct *p, int wakeup, bool head) 3 { 4 struct sched_rt_entity *rt_se = &p->rt; 5 6 if (wakeup) 7 rt_se->timeout = 0; 8 9 enqueue_rt_entity(rt_se, head); /* 实际工作 */ 10 11 if (!task_current(rq, p) && p->rt.nr_cpus_allowed > 1) 12 enqueue_pushable_task(rq, p); /* 添加到对应的hash表中 */ 13 } 14 15 static void enqueue_rt_entity(struct sched_rt_entity *rt_se, bool head) 16 { 17 dequeue_rt_stack(rt_se); /* 先从运行队列中删除 */ 18 for_each_sched_rt_entity(rt_se) 19 __enqueue_rt_entity(rt_se, head); /* 然后添加到运行队列尾部 */ 20 } 21 22 static void __enqueue_rt_entity(struct sched_rt_entity *rt_se, bool head) 23 { 24 struct rt_rq *rt_rq = rt_rq_of_se(rt_se); 25 struct rt_prio_array *array = &rt_rq->active; 26 struct rt_rq *group_rq = group_rt_rq(rt_se); 27 struct list_head *queue = array->queue + rt_se_prio(rt_se); 28 29 /* 30 * Don't enqueue the group if its throttled, or when empty. 31 * The latter is a consequence of the former when a child group 32 * get throttled and the current group doesn't have any other 33 * active members. 34 */ 35 if (group_rq && (rt_rq_throttled(group_rq) || !group_rq->rt_nr_running)) 36 return; 37 38 if (head) 39 list_add(&rt_se->run_list, queue); 40 else 41 list_add_tail(&rt_se->run_list, queue); 42 __set_bit(rt_se_prio(rt_se), array->bitmap); 43 44 inc_rt_tasks(rt_se, rt_rq); /* 运行进程数增一 */ 45 }
该函数先获取运行队列中的优先级队列,然后调用include/linux/list.h:list_add_tail()--->__list_add(),将进程插入到链表的末尾。如下:
1 static inline void __list_add(struct list_head *new, 2 struct list_head *prev, 3 struct list_head *next) 4 { 5 next->prev = new; 6 new->next = next; 7 new->prev = prev; 8 prev->next = new; 9 }
2)进程选择pick_next_task_rt:实时调度会选择最高优先级的实时进程来运行。调用_pick_next_task_rt()--->pick_next_rt_entity()来完成获取下一个进程的工作。如下:
1 static struct task_struct *pick_next_task_rt(struct rq *rq) 2 { 3 struct task_struct *p = _pick_next_task_rt(rq); /* 实际工作 */ 4 5 /* The running task is never eligible for pushing */ 6 if (p) 7 dequeue_pushable_task(rq, p); 8 9 #ifdef CONFIG_SMP 10 /* 11 * We detect this state here so that we can avoid taking the RQ 12 * lock again later if there is no need to push 13 */ 14 rq->post_schedule = has_pushable_tasks(rq); 15 #endif 16 17 return p; 18 } 19 20 static struct task_struct *_pick_next_task_rt(struct rq *rq) 21 { 22 struct sched_rt_entity *rt_se; 23 struct task_struct *p; 24 struct rt_rq *rt_rq; 25 26 rt_rq = &rq->rt; 27 28 if (unlikely(!rt_rq->rt_nr_running)) 29 return NULL; 30 31 if (rt_rq_throttled(rt_rq)) 32 return NULL; 33 34 do { /* 遍历组调度中的每个进程 */ 35 rt_se = pick_next_rt_entity(rq, rt_rq); 36 BUG_ON(!rt_se); 37 rt_rq = group_rt_rq(rt_se); 38 } while (rt_rq); 39 40 p = rt_task_of(rt_se); 41 /* 更新执行域 */ 42 p->se.exec_start = rq->clock_task; 43 44 return p; 45 } 46 47 static struct sched_rt_entity *pick_next_rt_entity(struct rq *rq, 48 struct rt_rq *rt_rq) 49 { 50 struct rt_prio_array *array = &rt_rq->active; 51 struct sched_rt_entity *next = NULL; 52 struct list_head *queue; 53 int idx; 54 /* 找到第一个可用的 */ 55 idx = sched_find_first_bit(array->bitmap); 56 BUG_ON(idx >= MAX_RT_PRIO); 57 /* 从链表组中找到对应的链表 */ 58 queue = array->queue + idx; 59 next = list_entry(queue->next, struct sched_rt_entity, run_list); 60 /* 返回找到的运行实体 */ 61 return next; 62 }
该函数调用include/asm-generic/bitops/sched.h:sched_find_first_bit()返回位图中当前被置位的最高优先级,以作为这组链表的数组下标找到其优先级队列。然后调用include/linux/list.h:list_entry()--->include/linux/kernel.h:container_of(),返回该优先级队列中的第一个进程,以作为下一个要运行的实时进程。例如当前所有实时进程中最高优先级为45(换句话说,系统中没有任何实时进程的优先级小于45),则直接读取rt_prio_array中的queue[45],得到优先级为45的进程队列指针。该队列头上的第一个进程就是被选中的进程。这种算法的复杂度为O(1)。
sched_find_first_bit的实现如下。它与CPU体系结构相关,其他体系结构会实现自己的sched_find_fist_bit函数。下面的实现以最快的方式搜索100 bit的位图,它能保证100 bit中至少有一位被清除。
1 static inline int sched_find_first_bit(const unsigned long *b) 2 { 3 #if BITS_PER_LONG == 64 4 if (b[0]) 5 return __ffs(b[0]); 6 return __ffs(b[1]) + 64; 7 #elif BITS_PER_LONG == 32 8 if (b[0]) 9 return __ffs(b[0]); 10 if (b[1]) 11 return __ffs(b[1]) + 32; 12 if (b[2]) 13 return __ffs(b[2]) + 64; 14 return __ffs(b[3]) + 96; 15 #else 16 #error BITS_PER_LONG not defined 17 #endif 18 }
3)进程删除dequeue_task_rt:从优先级队列中删除实时进程,并更新调度信息,然后把这个进程添加到队尾。调用链为dequeue_rt_entity()--->dequeue_rt_stack()--->__dequeue_rt_entity(),如下:
1 static void dequeue_task_rt(struct rq *rq, struct task_struct *p, int sleep) 2 { 3 struct sched_rt_entity *rt_se = &p->rt; 4 /* 更新调度信息 */ 5 update_curr_rt(rq); 6 /* 实际工作,将rt_se从运行队列中删除然后 7 添加到队列尾部 */ 8 dequeue_rt_entity(rt_se); 9 /* 从hash表中删除 */ 10 dequeue_pushable_task(rq, p); 11 } 12 13 static void update_curr_rt(struct rq *rq) 14 { 15 struct task_struct *curr = rq->curr; 16 struct sched_rt_entity *rt_se = &curr->rt; 17 struct rt_rq *rt_rq = rt_rq_of_se(rt_se); 18 u64 delta_exec; 19 20 if (!task_has_rt_policy(curr)) /* 判断是否问实时调度进程 */ 21 return; 22 /* 执行时间 */ 23 delta_exec = rq->clock_task - curr->se.exec_start; 24 if (unlikely((s64)delta_exec < 0)) 25 delta_exec = 0; 26 27 schedstat_set(curr->se.exec_max, max(curr->se.exec_max, delta_exec)); 28 /* 更新当前进程的总的执行时间 */ 29 curr->se.sum_exec_runtime += delta_exec; 30 account_group_exec_runtime(curr, delta_exec); 31 /* 更新执行的开始时间 */ 32 curr->se.exec_start = rq->clock_task; 33 cpuacct_charge(curr, delta_exec); /* 组调度相关 */ 34 35 sched_rt_avg_update(rq, delta_exec); 36 37 if (!rt_bandwidth_enabled()) 38 return; 39 40 for_each_sched_rt_entity(rt_se) { 41 rt_rq = rt_rq_of_se(rt_se); 42 43 if (sched_rt_runtime(rt_rq) != RUNTIME_INF) { 44 spin_lock(&rt_rq->rt_runtime_lock); 45 rt_rq->rt_time += delta_exec; 46 if (sched_rt_runtime_exceeded(rt_rq)) 47 resched_task(curr); 48 spin_unlock(&rt_rq->rt_runtime_lock); 49 } 50 } 51 } 52 53 static void dequeue_rt_entity(struct sched_rt_entity *rt_se) 54 { 55 dequeue_rt_stack(rt_se); /* 从运行队列中删除 */ 56 57 for_each_sched_rt_entity(rt_se) { 58 struct rt_rq *rt_rq = group_rt_rq(rt_se); 59 60 if (rt_rq && rt_rq->rt_nr_running) 61 __enqueue_rt_entity(rt_se, false); /* 添加到队尾 */ 62 } 63 } 64 65 static void dequeue_rt_stack(struct sched_rt_entity *rt_se) 66 { 67 struct sched_rt_entity *back = NULL; 68 69 for_each_sched_rt_entity(rt_se) { /* 遍历整个组调度实体 */ 70 rt_se->back = back; /* 可见rt_se的back实体为组调度中前一个调度实体 */ 71 back = rt_se; 72 } 73 /* 将组中的所有进程从运行队列中移除 */ 74 for (rt_se = back; rt_se; rt_se = rt_se->back) { 75 if (on_rt_rq(rt_se)) 76 __dequeue_rt_entity(rt_se); 77 } 78 } 79 80 static void __dequeue_rt_entity(struct sched_rt_entity *rt_se) 81 { 82 struct rt_rq *rt_rq = rt_rq_of_se(rt_se); 83 struct rt_prio_array *array = &rt_rq->active; 84 /* 移除进程 */ 85 list_del_init(&rt_se->run_list); 86 /* 如果链表变为空,则将位图中对应的bit位清零 */ 87 if (list_empty(array->queue + rt_se_prio(rt_se))) 88 __clear_bit(rt_se_prio(rt_se), array->bitmap); 89 90 dec_rt_tasks(rt_se, rt_rq); /* 运行进程计数减一 */ 91 }
可见更新调度信息的函数为update_curr_rt(),在dequeue_rt_entity()中将当前实时进程从运行队列中移除,并添加到队尾。完成工作函数为dequeue_rt_stack()--->__dequeue_rt_entity(),它调用include/linux/list.h:list_del_init()--->__list_del()删除进程。然后如果链表变为空,则将位图中对应优先级的bit位清零。如下:
1 static inline void __list_del(struct list_head * prev, struct list_head * next)
2 {
3 next->prev = prev;
4 prev->next = next;
5 }
从上面的介绍可以看出,对于实时调度,Linux的实现比较简单,仍然采用之前的O(1)调度策略,把所有的运行进程根据优先级放到不用的队列里面,采用位图方式进行使用记录。进队列仅仅是删除原来队列里面的本进程,然后将他挂到队列尾部;而对于“移除”操作,也仅仅是从队列里面移除后添加到运行队列尾部。
5、Linux运行时调优和调试选项
Linux引入了重要的sysctls来在运行时对调度程序进行调优(以ns结尾的名称以纳秒为单位):
sched_child_runs_first:child在fork之后进行调度;此为默认设置。如果设置为 0,那么先调度parent。
sched_min_granularity_ns:针对CPU密集型任务执行最低级别抢占粒度。
sched_latency_ns:针对CPU密集型任务进行目标抢占延迟(Targeted preemption latency)。
sched_wakeup_granularity_ns:针对SCHED_OTHER的唤醒粒度。
sched_batch_wakeup_granularity_ns:针对SCHED_BATCH的唤醒(Wake-up)粒度。
sched_features:包含各种与调试相关的特性的信息。
sched_stat_granularity_ns:收集调度程序统计信息的粒度。
sched_compat_yield:由于CFS进行了改动,严重依赖sched_yield()的行为的应用程序可以要求不同的性能,因此推荐启用sysctls。
下面是系统中运行时参数的典型值:
jackzhou@ubuntu:~/linux$ sudo sysctl -A | grep "sched" | grep -v "domain"
kernel.sched_child_runs_first = 0
kernel.sched_min_granularity_ns = 2000000
kernel.sched_latency_ns = 10000000
kernel.sched_wakeup_granularity_ns = 2000000
kernel.sched_shares_ratelimit = 500000
kernel.sched_shares_thresh = 4
kernel.sched_features = 32611451
kernel.sched_migration_cost = 500000
kernel.sched_nr_migrate = 32
kernel.sched_time_avg = 1000
kernel.sched_rt_period_us = 1000000
kernel.sched_rt_runtime_us = 950000
kernel.sched_compat_yield = 0
Linux调度程序附带了一个非常棒的调试接口,还提供了运行时统计信息,分别在 kernel/sched_debug.c 和 kernel/sched_stats.h 中实现。要提供调度程序的运行时信息和调试信息,需要将一些文件添加到proc文件系统:
/proc/sched_debug:显示运行时调度程序可调优选项的当前值、CFS 统计信息和所有可用 CPU 的运行队列信息。当读取这个 proc 文件时,将调用sched_debug_show(),这个函数在 sched_debug.c 中定义。
/proc/schedstat:为所有相关的 CPU 显示特定于运行队列的统计信息以及 SMP 系统中特定于域的统计信息。kernel/sched_stats.h 中定义的 show_schedstat() 函数将处理 proc 条目中的读操作。
/proc/[PID]/sched:显示与相关调度实体有关的信息。在读取这个文件时,将调用 kernel/sched_debug.c 中定义的 proc_sched_show_task() 函数。