一、简介
idle调度类实现文件是idle.c,最低优先级。每个cpu绑定一个idle线程到 iq->idle 上。
二、相关结构
1. sched_class idle_sched_class
const struct sched_class idle_sched_class __section("__idle_sched_class") = { /* no enqueue/yield_task for idle tasks */ /* * dequeue is not valid, we print a debug message there: * 不应dequeue idle线程,若是调用dequeue了,直接dump_stack() */ .dequeue_task = dequeue_task_idle, /* 只是执行 resched_curr(rq); */ .check_preempt_curr = check_preempt_curr_idle, /* 直接返回 rq->idle */ .pick_next_task = pick_next_task_idle, /* 空函数 */ .put_prev_task = put_prev_task_idle, /* 只是增加 rq->sched_goidle 计数 */ .set_next_task = set_next_task_idle, #ifdef CONFIG_SMP /* 只是 WARN_ON_ONCE(1) */ .balance = balance_idle, /* 选核,直接返回task_cpu(p),IDLE tasks as never migrated */ .select_task_rq = select_task_rq_idle, /* 设置idle线程的p->cpus_mask竟然是支持的 */ .set_cpus_allowed = set_cpus_allowed_common, #endif /* 空函数 */ .task_tick = task_tick_idle, /* 直接是执行 BUG(); */ .prio_changed = prio_changed_idle, /* 直接是执行 BUG(); 执行路径只有 rt_mutex_setprio/__sched_setscheduler */ .switched_to = switched_to_idle, /* 空函数 */ .update_curr = update_curr_idle, };
2. idle_threads
static DEFINE_PER_CPU(struct task_struct *, idle_threads); //smpboot.c
里面保存了各个cpu上的idle任务,静态的,且相关函数没有EXPORT_SYMBOL导出,相当于一个模块内部的变量。
三、实现概述
1. rq->idle 任务初始化
在 init_idle() 中进行初始化
/** * init_idle - set up an idle thread for a given CPU * @idle: task in question * @cpu: CPU the idle task belongs to * * 注意:此函数不会设置空闲线程的 NEED_RESCHED 标志,以使启动更加稳健。 */ void __init init_idle(struct task_struct *idle, int cpu) { struct rq *rq = cpu_rq(cpu); unsigned long flags; __sched_fork(0, idle); raw_spin_lock_irqsave(&idle->pi_lock, flags); raw_spin_lock(&rq->lock); idle->state = TASK_RUNNING; idle->se.exec_start = sched_clock(); /* idle线程特有标识 */ idle->flags |= PF_IDLE; /* 设置此idle线程只能运行在这一个cpu上 */ set_cpus_allowed_common(idle, cpumask_of(cpu)); __set_task_cpu(idle, cpu); /* 将参数idle task设置为rq的idle线程 */ rq->idle = idle; rcu_assign_pointer(rq->curr, idle); idle->on_rq = TASK_ON_RQ_QUEUED; idle->on_cpu = 1; /* 初始化为不可被抢占 */ init_idle_preempt_count(idle, cpu); /* idle调度类 */ idle->sched_class = &idle_sched_class; /* idle线程的名字是固定的,都是"swapper/X",但是"ps -AT | grep swapper"是不会显示的 */ sprintf(idle->comm, "%s/%d", INIT_TASK_COMM, cpu); }
init_idle()的调用路径:
start_kernel //init/main.c sched_init //sched/core.c init_idle(current, smp_processor_id()); //sched/core.c 只有boot cpu执行,此时PID还是0 idle_thread_set_boot_cpu(); //per_cpu(idle_threads, smp_processor_id()) = current; arch_call_rest_init //init/main.c 在 sched_init 之后,函数末尾位置才调用 rest_init //init/main.c kernel_thread(kernel_init, NULL, CLONE_FS) 首先生成 init 以便它获得 pid 1 kernel_init //init/main.c 此时PID就是1了 kernel_init_freeable //init/main.c smp_init //kernel/smp.c idle_threads_init //smpboot.c for_each_possible_cpu(cpu) if (cpu != boot_cpu) 对于非boot的每个CPU都执行 idle_init //smpboot.c fork_idle //fork.c init_idle(task, cpu); //fork.c per_cpu(idle_threads, cpu) = tsk; //将fork出来的idle线程赋值给per-cpu的idle_threads指针
可见,idle线程的名字为"swapper/X",只绑定了一个CPU,相当于是per-cpu的idle线程。idle线程用 PF_IDLE 进行标识,由rq->idle成员直接指向。
2. idle线程运行内容
(1) boot cpu 执行初始化流程后
/* start_kernel arch_call_rest_init rest_init cpu_startup_entry(CPUHP_ONLINE); */ void cpu_startup_entry(enum cpuhp_state state) { arch_cpu_idle_prepare(); //空函数 cpuhp_online_idle(state); /* 进入idle状态 */ while (1) do_idle(); }
do_idle的实现:
static void do_idle(void) { int cpu = smp_processor_id(); tick_nohz_idle_enter(); /* 若不需要调度,就在这死循环 */ while (!need_resched()) { rmb(); local_irq_disable(); if (cpu_is_offline(cpu)) { tick_nohz_idle_stop_tick(); cpuhp_report_idle_dead(); arch_cpu_idle_dead(); } /* 这里有执行wakeup操作,有 trace_rcu_nocb_wake 可以跟踪 */ rcu_nocb_flush_deferred_wakeup(); /* 判断是否要进入更深层的休眠 */ if (cpu_idle_force_poll || tick_check_broadcast_expired()) { tick_nohz_idle_restart_tick(); /* 若是从这里退出有 trace_cpu_idle 可以跟踪 */ cpu_idle_poll(); } else { /* 进入更深层的idle */ cpuidle_idle_call(); } arch_cpu_idle_exit(); } preempt_set_need_resched(); tick_nohz_idle_exit(); __current_clr_polling(); smp_mb__after_atomic(); /* 会处理其它ipi中断发过来的请求和触发软中断 */ flush_smp_call_function_from_idle(); /* 调用 __schedule(false) 触发任务切换 */ schedule_idle(); }
pid=0的内核boot线程最终归宿是idle线程,也就是boot cpu的idle线程的pid=0。
(2) non-boot cpu
是在 pid=1 的 kernel_init() 的执行路径中通过 fork_idle(int cpu) 依次为每一个非boot cpu创建的
struct task_struct * __init fork_idle(int cpu) { struct task_struct *task; struct kernel_clone_args args = { .flags = CLONE_VM, }; task = copy_process(&init_struct_pid, 0, cpu_to_node(cpu), &args); if (!IS_ERR(task)) { init_idle_pids(task); init_idle(task, cpu); } return task; }
copy_process 函数参数1传参是全局的 struct pid init_struct_pid,其 .numbers.nr 是初始化为0的。在 copy_process 中对传参 arg1=init_struct_pid 的新创建的任务的 pid 赋值为 p->pid = pid_nr(pid) 也即是 numbers.nr,也就是0。也就是说非boot cpu的idle线程的pid也是0。
TODO: fork_idle 得到的idle线程执行的代码是什么呢? 是如何执行到的?
3. 选 idle 线程时机
static inline struct task_struct *pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf) { const struct sched_class *class; struct task_struct *p; /* 针对CFS的优化选核 */ if (likely(prev->sched_class <= &fair_sched_class && rq->nr_running == rq->cfs.h_nr_running)) { p = pick_next_task_fair(rq, prev, rf); if (unlikely(p == RETRY_TASK)) goto restart; /* Assumes fair_sched_class->next == idle_sched_class */ if (!p) { put_prev_task(rq, prev); /* 选不到任务就让idle任务运行 */ p = pick_next_task_idle(rq); } return p; } restart: /* 这里面有 put_prev_task,虽然CFS选核中有put,但是上面没有执行 */ put_prev_task_balance(rq, prev, rf); for_each_class(class) { /* 这里按调度类优先级从高到底遍历,若选不到高优先级任务,就选中最低优先级的idle线程来运行 */ p = class->pick_next_task(rq); if (p) return p; } /* The idle class should always have a runnable task: */ BUG(); }
调度类是通过链接脚本的方式排列在一起,低优先级调度类低地址,高优先级调度类高地址。for_each_class 进行遍历的时候,先遍历高地址,再遍历低地址。
//include/asm-generic/vmlinux.lds.h #define SCHED_DATA \ STRUCT_ALIGN(); \ __begin_sched_classes = .; \ *(__idle_sched_class) \ *(__fair_sched_class) \ *(__rt_sched_class) \ *(__dl_sched_class) \ *(__stop_sched_class) \ __end_sched_classes = .; /* defined in include/asm-generic/vmlinux.lds.h */ extern struct sched_class __begin_sched_classes[]; extern struct sched_class __end_sched_classes[]; #define sched_class_highest (__end_sched_classes - 1) #define sched_class_lowest (__begin_sched_classes - 1) #define for_class_range(class, _from, _to) \ for (class = (_from); class != (_to); class--) #define for_each_class(class) \ for_class_range(class, sched_class_highest, sched_class_lowest)
四、相关实验
1. idle线程唤醒任务
let i=0; while true; do if [ i -lt 10 ]; then let i=i+1; else let i=0; sleep 0.1; fi; done & [1] 8241
使用这个脚本验证,运行后pid=8241,从trace上看,sh[8241] 一直是被 sleep [XX] 唤醒,而 sleep [XX] 是被idle线程 swapper [0] 唤醒。sleep线程写为pid=XX是为了表示 sleep 线程其PID一直在变,看起来是每次都是新建sleep进程后进行唤醒的。
2. 内核中执行 msleep() 函数后的唤醒,trace上看,有时是被idle线程唤醒的。用户空间执行usleep()休眠的线程,从trace上看也有一部分是被idle线程唤醒的,而且在唤醒CPU上此时并没有中断或软中断!
五、经验总结
1. 在 init_idle() 函数中将 per-cpu 的 idle 线程的名字固定为"swapper/X"。用 PF_IDLE 标识idle线程。per-cpu的idle线程由各cpu的rq->idle成员直接指向。所有cpu上的idle线程的pid都等于0,且使用"ps -AT"查看不到idle线程。
2. 内核初始化线程最终会成为boot cpu上的idle线程,并且在idle线程中会执行一些唤醒操作。
3. trace上idle线程显示为"<idle>-0".