schedule() 是 Linux 内核调度器的核心入口函数,它的核心作用是从就绪队列中挑选最优的进程,完成当前进程与目标进程的上下文切换,是决定哪个进程能获得 CPU 执行权的 “总导演”。
该函数位于 kernel/sched/core.c,触发时机非常广泛:
- 进程主动放弃 CPU(如
sleep()、yield()) - 进程被抢占(如高优先级进程被唤醒、时间片耗尽)
- 内核态完成关键操作后检查
need_resched标志
函数核心作用
- 前置准备与状态检查:保存当前进程的运行状态,清理临时调度标志,确保调度环境安全。
- 挑选下一个待运行进程:调用调度类的
pick_next_task()方法,从当前 CPU 的运行队列(runqueue)中选择 “最优” 进程(CFS 选虚拟运行时间最小的,RT 选优先级最高的)。 - 完成进程上下文切换:若选中的进程不是当前进程,切换页表、栈、寄存器等硬件 / 软件上下文,让目标进程获得 CPU 执行权。
- 后置清理与统计更新:更新当前进程的调度统计信息(如运行时间、等待时间),处理调度后的收尾工作。
函数 原型 (以 Linux 5.10 为例)
asmlinkage void __sched schedule(void)
AI写代码
- 修饰符
asmlinkage:表示函数仅从栈中获取参数(内核函数的特殊修饰)。 - 修饰符
__sched:标记这是调度器相关函数,用于内核栈校验和统计。 - 返回值:无(切换上下文后,当前进程的执行会被暂停,直到后续被再次调度执行时,从切换点继续返回)。
注意:实际开发中,用户 / 内核代码很少直接调用
schedule(),更多是调用schedule_timeout()、yield()等封装函数,最终间接触发schedule()。
核心实现流程(基于 Linux 5.10)
Linux 5.10 中 schedule() 是
轻量级
封装,核心逻辑在 __schedule() 中,整体流程如下(简化版,去除大量异常处理和统计逻辑):
- // 外层封装:schedule()
- asmlinkage void __sched schedule(void)
- {
- struct task_struct *tsk = current;
-
- // 1. 前置检查:禁止抢占(防止调度过程被打断)
- sched_preempt_disable();
- // 2. 调用核心调度逻辑 __schedule()
- __schedule(false);
- // 3. 后置恢复:启用抢占
- sched_preempt_enable();
- }
-
- // 核心逻辑:__schedule()
- static void __sched __schedule(bool preempt)
- {
- struct task_struct *prev, *next;
- struct rq *rq;
- unsigned long flags;
-
- // 步骤1:初始化与加锁
- prev = current; // prev 指向当前正在运行的进程
- rq = this_rq(); // 获取当前 CPU 的运行队列(runqueue)
- raw_spin_lock_irqsave(&rq->lock, flags); // 锁定 runqueue,关闭中断
-
- // 步骤2:更新当前进程状态与 runqueue 时钟
- update_rq_clock(rq); // 更新运行队列的时钟,为调度提供时间基准
- clear_tsk_need_resched(prev); // 清除当前进程的 "需要调度" 标志
-
- // 若当前进程不是运行态(如休眠),将其从运行队列中移除
- if (prev->state != TASK_RUNNING && !(preempt && prev->state == TASK_INTERRUPTIBLE)) {
- dequeue_task(rq, prev, DEQUEUE_SLEEP); // 从 runqueue 中删除
- prev->on_rq = 0; // 标记进程不在运行队列上
- }
-
- // 步骤3:挑选下一个要运行的进程(核心!)
- next = pick_next_task(rq, prev, preempt); // 从 runqueue 中选最优进程
-
- // 步骤4:更新 runqueue 统计信息(如负载、运行进程数)
- rq->nr_switches++; // 累计上下文切换次数
- rq->curr = next; // 将 runqueue 的当前进程设为 next
-
- // 步骤5:完成上下文切换(若 prev != next)
- if (prev != next) {
- prepare_task_switch(rq, prev, next); // 切换前准备(如清理缓存、更新 mm_struct)
- // 真正的上下文切换:硬件+软件上下文切换
- context_switch(rq, prev, next);
- finish_task_switch(prev); // 切换后清理(如释放 prev 进程的资源)
- } else {
- raw_spin_unlock_irqrestore(&rq->lock, flags); // 无需切换,直接解锁
- }
- }
AI写代码
关键步骤拆解
进程状态处理与出队(dequeue_task)
- 若当前进程(
prev)已进入休眠状态(TASK_INTERRUPTIBLE/TASK_UNINTERRUPTIBLE),则调用dequeue_task将其从运行队列中移除,确保后续挑选进程时不会选中它。 - 仅
TASK_RUNNING状态的进程会保留在运行队列中,等待被挑选。
挑选下一个进程(pick_next_task)
- 这是调度器的 “决策核心”,遵循调度类优先级顺序(DL 硬实时 > RT 软实时 > CFS 普通进程 > IDLE 空闲进程)。
- 不同调度类实现各自的
pick_next_task方法: - CFS 调度类:从红黑树中找到
vruntime(虚拟运行时间)最小的进程,保证公平调度。 - RT 调度类:从优先级链表中找到最高优先级的进程,保证实时响应。
- IDLE 调度类:仅当运行队列中无其他进程时,才挑选
idle进程(占用 CPU 执行空循环)。
上下文切换(context_switch)
这是 schedule() 最底层的操作,负责完成 “进程切换” 的实际工作,分为两个核心步骤:
- 地址空间切换:若
prev和next是不同进程(非线程),调用switch_mm()切换页表(mm_struct),让 CPU 访问新进程的虚拟地址空间。 - 寄存器与栈切换:调用
switch_to()(汇编实现,与架构强相关),保存prev的寄存器状态到其内核栈,恢复next的寄存器状态从其内核栈,最终让next进程获得 CPU 执行权。 switch_to()是架构相关代码,在 ARM64 中位于arch/arm64/kernel/process.S,X86 中位于arch/x86/kernel/process_64.c。
与之前函数的关联(wake_up_new_task/wake_up_process)
schedule() 是前两个函数的 “最终落地者”,三者构成了 Linux 进程调度的完整链路:
wake_up_new_task():将新进程加入运行队列,标记 “需要调度”(可选),但不直接触发schedule()。wake_up_process():将休眠进程唤醒并加入运行队列,标记 “需要调度”(可选),但不直接触发schedule()。schedule():当内核到达 “调度点”(如系统调用返回、中断处理完成),检查到need_resched标志为真,执行调度逻辑,将前两个函数加入队列的进程,挑选并切换执行。
简单说:前两个函数是 “把选手送上赛场”,schedule() 是 “吹哨开始比赛,挑选最优选手上场”。
架构相关差异(以 ARM64 为例)
schedule() 的核心逻辑(pick_next_task)是架构无关的,但底层的 context_switch 尤其是 switch_to() 是架构强相关的,ARM64 有以下特殊处理:
- 内核栈切换:ARM64 每个进程的内核栈是独立的(大小为 16KB),
switch_to()会更新sp寄存器(栈指针),指向next进程的内核栈。 - TPIDR_EL1 寄存器更新:该寄存器存储当前进程的
task_struct指针,switch_to()会更新该寄存器,让current宏能正确获取当前进程。 - 中断上下文保护:切换前关闭中断,切换完成后恢复中断状态,防止切换过程中被中断打断导致栈混乱。
- 大小核适配:
this_rq()会根据当前 CPU 是大核还是小核,获取对应的运行队列,确保进程在指定核组内被调度。
触发时机与调度点
schedule() 不会被随机调用,只能在安全的调度点执行,常见触发时机:
- 主动触发:进程调用
msleep()、pthread_yield()等函数,最终调用schedule()放弃 CPU。 - 被动触发:
- 进程时间片耗尽,时钟中断处理函数标记
need_resched,中断返回时触发schedule()。 - 高优先级进程被唤醒,
wake_up_process()标记need_resched,当前进程执行到调度点时触发schedule()。
- 进程时间片耗尽,时钟中断处理函数标记
- 内核自动触发:系统调用返回用户态前、中断处理完成后,内核会检查
need_resched标志,若为真则触发schedule()。
常见问题与注意事项
- 调度死锁:若在持有自旋锁时触发
schedule(),会导致死锁(自旋锁不允许进程休眠 / 切换),内核提供might_sleep()宏用于检测此类问题。 need_resched标志遗漏:若唤醒进程后未标记need_resched,可能导致新进程无法被及时调度,出现响应延迟。- 上下文切换开销:过于频繁的
schedule()会增加上下文切换开销(页表切换、缓存失效),影响系统性能,需合理控制进程唤醒频率。
总结
schedule()是 Linux 调度器的核心入口,核心职责是挑选最优进程并完成上下文切换。- 其核心逻辑分为 “前置准备 - 挑选进程 - 上下文切换 - 后置清理” 四步,其中
pick_next_task()(决策)和context_switch()(落地)是关键。 - 它与
wake_up_new_task()/wake_up_process()构成完整调度链路,前两者负责入队,schedule()负责执行切换。 - 核心逻辑架构无关,底层上下文切换与架构强相关,且仅能在安全调度点触发。


1万+

被折叠的 条评论
为什么被折叠?



