schedule()函数

schedule()Linux 内核调度器的核心入口函数,它的核心作用是从就绪队列中挑选最优的进程,完成当前进程与目标进程的上下文切换,是决定哪个进程能获得 CPU 执行权的 “总导演”。

该函数位于 kernel/sched/core.c,触发时机非常广泛:

  1. 进程主动放弃 CPU(如 sleep()yield()
  2. 进程被抢占(如高优先级进程被唤醒、时间片耗尽)
  3. 内核态完成关键操作后检查 need_resched 标志

函数核心作用

  1. 前置准备与状态检查:保存当前进程的运行状态,清理临时调度标志,确保调度环境安全。
  2. 挑选下一个待运行进程:调用调度类的 pick_next_task() 方法,从当前 CPU 的运行队列(runqueue)中选择 “最优” 进程(CFS 选虚拟运行时间最小的,RT 选优先级最高的)。
  3. 完成进程上下文切换:若选中的进程不是当前进程,切换页表、栈、寄存器等硬件 / 软件上下文,让目标进程获得 CPU 执行权。
  4. 后置清理与统计更新:更新当前进程的调度统计信息(如运行时间、等待时间),处理调度后的收尾工作。

函数 原型 (以 Linux 5.10 为例)

asmlinkage void __sched schedule(void)
AI写代码
  • 修饰符 asmlinkage:表示函数仅从栈中获取参数(内核函数的特殊修饰)。
  • 修饰符 __sched:标记这是调度器相关函数,用于内核栈校验和统计。
  • 返回值:无(切换上下文后,当前进程的执行会被暂停,直到后续被再次调度执行时,从切换点继续返回)。

注意:实际开发中,用户 / 内核代码很少直接调用 schedule(),更多是调用 schedule_timeout()yield() 等封装函数,最终间接触发 schedule()

核心实现流程(基于 Linux 5.10)

Linux 5.10 中 schedule() 轻量级 封装,核心逻辑在 __schedule() 中,整体流程如下(简化版,去除大量异常处理和统计逻辑):

  1. // 外层封装:schedule()
  2. asmlinkage void __sched schedule(void)
  3. {
  4. struct task_struct *tsk = current;
  5. // 1. 前置检查:禁止抢占(防止调度过程被打断)
  6. sched_preempt_disable();
  7. // 2. 调用核心调度逻辑 __schedule()
  8. __schedule(false);
  9. // 3. 后置恢复:启用抢占
  10. sched_preempt_enable();
  11. }
  12. // 核心逻辑:__schedule()
  13. static void __sched __schedule(bool preempt)
  14. {
  15. struct task_struct *prev, *next;
  16. struct rq *rq;
  17. unsigned long flags;
  18. // 步骤1:初始化与加锁
  19. prev = current; // prev 指向当前正在运行的进程
  20. rq = this_rq(); // 获取当前 CPU 的运行队列(runqueue)
  21. raw_spin_lock_irqsave(&rq->lock, flags); // 锁定 runqueue,关闭中断
  22. // 步骤2:更新当前进程状态与 runqueue 时钟
  23. update_rq_clock(rq); // 更新运行队列的时钟,为调度提供时间基准
  24. clear_tsk_need_resched(prev); // 清除当前进程的 "需要调度" 标志
  25. // 若当前进程不是运行态(如休眠),将其从运行队列中移除
  26. if (prev->state != TASK_RUNNING && !(preempt && prev->state == TASK_INTERRUPTIBLE)) {
  27. dequeue_task(rq, prev, DEQUEUE_SLEEP); // 从 runqueue 中删除
  28. prev->on_rq = 0; // 标记进程不在运行队列上
  29. }
  30. // 步骤3:挑选下一个要运行的进程(核心!)
  31. next = pick_next_task(rq, prev, preempt); // 从 runqueue 中选最优进程
  32. // 步骤4:更新 runqueue 统计信息(如负载、运行进程数)
  33. rq->nr_switches++; // 累计上下文切换次数
  34. rq->curr = next; // 将 runqueue 的当前进程设为 next
  35. // 步骤5:完成上下文切换(若 prev != next
  36. if (prev != next) {
  37. prepare_task_switch(rq, prev, next); // 切换前准备(如清理缓存、更新 mm_struct)
  38. // 真正的上下文切换:硬件+软件上下文切换
  39. context_switch(rq, prev, next);
  40. finish_task_switch(prev); // 切换后清理(如释放 prev 进程的资源)
  41. } else {
  42. raw_spin_unlock_irqrestore(&rq->lock, flags); // 无需切换,直接解锁
  43. }
  44. }
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() 最底层的操作,负责完成 “进程切换” 的实际工作,分为两个核心步骤:

  • 地址空间切换:若 prevnext 是不同进程(非线程),调用 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 进程调度的完整链路:

  1. wake_up_new_task():将新进程加入运行队列,标记 “需要调度”(可选),但不直接触发 schedule()
  2. wake_up_process():将休眠进程唤醒并加入运行队列,标记 “需要调度”(可选),但不直接触发 schedule()
  3. schedule():当内核到达 “调度点”(如系统调用返回、中断处理完成),检查到 need_resched 标志为真,执行调度逻辑,将前两个函数加入队列的进程,挑选并切换执行

简单说:前两个函数是 “把选手送上赛场”,schedule() 是 “吹哨开始比赛,挑选最优选手上场”。

架构相关差异(以 ARM64 为例)

schedule() 的核心逻辑(pick_next_task)是架构无关的,但底层的 context_switch 尤其是 switch_to() 是架构强相关的,ARM64 有以下特殊处理:

  1. 内核栈切换:ARM64 每个进程的内核栈是独立的(大小为 16KB),switch_to() 会更新 sp 寄存器(栈指针),指向 next 进程的内核栈。
  2. TPIDR_EL1 寄存器更新:该寄存器存储当前进程的 task_struct 指针,switch_to() 会更新该寄存器,让 current 宏能正确获取当前进程。
  3. 中断上下文保护:切换前关闭中断,切换完成后恢复中断状态,防止切换过程中被中断打断导致栈混乱。
  4. 大小核适配this_rq() 会根据当前 CPU 是大核还是小核,获取对应的运行队列,确保进程在指定核组内被调度。

触发时机与调度点

schedule() 不会被随机调用,只能在安全的调度点执行,常见触发时机:

  1. 主动触发:进程调用 msleep()pthread_yield() 等函数,最终调用 schedule() 放弃 CPU。
  2. 被动触发
    • 进程时间片耗尽,时钟中断处理函数标记 need_resched,中断返回时触发 schedule()
    • 高优先级进程被唤醒,wake_up_process() 标记 need_resched,当前进程执行到调度点时触发 schedule()
  3. 内核自动触发:系统调用返回用户态前、中断处理完成后,内核会检查 need_resched 标志,若为真则触发 schedule()

常见问题与注意事项

  1. 调度死锁:若在持有自旋锁时触发 schedule(),会导致死锁(自旋锁不允许进程休眠 / 切换),内核提供 might_sleep() 宏用于检测此类问题。
  2. need_resched 标志遗漏:若唤醒进程后未标记 need_resched,可能导致新进程无法被及时调度,出现响应延迟。
  3. 上下文切换开销:过于频繁的 schedule() 会增加上下文切换开销(页表切换、缓存失效),影响系统性能,需合理控制进程唤醒频率。

总结

  1. schedule() 是 Linux 调度器的核心入口,核心职责是挑选最优进程并完成上下文切换。
  2. 其核心逻辑分为 “前置准备 - 挑选进程 - 上下文切换 - 后置清理” 四步,其中 pick_next_task()(决策)和 context_switch()(落地)是关键。
  3. 它与 wake_up_new_task()/wake_up_process() 构成完整调度链路,前两者负责入队,schedule() 负责执行切换。
  4. 核心逻辑架构无关,底层上下文切换与架构强相关,且仅能在安全调度点触发。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值