Linux sched_idle空闲调度类与idle进程周期

Linux sched_idle空闲调度类与idle进程周期

Linux sched_idle空闲调度类与idle进程周期

idle_sched_class是Linux内核优先级最低的调度类,位于stop_sched_class之后、完全公平调度类之前。其唯一任务(per-CPU的idle线程,pid=0)在runqueue无其他可运行任务时被pick_next_task_idle选中。idle线程的prio为MAX_PRIO(140),在prio chaining中永远不会与正常任务竞争——它的存在完全是为了让CPU在无事可做时执行WFI/HLT指令降低功耗。

```c
DEFINE_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状态被抢占。

```c
static 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_PREEMPT
idle->thread_info.preempt_count = PREEMPT_ACTIVE;
#endif

rq->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。

```c
static 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。

```c
select: 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类。

```c
static 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写入该标志时硬件自动唤醒。

```c
static 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重入。