Linux sched_idle空闲调度类与idle进程周期
Linux sched_idle空闲调度类与idle进程周期idle_sched_class是Linux内核优先级最低的调度类位于stop_sched_class之后、完全公平调度类之前。其唯一任务per-CPU的idle线程pid0在runqueue无其他可运行任务时被pick_next_task_idle选中。idle线程的prio为MAX_PRIO140在prio chaining中永远不会与正常任务竞争——它的存在完全是为了让CPU在无事可做时执行WFI/HLT指令降低功耗。cDEFINE_SCHED_CLASS(idle) {.enqueue_task enqueue_task_idle,.dequeue_task dequeue_task_idle,.yield_task yield_task_idle,.check_preempt_curr check_preempt_curr_idle,.pick_next_task pick_next_task_idle,.put_prev_task put_prev_task_idle,#ifdef CONFIG_SMP.select_task_rq select_task_rq_idle,.set_cpus_allowed set_cpus_allowed_idle,#endif.task_tick task_tick_idle,.priority_class 1, /* 倒数第二低仅高于stop_class */};pick_next_task_idle()直接返回rq-idle——即当前CPU的idle线程task_struct。该线程在sched_init()阶段通过init_idle()初始化其thread_struct.sp直接指向当前CPU的内核栈底部thread_info.preempt_count设置为PREEMPT_ACTIVE防止idle状态被抢占。cstatic struct task_struct *pick_next_task_idle(struct rq *rq){rq-idle_balance 0; /* 清零idle线程不参与load balance */return rq-idle;}void init_idle(struct task_struct *idle, int cpu){struct rq *rq cpu_rq(cpu);__sched_fork(0, idle);raw_spin_lock_irqsave(rq-lock, flags);idle-__state TASK_RUNNING;idle-se.exec_start sched_clock();idle-flags | PF_IDLE;kasan_unpoison_task_stack(idle);/** idle线程的调度实体不加入任何cfs_rq或rt_rq* 但preempt_count必须设为PREEMPT_ACTIVE以阻止* schedule()在idle线程内部再次被调用*/#ifdef CONFIG_PREEMPTidle-thread_info.preempt_count PREEMPT_ACTIVE;#endifrq-idle idle;rq-curr idle;raw_spin_unlock_irqrestore(rq-lock, flags);}idle线程的主循环位于cpu_startup_entry()中由boot CPU在rest_init()中启动从属CPU在secondary_startup_64中跳转。该函数调用do_idle()其内部是一个无限循环调用cpuidle_select选择C-state、调用cpuidle_enter进入、退出后检查TIF_NEED_RESCHED。cstatic void do_idle(void){int cpu smp_processor_id();/** 检查是否有pending的TTWU queue* 如果有其他CPU已经将task加入当前CPU的runqueue* 则不应进入idle*/if (ttwu_pending())return;/* 进入cpuidle框架选择最深C-state */cpuidle_select(drv, dev, stop_tick);/* tick停止处理——NO_HZ路径 */tick_nohz_idle_enter();while (!need_resched()) {/* 进入实际idle状态 */cpuidle_enter(drv, dev, next_state);/* 退出后检查是否需要重新计算C-state */if (cpuidle_need_update(cpu))cpuidle_reflect(dev, next_state);}tick_nohz_idle_exit();/** 退出idle后立即调用schedule_preempt_disabled()* 将当前线程从rq-idle切回真正的任务*/sched_preempt_enable_no_resched();schedule_preempt_disabled();}tick_nohz_idle_enter()是idle周期中的关键路径决定当前CPU是否停止周期性tick。若整个系统没有足够的周期性工作负载即只有当前task_struct和timer列表可以延期tick_nohz_stop_tick()将dev-tick_stopped置1。NO_HZ全停止后cpu必须依靠外部中断网卡、timer_alarm来唤醒——如果后续没有外部中断到达cpu会无限期停留在idle状态。这也是为什么rcu_needs_cpu()必须返回true来阻止tick停止否则RCU callbacks会因饥饿而触发RCU stall warning。cselect: bool tick_nohz_stop_tick(struct tick_sched *ts, int cpu){struct clock_event_device *dev __this_cpu_read(tick_cpu_device.evtdev);unsigned long base_jiffies;u64 expires;/* 计算下一个定时器到期时间 */expires tick_nohz_next_event(ts, cpu);/* 如果expires离当前距离太近 1 tick不停止tick */if (expires - basemono TICK_NSEC)return false;/* 检查RCU是否需要这个CPU */if (!rcu_needs_cpu() cpu_online(cpu)) {ts-tick_stopped 1;ts-idle_jiffies base_jiffies;dev-next_event KTIME_MAX;return true;}return false;}SCHED_IDLE优先级策略通过SCHED_IDLE调度策略设置非idle调度类与idle_sched_class有本质区别。SCHED_IDLE策略的任务仍然属于CFS调度类但其weight通过task_struct-se.load.weight 3普通任务1024被压缩到极低。这意味着SCHED_IDLE任务的vruntime增长极快在CFS红黑树中几乎总是被推到最右端只在所有其他CFS任务都阻塞时才会被选中。而idle_sched_class是独立调度类所有非idle调度类都无法从runqueue中找到任务时才会轮询到idle类。cstatic void set_load_weight(struct task_struct *p, bool update_load){int prio p-static_prio - MAX_RT_PRIO;struct load_weight *load p-se.load;if (idle_policy(p-policy)) {/* SCHED_IDLE策略weight设为最小 */load-weight scale_load(WEIGHT_IDLEPRIO);load-inv_weight WMULT_IDLEPRIO;return;}load-weight scale_load(sched_prio_to_weight[prio]);load-inv_weight sched_prio_to_wmult[prio];}idle进程的调度决策本身不会通过scheduler_tick触发因为task_tick_idle()实现为空函数。但是scheduler_tick在rq-curr rq-idle时仍会执行update_rq_clock和calc_global_load_tick——这意味着即使CPU空闲全局tick负载计算仍在进行。这部分开销在NO_HZ_FULL模式下被避免当单个任务独占CPU时tick_stopped后calc_load_nohz_start/stop管理全局负载汇总。一个边界case是nohz_full隔离CPU上的idle行为。当isolcpus或nohz_full将CPU从通用调度域排除时该CPU的idle线程在do_idle()循环中不受scheduler_tick打扰但必须处理per-CPU的arch_timer中断。如果wakeup事件发生在其他CPU上通过llist的TTWU queue提交给目标CPU时目标CPU必须通过arch_send_call_function_single_ipi()唤醒——但如果目标CPU处于mwait cstate超过1的深度睡眠IPI可能无法及时到达。Intel的mwait_monitor/hint机制通过MONITOR/MWAIT指令对address monitoring和store detection来避免这个问题idle线程在进入deep C-state前用MONITOR监视runqueue的__ttwu_pending标志所在地址当其他CPU写入该标志时硬件自动唤醒。cstatic inline void mwait_idle_with_hints(unsigned long ax, unsigned long cx){if (!current_set_polling_and_test()) {if (this_cpu_has(X86_FEATURE_CLFLUSH_MONITOR)) {mb();__monitor((void *)¤t_thread_info()-flags, 0, 0);if (!need_resched())__mwait(ax, cx);}}current_clr_polling();}最后idle线程的退出路径存在一个条件竞态do_idle()中检查need_resched()后调用schedule_preempt_disabled()但此时若有IRQ在检查窗口和schedule之间触发并set_tsk_need_resched两次need_resched都不会丢失。但在CONFIG_PREEMPT_NONE下schedule()的入口preempt_disable_notrace是barrier()无法阻止IRQ在中断返回时再次调用schedule()——从而产生__schedule()重入。为此schedule()入口通过schedule_debug(prev)校验in_sched_functions()来捕获并panic重入。