Linux 6.6内核 CPU 深度解析(七):调度器启动 — 从单核到多核,调度器分阶段点亮

〇、全景:调度器的启动,是"四样东西分阶段就位"

一个 CPU 要真正参与调度,不是"某个开关一拨"就完成的,而是有四样东西要先后就位:

  1. 调度类(sched_class)------决定"用什么策略调度"(CFS 公平 / RT 实时 / DL 截止期...);
  2. runqueue(rq)------决定"每个 CPU 的任务在哪排队"(per-CPU 的队列);
  3. idle 线程------决定"这个 CPU 没活干时跑什么"(兜底线程);
  4. 调度域(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;
}

几个关键点:

  1. sched_init_domains 是重头戏:它根据 CPU 的拓扑构建一棵分层的调度域树------x86 默认从内到外是 SMT(核内 sibling)→ MC(多核,同 die 内不同 core)→ DIE(同 socket)→ NUMA(跨 node),层级越往外迁移越贵。负载均衡器就是在这棵树上按"就近优先"的原则搬任务:先在同 SMT 域内找,找不到再往上层域扩展。

  2. 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 阶段再补。

  3. 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
相关推荐
50万马克的面包41 分钟前
CSDN-栈队列数组-知识点整理
linux·运维·服务器
MicrosoftCloud1 小时前
用户权限 04|/etc/sudoers 安全配置:从最小授权到「别把 ALL 随便给人」
运维·安全·ubuntu·sudo·sudoers
wjkjpcba2 小时前
PCBA烧录程序是什么:PCBA包工包料厂家解析烧录与测试
linux·数据库·人工智能·smt贴片加工·pcba贴片加工厂
꯭自꯭闭꯭2 小时前
达梦事物特性及MVCC
linux·运维·数据库
91刘仁德3 小时前
HTTP协议详解:从URL到HTTP服务器的完整实战
服务器·网络·笔记·网络协议·http
亚川楼宇自控系统数据中心厂家4 小时前
实验室智能化管理系统|人环物智一体化管控平台
运维
羔羊++4 小时前
18_实验十七_其他命令与自定义启动变量
linux
zmsup5 小时前
Agent 上下文调优:结合 OpsArk 运维智能体,设计模型每一步真正需要的信息
运维·agent·上下文压缩·运维智能体·上下文调优
DYWorker0016 小时前
Linux驱动:RK3568 GMAC + YT8531C 千兆以太网全流程解剖
linux