〇、全景:调度器的启动,是"四样东西分阶段就位"
一个 CPU 要真正参与调度,不是"某个开关一拨"就完成的,而是有四样东西要先后就位:
- 调度类(
sched_class)------决定"用什么策略调度"(CFS 公平 / RT 实时 / DL 截止期...); - runqueue(
rq)------决定"每个 CPU 的任务在哪排队"(per-CPU 的队列); - idle 线程------决定"这个 CPU 没活干时跑什么"(兜底线程);
- 调度域(
sched domain)------决定"负载怎么跨 CPU 迁移"(多核协同)。
这四样东西,内核是分三个阶段点亮的:
#mermaid-svg-CLek2k2UKWGMrqnC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CLek2k2UKWGMrqnC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CLek2k2UKWGMrqnC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CLek2k2UKWGMrqnC .error-icon{fill:#552222;}#mermaid-svg-CLek2k2UKWGMrqnC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CLek2k2UKWGMrqnC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CLek2k2UKWGMrqnC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CLek2k2UKWGMrqnC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CLek2k2UKWGMrqnC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CLek2k2UKWGMrqnC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CLek2k2UKWGMrqnC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CLek2k2UKWGMrqnC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CLek2k2UKWGMrqnC .marker.cross{stroke:#333333;}#mermaid-svg-CLek2k2UKWGMrqnC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CLek2k2UKWGMrqnC p{margin:0;}#mermaid-svg-CLek2k2UKWGMrqnC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-CLek2k2UKWGMrqnC .cluster-label text{fill:#333;}#mermaid-svg-CLek2k2UKWGMrqnC .cluster-label span{color:#333;}#mermaid-svg-CLek2k2UKWGMrqnC .cluster-label span p{background-color:transparent;}#mermaid-svg-CLek2k2UKWGMrqnC .label text,#mermaid-svg-CLek2k2UKWGMrqnC span{fill:#333;color:#333;}#mermaid-svg-CLek2k2UKWGMrqnC .node rect,#mermaid-svg-CLek2k2UKWGMrqnC .node circle,#mermaid-svg-CLek2k2UKWGMrqnC .node ellipse,#mermaid-svg-CLek2k2UKWGMrqnC .node polygon,#mermaid-svg-CLek2k2UKWGMrqnC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CLek2k2UKWGMrqnC .rough-node .label text,#mermaid-svg-CLek2k2UKWGMrqnC .node .label text,#mermaid-svg-CLek2k2UKWGMrqnC .image-shape .label,#mermaid-svg-CLek2k2UKWGMrqnC .icon-shape .label{text-anchor:middle;}#mermaid-svg-CLek2k2UKWGMrqnC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-CLek2k2UKWGMrqnC .rough-node .label,#mermaid-svg-CLek2k2UKWGMrqnC .node .label,#mermaid-svg-CLek2k2UKWGMrqnC .image-shape .label,#mermaid-svg-CLek2k2UKWGMrqnC .icon-shape .label{text-align:center;}#mermaid-svg-CLek2k2UKWGMrqnC .node.clickable{cursor:pointer;}#mermaid-svg-CLek2k2UKWGMrqnC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-CLek2k2UKWGMrqnC .arrowheadPath{fill:#333333;}#mermaid-svg-CLek2k2UKWGMrqnC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-CLek2k2UKWGMrqnC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-CLek2k2UKWGMrqnC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CLek2k2UKWGMrqnC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-CLek2k2UKWGMrqnC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CLek2k2UKWGMrqnC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-CLek2k2UKWGMrqnC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-CLek2k2UKWGMrqnC .cluster text{fill:#333;}#mermaid-svg-CLek2k2UKWGMrqnC .cluster span{color:#333;}#mermaid-svg-CLek2k2UKWGMrqnC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-CLek2k2UKWGMrqnC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CLek2k2UKWGMrqnC rect.text{fill:none;stroke-width:0;}#mermaid-svg-CLek2k2UKWGMrqnC .icon-shape,#mermaid-svg-CLek2k2UKWGMrqnC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CLek2k2UKWGMrqnC .icon-shape p,#mermaid-svg-CLek2k2UKWGMrqnC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-CLek2k2UKWGMrqnC .icon-shape .label rect,#mermaid-svg-CLek2k2UKWGMrqnC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CLek2k2UKWGMrqnC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-CLek2k2UKWGMrqnC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-CLek2k2UKWGMrqnC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 阶段一:sched_init(单核)
start_kernel 里,只有 BSP 在跑
阶段二:AP 拉起(smp_init)
为每个 AP 建 idle 线程
阶段三:sched_init_smp(SMP)
构建调度域,多核协同
调度类已编译期就位
-
初始化所有 rq
-
BSP 的 idle 线程
idle_threads_init
fork_idle 每个 AP 的 idle
sched_init_domains
把 CPU 组织成拓扑
一句话主线:调度器启动 = 四样东西(调度类 / rq / idle 线程 / 调度域)分三阶段就位 ------
sched_init在单核阶段把 BSP 点亮,smp_init拉起 AP 时给每个 AP 建 idle 线程,sched_init_smp最后构建调度域让多核协同起来。这篇讲的,就是这四样东西各自"什么时候、怎么"就位。
一、调度类:编译期就位的"策略骨架"
调度器的第一步,其实是编译期就完成的------五个调度类(scheduling class)在链接时就已经按优先级排好了队。
1.1 五个调度类与优先级
内核有五个调度类,优先级从高到低:stop > deadline > rt > fair > idle 。每个 CPU 的 rq 上,调度器总是先挑高优先级调度类里的任务跑,高优先级的没任务了才轮到低优先级。
| 调度类 | 优先级 | 管什么 |
|---|---|---|
stop_sched_class |
最高 | 抢占一切、不被抢占(stop_machine 用:CPU offline、kexec 等需停掉其他 CPU 的操作) |
dl_sched_class |
次高 | 实时截止期任务(SCHED_DEADLINE) |
rt_sched_class |
中 | 实时任务(SCHED_FIFO / SCHED_RR) |
fair_sched_class |
次低 | 普通任务(CFS,SCHED_NORMAL) |
idle_sched_class |
最低 | 兜底(永远有任务可挑) |
1.2 静态注册:DEFINE_SCHED_CLASS + 链接脚本排序
关键点是:调度类不是运行时"注册"进去的,而是编译期静态定义的 。每个调度类用宏 DEFINE_SCHED_CLASS 定义,把它放进一个独立的段(section):
c
// kernel/sched/sched.h (v6.6, line 2317)
#define DEFINE_SCHED_CLASS(name) \
const struct sched_class name##_sched_class \
__aligned(__alignof__(struct sched_class)) \
__section("__" #name "_sched_class")
五个调度类各调一次(stop_task.c:117、deadline.c:2718、rt.c:2699、fair.c:12904、idle.c:478),得到五个段:__stop_sched_class、__dl_sched_class、...、__idle_sched_class。
然后链接脚本把这五个段按优先级顺序排在一起:
c
// include/asm-generic/vmlinux.lds.h (v6.6, line 129)
__sched_class_highest = .; \
*(__stop_sched_class) \
*(__dl_sched_class) \
*(__rt_sched_class) \
*(__fair_sched_class) \
*(__idle_sched_class) \
__sched_class_lowest = .;
于是内核里就有了一段地址连续、按优先级降序排布 的 struct sched_class 数组。遍历调度类就是简单地从这个数组头走到尾:
c
// kernel/sched/sched.h (v6.6, line 2329)
#define for_each_class(class) \
for_class_range(class, __sched_class_highest, __sched_class_lowest)
__sched_class_highest 指向 stop_sched_class(地址最低、优先级最高),__sched_class_lowest 是 idle_sched_class 之后。sched_init 开头甚至用 BUG_ON 校验这个排布没被链接器搞坏:
c
// kernel/sched/core.c (v6.6, line 9915)
BUG_ON(&idle_sched_class != &fair_sched_class + 1 ||
&fair_sched_class != &rt_sched_class + 1 ||
&rt_sched_class != &dl_sched_class + 1);
1.3 为什么用链接脚本,而不是运行时注册
这和 hotplug 状态机(#3 的 cpuhp_setup_state 动态注册)恰恰相反,是个值得记住的设计选择:
- 性能 :调度是内核最热的热路径。
for_each_class直接在连续内存里线性扫描,无锁、无链表遍历、无注册表查找。如果运行时注册,每次挑任务都要多一层间接访问。 - 优先级固定:五个调度类的优先级是内核设计的常量,从不会变。既然是编译期就确定的顺序,就没必要付运行时的代价去维护一个可变结构。
一句话:调度类的"注册"在链接那一刻就结束了,sched_init 里没有任何"注册调度类"的动作------它只校验了一下链接结果没坏。
二、sched_init:单核点亮
真正"初始化调度器"的函数是 sched_init,它在 start_kernel 里很早被调用(此时只有 BSP 一个 CPU 在跑,很多东西还没就位):
c
// init/main.c (v6.6, line 940)
sched_init();
2.1 做什么:初始化 per-CPU 的 rq + root_task_group
sched_init(core.c:9909)的核心是给所有 possible CPU 初始化 runqueue 数据结构:
c
// kernel/sched/core.c (v6.6, line 9973)
for_each_possible_cpu(i) {
struct rq *rq = cpu_rq(i);
raw_spin_lock_init(&rq->__lock);
rq->nr_running = 0;
init_cfs_rq(&rq->cfs);
init_rt_rq(&rq->rt);
init_dl_rq(&rq->dl);
// ...
#ifdef CONFIG_SMP
rq->sd = NULL; // 调度域还没建,等 sched_init_smp
rq->rd = NULL;
rq->online = 0; // 还没上线
// ...
#endif
}
注意几个细节:
for_each_possible_cpu遍历的是可能的 CPU(可能比实际在线的多),所以现在只是把数据结构"铺好",CPU 并没上线(rq->online = 0)。rq->sd = NULL、rq->rd = NULL:调度域此时还是空的。这意味着现阶段调度器只能"单核自转",跨核的负载均衡完全没建立。
此外还会初始化 root_task_group(所有任务默认所属的 task group,group scheduling 的根)。
2.2 把 BSP 变成 idle 线程:init_idle(current, 0)
sched_init 最关键的一步,是把当前正在跑的 init_task(BSP 的内核第一个任务)直接变成 CPU0 的 idle 线程:
c
// kernel/sched/core.c (v6.6, line 10081)
init_idle(current, smp_processor_id());
这里有个和 AP 截然不同的点:BSP 不需要 fork 一个 idle 线程 。因为 start_kernel 本来就是 init_task 在跑------它是现成的、已经在线的内核任务,直接"转岗"当 idle 最省事。sched_init 末尾还把这个事实登记进 per-CPU 变量:
c
// kernel/sched/core.c (v6.6, line 10086)
idle_thread_set_boot_cpu(); // 把 current 存进 per_cpu(idle_threads, 0)
最后 scheduler_running = 1(core.c:10097),标志调度器进入可运行状态。
三、idle 线程:每个 CPU 的"没活干时跑什么"
idle 线程是"调度器启动"里最容易被忽略、但其实最关键的一环。它回答了一个边界问题:当一个 CPU 的 rq 上没有任何可运行任务时,rq->curr 该指向谁? 答案必须是一个"永远存在、永远可运行"的兜底任务,否则调度器挑任务时会踩空。
init_idle(core.c:9250)就是干这个的,它把某个 task 挂到 rq 上、标记为 idle:
c
// kernel/sched/core.c (v6.6, line 9298)
rq->idle = idle;
rcu_assign_pointer(rq->curr, idle);
idle->on_rq = TASK_ON_RQ_QUEUED;
两行核心:rq->idle = idle(这是本 CPU 的兜底线程)和 rq->curr = idle(现在就当它是"当前任务")。
3.1 两条路径:BSP 复用,AP 新建
idle 线程的建立有两条完全不同的路径,对应 BSP 和 AP:
| BSP(CPU0) | AP(其他 CPU) | |
|---|---|---|
| 谁建立的 | sched_init → init_idle(current, 0) |
idle_threads_init → idle_init → fork_idle |
| 线程哪来的 | 复用 init_task(现成的内核第一个任务) |
fork 出一个新 task |
| 时机 | start_kernel 早期(单核阶段) |
smp_init(AP 拉起阶段) |
AP 的路径:fork_idle(fork.c:2817)用 copy_process fork 一个全新的 task。这里有个反直觉的点:它的入口函数 idle_dummy 永远不会被调用 (源码注释明说 /* This function is never called */)------idle 线程不是靠"被调度选中后执行 fn"跑起来的,它只是需要一个 task_struct 占位(让 rq->idle / rq->curr 有值),真正的"运行"是 CPU 执行流直接停在 cpu_startup_entry 的 idle 循环里(BSP 在 rest_init、AP 在 start_secondary 末尾)。fork 完再 init_idle(task, cpu) 挂到 rq:
c
// kernel/fork.c (v6.6, line 2817)
struct task_struct * __init fork_idle(int cpu)
{
struct kernel_clone_args args = {
.flags = CLONE_VM,
.fn = &idle_dummy, // idle 线程不执行任何"工作"
.kthread = 1,
.idle = 1,
};
task = copy_process(&init_struct_pid, 0, cpu_to_node(cpu), &args);
if (!IS_ERR(task)) {
init_idle_pids(task);
init_idle(task, cpu); // 挂到 rq 当 idle
}
return task;
}
3.2 idle_threads_init 跳过 boot CPU
idle_threads_init(smpboot.c:66)为所有非 boot CPU fork idle 线程,唯独跳过 boot CPU:
c
// kernel/smpboot.c (v6.6, line 66)
void __init idle_threads_init(void)
{
unsigned int cpu, boot_cpu;
boot_cpu = smp_processor_id();
for_each_possible_cpu(cpu) {
if (cpu != boot_cpu)
idle_init(cpu); // 跳过 boot CPU
}
}
为什么跳过?因为 BSP 的 idle 线程(init_task)在 sched_init 里已经就位了,不需要再 fork。这个"跳过"正好印证了上一节的两条路径。
四、AP 拉起:多核点亮(smp_init)
到 smp_init(init/main.c:1540,在 kernel_init_freeable 里)这一阶段,内核要做的第一件事就是给 AP 建 idle 线程,然后把 AP 踢醒:
c
// kernel/smp.c (v6.6, line 960)
void __init smp_init(void)
{
idle_threads_init(); // ① 先给 AP 建 idle 线程
// ...
bringup_nonboot_cpus(setup_max_cpus); // ② 再 INIT-SIPI 拉起 AP
smp_cpus_done(setup_max_cpus);
}
AP 被拉起后,走的是 #2(AP 拉起)和 #3(hotplug 状态机)已经讲过的链路:start_secondary(arch/x86/kernel/smpboot.c:239)→ 走 hotplug 状态机的 STARTING/ONLINE 段 → 最后 cpu_startup_entry(CPUHP_AP_ONLINE_IDLE)(:326)进 idle 循环。
从调度器的角度看,AP 上线的关键回调是 sched_cpu_activate(core.c:9646),它把这个 CPU 的 rq 置为 online:
c
// kernel/sched/core.c (v6.6, line 9681)
rq_lock_irqsave(rq, &rf);
if (rq->rd) {
BUG_ON(!cpumask_test_cpu(cpu, rq->rd->span));
set_rq_online(rq); // rq 上线
}
rq_unlock_irqrestore(rq, &rf);
到这一步,每个 AP 都有了自己的 rq + idle 线程,单个 CPU 已经可以独立调度了 。但还差最后一样东西:调度域。跨核的负载均衡(一个 CPU 忙、另一个闲时怎么把任务挪过去)还没建立------这正是 sched_init_smp 要补的。
五、sched_init_smp:多核协同(构建调度域)
smp_init 之后紧跟着 sched_init_smp(init/main.c:1541),它补齐调度器的最后一块拼图------调度域:
c
// kernel/sched/core.c (v6.6, line 9851)
void __init sched_init_smp(void)
{
sched_init_numa(NUMA_NO_NODE); // ① NUMA 拓扑
mutex_lock(&sched_domains_mutex);
sched_init_domains(cpu_active_mask); // ② 构建调度域
mutex_unlock(&sched_domains_mutex);
/* 把 init 挪到非隔离 CPU 上 */
set_cpus_allowed_ptr(current, housekeeping_cpumask(HK_TYPE_DOMAIN));
current->flags &= ~PF_NO_SETAFFINITY;
sched_init_granularity(); // ③ 调度粒度参数
init_sched_rt_class(); // ④ RT/DL 类的 SMP 初始化
init_sched_dl_class();
sched_smp_initialized = true;
}
几个关键点:
-
sched_init_domains是重头戏:它根据 CPU 的拓扑构建一棵分层的调度域树------x86 默认从内到外是 SMT(核内 sibling)→ MC(多核,同 die 内不同 core)→ DIE(同 socket)→ NUMA(跨 node),层级越往外迁移越贵。负载均衡器就是在这棵树上按"就近优先"的原则搬任务:先在同 SMT 域内找,找不到再往上层域扩展。 -
init_sched_rt_class/init_sched_dl_class(rt.c:2520/deadline.c:2533):只是分配 per-CPU 的 cpumask(local_cpu_mask/local_cpu_mask_dl),给 RT/DL 任务做 push/pull 迁移用。它们和init_sched_fair_class(fair.c:12990,在sched_init里就调了)的区别是:fair 的初始化还包括注册SCHED_SOFTIRQ负载均衡软中断,RT/DL 只差这几个迁移用的 mask,所以放到 SMP 阶段再补。 -
sched_smp_initialized = true:这个标志和sched_init里的scheduler_running = 1是一对------scheduler_running是"单核能调度了",sched_smp_initialized是"多核协同也建立好了"。后面 hotplug 运行时上线/下线 CPU,会先查这个标志决定要不要重建调度域(见sched_cpu_activate里的if (sched_smp_initialized))。
时序小结
把三个阶段放进整体启动时序(衔接 #1 的启动链路):
| 阶段 | 调用点 | 点亮了什么 |
|---|---|---|
| ① 单核 | start_kernel(:870)→ sched_init(:940) |
调度类已就位 + 所有 rq + BSP idle 线程 |
| ② 多核 | kernel_init(:1428)→ kernel_init_freeable(:1518)→ smp_init(:1540) |
每个 AP 的 idle 线程 + rq 上线 |
| ③ 协同 | 同上 → sched_init_smp(:1541) |
调度域 + RT/DL 迁移 mask |
注意一个容易忽略的点:sched_init 在 start_kernel 里(BSP 单核阶段),而 smp_init / sched_init_smp 在 kernel_init 里(pid1 的内核线程,此时 BSP 已经变成了 idle 线程,由 pid1 来干 SMP 化这些"收尾活")。这正好呼应 #1 里"BSP 在 rest_init 里创建 pid1 后自己变 idle"的设定。
六、为什么这样设计(Why 层)
6.1 为什么要分三个阶段,而不是一次性点亮
最直接的原因是信息不足 。sched_init 跑在 start_kernel 里,那时:
- 内存分配器、percpu 变量、CPU 拓扑(哪些 CPU 存在、怎么排布)都还没完全就位;
- 更关键的是,调度域必须等所有 CPU 都上线后才能建------你要画"哪些 CPU 属于同一个 domain",总得先知道 CPU 集合长什么样。
所以内核只能"能点亮的先点亮":单核阶段先把 rq 和 BSP idle 就位,让系统先跑起来;等 AP 都拉起来了,CPU 全集确定了,再回头建调度域。这是**"先让系统能跑,再逐步完善协同能力"**的渐进式引导(bootstrap),不是偷懒。
6.2 为什么 BSP 复用 init_task,AP 要 fork
BSP 的 idle 线程是"现成的":init_task 是内核编译期就静态定义好的第一个任务,start_kernel 就是它在跑。让它直接转岗当 idle,省掉一次 fork 的开销,也天然保证了"启动早期一定有个 idle 线程可挂"。
AP 则没有这种现成任务------它是被 INIT-SIPI 从实模式硬唤醒的裸 CPU,连栈都是新给的,只能 fork_idle 现造一个。"有现成的就复用,没有就 fork",这个取舍很朴素,但背后保证了启动最早期(哪怕是单核)也有 idle 兜底。
6.3 为什么调度类要静态注册(而非运行时注册)
回到 1.3,再往深一层:调度类静态注册,本质是**"优先级"这一维度的信息在编译期就完全确定了**。五个类的优先级关系永远不会变,那"按优先级排成数组"就是最自然、最高效的表达------for_each_class 一次指针自增就能访问下一个类,没有任何间接开销。
对比 hotplug 状态机(#3):它的状态是运行时可增可减的 (动态状态池 cpuhp_setup_state),所以必须用运行时注册。"编译期就确定的用静态数组,运行时会变的用注册表"------这两个截然不同的选择,映射到内核里就是"调度类 vs hotplug 状态"这两套机制,放在一起看能更清楚地理解各自的取舍。
6.4 为什么 idle 线程必须是"永远可运行"
调度器的核心循环是"从 rq 挑一个任务跑"。如果允许 rq 上"一个任务都没有",那这个循环就得处理空队列的边界------每次挑任务都要判空,麻烦且容易出 bug。idle 线程的存在,把这个边界消除了 :rq 上永远至少有一个 idle 任务可挑(idle_sched_class 优先级最低,只有没别的任务时才轮到它)。所以 idle 线程不是"优化",而是"让调度循环不需要处理空队列"的正确性保证。
七、与已有博客的交叉引用
| 已有博客 | 本篇覆盖的关系 |
|---|---|
| CPU #1 BSP 拉起 | sched_init 在 start_kernel 里把 BSP 的 init_task 变成 idle 线程------这正是 #1 里"BSP 终点是 idle"的调度侧细节 |
| CPU #2 AP 拉起 | smp_init → idle_threads_init → fork_idle 为 AP 建 idle,AP 的 start_secondary 最后 cpu_startup_entry 进 idle 循环 |
| CPU #3 hotplug 状态机 | sched_cpu_activate / sched_cpu_starting 是 hotplug 状态机的回调,AP 上线走状态机时让 rq 上线(set_rq_online);sched_smp_initialized 决定运行时热插拔要不要重建调度域 |
| CPU #6 cpufreq | schedutil governor 消费调度器的 util------调度器启动完成后,才谈得上调频 |
附:本篇关键源码索引
| 符号 | 位置 |
|---|---|
sched_init / sched_init_smp |
kernel/sched/core.c:9909 / :9851 |
sched_init / smp_init / sched_init_smp 调用点 |
init/main.c:940 / :1540 / :1541 |
DEFINE_SCHED_CLASS / for_each_class |
kernel/sched/sched.h:2317 / :2329 |
| 调度类链接脚本段 | include/asm-generic/vmlinux.lds.h:129 |
| 五个调度类定义 | stop_task.c:117 / deadline.c:2718 / rt.c:2699 / fair.c:12904 / idle.c:478 |
init_idle(挂 idle 到 rq) |
kernel/sched/core.c:9250 |
idle_threads_init / idle_init / idle_thread_set_boot_cpu |
kernel/smpboot.c:66 / :50 / :39 |
fork_idle(fork AP 的 idle) |
kernel/fork.c:2817 |
sched_cpu_activate(rq 上线) |
kernel/sched/core.c:9646 |
start_secondary(AP 入口,结尾进 idle) |
arch/x86/kernel/smpboot.c:239 |
init_sched_fair_class / init_sched_rt_class / init_sched_dl_class |
fair.c:12990 / rt.c:2520 / deadline.c:2533 |