[TOC]
kernel/sched 调度器核心(Scheduler Core) 进程调度与上下文切换的决策中心
历史与背景
这项技术是为了解决什么特定问题而诞生的?
kernel/sched/core.c
及其关联文件(如fair.c
, rt.c
)共同构成了Linux内核的进程调度器(Process Scheduler)。这项技术是为了解决在分时多任务操作系统中一个最根本、最核心的问题:在多个可运行的进程(或线程)之间,如何以及何时分配有限的CPU时间,以达到系统设定的目标。
这些目标通常是相互冲突的,调度器的设计就是在它们之间做出权衡和取舍:
- 公平性(Fairness):确保每个普通进程都能获得合理的CPU时间份额,防止任何进程被“饿死”。
- 吞吐量(Throughput):最大化系统在单位时间内完成的总工作量。这通常有利于长时间运行的计算密集型任务。
- 响应性(Responsiveness):最小化交互式任务(如GUI应用、文本编辑器)的响应延迟,让用户感觉系统“流畅”。这要求任务能被快速唤醒并执行。
- 实时性(Real-time):确保需要满足严格截止时间(deadline)的实时任务能够在规定时间内运行。
kernel/sched/core.c
扮演的角色是调度框架的中枢,它负责实现与具体调度策略无关的通用逻辑,如进程的唤醒、睡眠、上下文切换的触发,以及周期性的调度决策点(tick)。
它的发展经历了哪些重要的里程碑或版本迭代?
Linux调度器的发展史是一部不断追求更高性能、更好公平性和更低延迟的优化史。
- 早期调度器(O(N) Scheduler):非常简单,每次调度决策都需要遍历系统中所有的可运行进程,其复杂度与进程数成正比(O(N)),在进程数多时性能极差。
- O(1) 调度器:在2.5/2.6内核中引入,是一个重大的进步。它使用基于优先级的运行队列,使得选择下一个要运行的进程的时间复杂度变为常数(O(1))。但它在判断交互式任务方面过于复杂和启发式,公平性也不理想。
- 完全公平调度器(Completely Fair Scheduler, CFS):由Ingo Molnar开发,在2.6.23内核中引入,是革命性的里程碑。CFS彻底抛弃了复杂的启发式算法,转而追求一种理想化的、极其简单的“公平”模型:它模拟一个“理想的多任务CPU”,在这个CPU上,每个进程都以同等的速度并行执行。CFS的核心是维护一个以“虚拟运行时间”(vruntime)为key的红黑树,每次都选择树中vruntime最小的进程来运行。这使得它在提供出色公平性的同时,也保持了高效的进程选择性能(O(log N))。
- 实时调度策略的增强:内核始终支持
SCHED_FIFO
和SCHED_RR
这两种POSIX标准的实时调度策略。后来又引入了SCHED_DEADLINE
,这是一种基于最早截止时间优先(EDF)算法的调度策略,为硬实时任务提供了更强的可预测性保证。
目前该技术的社区活跃度和主流应用情况如何?
进程调度器是Linux内核中最核心、最活跃、被研究和优化得最深入的子系统之一。
- 主流应用:它是所有Linux系统的脉搏。从Android手机(对响应性要求极高)到大型服务器(对吞吐量要求高),再到嵌入式实时系统(对可预测性要求高),Linux调度器通过其模块化的调度类(Scheduling Classes)框架和可调参数,适应着各种截然不同的工作负载。CFS是目前所有通用Linux系统(桌面、服务器)的默认调度器。
核心原理与设计
它的核心工作原理是什么?
现代Linux调度器是一个基于**调度类(Scheduling Classes)**的模块化框架。core.c
负责顶层协调,而具体的调度决策则委托给不同的调度类。
运行队列(Runqueue):每个CPU核心都有一个自己的运行队列(
struct rq
)。这是一个核心数据结构,包含了该CPU上所有可运行的进程,以及与调度相关的各种状态。调度类(Scheduling Classes):调度器按优先级从高到低分为几个类,如:
StopTask
:最高优先级,用于停止CPU等内部任务。Deadline
(SCHED_DEADLINE
)Real-Time
(SCHED_FIFO
,SCHED_RR
)Fair
(SCHED_NORMAL
,SCHED_BATCH
,即CFS)IdleTask
:最低优先级,当没有其他任务可运行时执行。
核心调度函数
schedule()
:- 当内核需要在当前进程上进行重新调度时(例如,当前进程阻塞、时间片用完或被更高优先级的任务抢占),会调用
schedule()
。 schedule()
的核心逻辑是调用pick_next_task()
来选择下一个要运行的进程。pick_next_task()
会按优先级顺序遍历各个调度类。它会询问Deadline类:“你有可运行的进程吗?”,如果有,就选择它;如果没有,就去问Real-Time类,以此类推。- CFS(Fair类)是普通进程的归宿。只要系统中没有更高优先级的实时任务,调度决策就会落到CFS手中。
- 当内核需要在当前进程上进行重新调度时(例如,当前进程阻塞、时间片用完或被更高优先级的任务抢占),会调用
CFS的核心原理:
- 虚拟运行时间 (vruntime):CFS为每个进程维护一个vruntime。进程运行得越久,它的vruntime增长得越快。被赋予更高nice值的进程,其vruntime增长得更慢。
- 红黑树:每个CPU的CFS运行队列 (
cfs_rq
) 内部是一棵红黑树,所有可运行的普通进程都按其vruntime值在这棵树中排序。 - 决策:当CFS需要选择下一个进程时,它总是选择红黑树最左边的节点,因为这个节点的vruntime最小,意味着它是被“亏欠”CPU时间最多的进程。
上下文切换(Context Switch):
pick_next_task()
选出下一个要运行的进程(next
)后,schedule()
会调用context_switch()
。- 这个函数负责执行底层的、与CPU架构相关的操作:保存当前进程(
prev
)的CPU寄存器状态到其内核栈,然后加载next
进程的寄存器状态到CPU中。执行完这条指令后,CPU就开始执行next
进程的代码了。
它的主要优势体现在哪些方面?
- 模块化与可扩展性:调度类框架使得添加新的调度策略变得相对容易。
- CFS的公平性与简洁性:CFS用一个简单而优雅的模型取代了过去复杂的启发式算法,提供了出色的公平性和对交互式任务的良好支持。
- per-CPU 运行队列:避免了全局锁,提供了极佳的多核扩展性。
- 可调优性:通过
sysctl
和cgroup,提供了丰富的参数供系统管理员根据具体负载进行调优。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 通用性与特定负载的矛盾:作为一个通用调度器,它试图在所有目标(吞吐量、延迟、公平性)之间取得平衡,这意味着它在任何单一目标上可能都不是“最优”的。例如,一个专为HPC设计的调度器可能会牺牲公平性来换取极致的吞-吐量。
- 唤醒延迟:当一个任务在一个CPU上被唤醒,但它需要的数据却在另一个CPU的缓存中时,如何高效地进行任务迁移以获得最佳性能(缓存亲和性 vs 唤醒延迟),是一个非常复杂且持续在优化的难题。
- 实时性能:虽然Linux提供了实时调度策略,但要实现真正的硬实时,还需要配合
PREEMPT_RT
补丁集,该补丁集对内核中大量的自旋锁等进行了修改,以确保内核的可抢占性。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?
Linux调度器是所有基于Linux的系统的默认和首选解决方案。不同的调度策略适用于不同的场景:
SCHED_NORMAL
(CFS):桌面系统、Web服务器、数据库等绝大多数通用计算负载。SCHED_BATCH
(CFS):用于非交互式的、计算密集型的批处理任务。它告诉CFS可以稍微牺牲一点延迟来换取更好的缓存利用率和吞吐量。SCHED_FIFO
/SCHED_RR
:软实时应用,如音频/视频处理、工业控制中对延迟有要求但能容忍微小抖动的任务。SCHED_DEADLINE
:硬实时应用,如高频交易、机器人控制等,这些任务的正确性不仅取决于计算结果,还取决于计算完成的时间。
是否有不推荐使用该技术的场景?为什么?
对于绝大多数场景,Linux调度器都是适用的。只有在一些极端专用的领域,可能会选择其他操作系统:
- 极端的安全关键和硬实时系统:例如航空电子、医疗生命支持系统。这些领域通常使用经过严格形式化验证的、更简单的实时操作系统(RTOS)。虽然
PREEMPT_RT
Linux正在越来越多地进入这些领域,但传统的RTOS仍占有一席之地。
对比分析
请将其 与 其他相似技术 进行详细对比。
最有趣的对比是Linux调度器(以CFS为代表)与其他操作系统或调度思想的比较。
特性 | Linux CFS | 传统优先级调度器 (如 O(1), Windows NT) | 实时操作系统 (RTOS) 调度器 |
---|---|---|---|
核心思想 | 公平性。模拟理想并行执行,基于“虚拟运行时间”。 | 优先级。严格按照离散的优先级级别进行调度。 | 可预测性。通常是严格的、基于优先级的抢占式调度。 |
进程选择 | O(log N) ,从红黑树中选择vruntime最小的。 |
O(1) ,从优先级位图中找到最高优先级的队列。 |
O(1) 或 O(N) ,取决于实现。 |
公平性 | 非常好,是其核心设计目标。 | 较差。低优先级任务可能被持续饿死。 | 不保证。公平性不是其主要目标。 |
对交互任务的处理 | 内生地好。交互任务因睡眠时间长,vruntime增长慢,被唤醒后能很快被调度。 | 启发式。需要复杂的算法(如睡眠时间、CPU使用率)来“猜测”谁是交互任务并提升其优先级。 | 不区分。只看优先级。 |
实时支持 | 通过独立的、更高优先级的调度类来实现。 | 通常也是通过高优先级来实现。 | 核心设计目标。整个系统的设计都为保证可预测性服务。 |
复杂性 | 模型简单,实现中等。 | 模型复杂(启发式),实现复杂。 | 模型简单,实现相对简单。 |
include/linux/sched/task.h
task_lock task_unlock
1 | /* |
include/uapi/linux/sched.h
调度策略
1 | /* |
include/linux/sched.h
cond_resched 条件调度
1 | static inline int _cond_resched(void) |
kernel/sched/sched.h
global_rt_runtime global_rt_period
1 | /* |
raw_spin_rq_lock 运行队列的原子自旋锁
这段代码和注释详细描述了调度器中任务状态的序列化规则,以及如何通过锁机制确保多线程环境下的线程安全性。以下是对主要内容的分段解析:
锁的顺序
注释中首先定义了锁的获取顺序,以避免死锁问题:
p->pi_lock
是任务的优先级继承锁,用于保护任务的优先级相关数据。rq->lock
是调度队列的锁,用于保护调度队列的状态。hrtimer_cpu_base->lock
是高精度定时器的锁,通常用于带宽控制。
此外,当涉及多个调度队列时,锁的顺序必须遵循 rq1->lock
在 rq2->lock
之前获取的规则,其中 rq1 < rq2
。
常规状态(Regular State)
在正常的调度状态下,rq->lock
是主要的同步机制:
- 调度器的核心函数
__schedule()
会获取当前 CPU 的rq->lock
,以确保安全地操作本地调度队列。它可能会从运行队列中移除任务,并选择下一个最合适的任务运行。 - 任务的入队操作(enqueue)也需要在
rq->lock
下进行,即使操作来自其他 CPU。 - 对于跨 NUMA 域的任务唤醒,可能会使用中断(IPI)将任务的入队操作转移到本地 CPU,以减少运行队列状态的频繁切换。
任务唤醒(wakeups)尤其是涉及迁移的唤醒操作非常复杂,为了避免同时获取两个 rq->lock
,需要特殊的处理。
特殊状态(Special State)
对于系统调用或外部操作,会使用 task_rq_lock()
同时获取 p->pi_lock
和 rq->lock
。这确保了在持有任一锁时,任务的状态是稳定的。例如:
- 修改任务的 CPU 亲和性(
sched_setaffinity
)。 - 调整任务的优先级(
set_user_nice
)。 - 更改调度策略(
__sched_setscheduler
)。 - 更新 NUMA 节点或任务组。
这些操作需要对任务的状态进行一致性保护,因此需要同时持有两个锁。
任务状态字段
p->state
任务的状态(TASK_*
)可以通过无锁方式修改,例如set_current_state()
或try_to_wake_up()
。其中try_to_wake_up()
使用p->pi_lock
来防止并发问题。p->on_rq
表示任务是否在运行队列中:0
:不在运行队列中。1
:任务已入队。2
:任务正在迁移(TASK_ON_RQ_MIGRATING
)。此状态用于在不同时持有两个rq->lock
的情况下进行迁移。
特殊情况下,任务可能在运行队列中但不可运行(例如
p->se.sched_delayed
为true
)。p->on_cpu
表示任务是否正在运行:1
:任务正在其 CPU 上运行。0
:任务未运行。
这一字段在任务被调度入 CPU 前设置,在任务被调度出 CPU 后清除。
task_cpu(p)
任务的 CPU 分配由set_task_cpu()
修改,但有以下规则:- 不应对阻塞任务调用
set_task_cpu()
。 - 唤醒任务时在
p->pi_lock
下调用。 - 迁移任务时在
rq->lock
或双队列锁(double_rq_lock()
)下调用。
- 不应对阻塞任务调用
函数 raw_spin_rq_lock_nested
该函数实现了对调度队列锁的嵌套加锁逻辑:
- 如果调度核心功能被禁用,直接对
rq->__lock
加锁。 - 如果调度核心功能启用,进入循环尝试获取锁,确保锁指针在加锁期间未发生变化。
通过这种设计,函数能够在不同的调度模式下安全地获取锁,保护调度队列的状态。
总结
这段代码和注释详细描述了调度器中任务状态的管理规则,以及如何通过锁机制确保多线程环境下的安全性和一致性。它涵盖了正常调度状态、特殊操作以及任务状态字段的修改规则,是调度器实现中关键的同步逻辑。
1 | void raw_spin_rq_lock_nested(struct rq *rq, int subclass) |
rq::clock_update_flags bits
这段代码定义了运行队列(rq
)的 clock_update_flags
字段的三个标志位(RQCF_REQ_SKIP
、RQCF_ACT_SKIP
和 RQCF_UPDATED
),它们用于优化和调试调度器中运行队列的时钟更新逻辑。这些标志位通过位操作进行管理,帮助调度器高效地处理任务调度中的时钟更新。
标志位说明
RQCF_REQ_SKIP
(0x01):- 表示请求在下一次调用
__schedule()
时跳过运行队列时钟的更新。 - 这是一个优化标志,用于避免对相邻运行队列的时钟进行不必要的更新,从而减少开销。
- 表示请求在下一次调用
RQCF_ACT_SKIP
(0x02):- 在
__schedule()
内部设置,表示当前正在跳过运行队列时钟的更新。 - 当此标志生效时,对
update_rq_clock()
的调用将被忽略。
- 在
RQCF_UPDATED
(0x04):- 这是一个调试标志,用于指示自上次固定运行队列锁(
rq::lock
)以来,是否调用过update_rq_clock()
。 - 该标志帮助开发者检查时钟更新是否按预期发生。
- 这是一个调试标志,用于指示自上次固定运行队列锁(
标志位的使用
在
__schedule()
中,clock_update_flags
的值可能会左移一位(通过位移操作将RQCF_REQ_SKIP
提升为RQCF_ACT_SKIP
),以便快速切换到跳过时钟更新的状态。为了检查
RQCF_UPDATED
是否被设置,必须使用以下条件:1
if (rq->clock_update_flags >= RQCF_UPDATED)
这是因为标志位可能被左移,但不会超过一个位置。
在调用
rq_unpin_lock()
时,clock_update_flags
1 | /* |
rq_pin_lock 将运行队列锁定到当前 CPU
1 | /* |
task_rq_lock task_rq_unlock 任务运行队列锁
1 | DEFINE_LOCK_GUARD_1(task_rq_lock, struct task_struct, |
update_rq_clock_task 更新运行队列的时钟任务
1 | /* |
update_rq_clock 更新运行队列(rq)的时钟
1 | void update_rq_clock(struct rq *rq) |
task_rq 获取任务所在的运行队列
1 |
DEFINE_SCHED_CLASS 定义调度类
1 | /* |
put_prev_task 将前一个任务从运行队列中移除
1 | static inline void put_prev_task(struct rq *rq, struct task_struct *prev) |
set_next_task 设置下一个任务
1 | static inline void set_next_task(struct rq *rq, struct task_struct *next) |
rt_effective_prio 计算实时任务的有效优先级
1 |
|
entity_is_task 判断实体是否为任务
1 |
task_on_rq_queued 判断任务是否在运行队列中排队
1 | static inline int task_on_rq_queued(struct task_struct *p) |
task_current_donor 判断任务是否为当前的捐赠者
1 | /* |
task_has_idle_policy 判断任务是否具有空闲策略
1 | /* |
task_on_cpu 判断任务是否在 CPU 上运行
1 | /* |
include/linux/sched/mm.h
mmgrab_lazy_tlb 懒惰的 转译后备缓冲区(Translation Lookaside Buffer)内存管理单元(MMU)引用计数
- 该函数适用于需要长时间或不确定时间内保持对 mm_struct 的引用的场景。
- 例如,当某些内核子系统需要访问进程的内存管理信息时,可以调用 mmgrab 来确保 mm_struct 在操作期间不会被释放
1 | /** |
mmdrop_lazy_tlb_sched Mmdrop 懒惰 TLB 调度
1 | static inline void mmdrop(struct mm_struct *mm) |
include/uapi/linux/sched/types.h
1 | /** |
kernel/sched/clock.c
sched_clock_cpu
1 | notrace unsigned long long __weak sched_clock(void) |
sched_clock_init
1 | void __init sched_clock_init(void) |
kernel/sched/core.c
hrtick_rq_init 高分辨率定时器初始化
1 | static void hrtick_rq_init(struct rq *rq) |
set_load_weight 设置负载权重
- 该函数用于设置任务的负载权重(load weight),根据任务的调度策略和优先级来计算负载权重。
- 负载权重是调度器用来衡量任务对 CPU 资源需求的重要指标,直接影响任务的调度优先级。
1 |
|
dequeue_task 任务在运行队列中 将其从队列中移除
1 | /* |
__sched_fork 为新创建的进程(task_struct)执行与调度器相关的初始化操作
1 | /* |
sched_fork 为新创建的进程执行调度器相关的初始化
1 | /* |
init_idle 空闲任务初始化
1 | /** |
sched_init 调度器初始化
1 | void __init sched_init(void) |
sched_submit_work 提交任务的工作
1 | static inline void sched_submit_work(struct task_struct *tsk) |
pick_next_task 选择下一个任务
1 | /* |
prepare_lock_switch 准备切换任务
1 | /** |
context_switch 上下文切换
1 | /* |
__schedule 主调度器
1 | /* |
__schedule_loop 实现任务调度的循环逻辑
1 | static __always_inline void __schedule_loop(int sched_mode) |
schedule 实现任务的切换和调度
1 | asmlinkage __visible void __sched schedule(void) |
do_task_dead 任务退出的最后一步
1 | /* |
finish_lock_switch 完成锁切换
1 | static inline void finish_lock_switch(struct rq *rq) |
finish_task_switch 完成任务切换
1 | /* |
schedule_tail 完成上下文切换的后半部分清理工作
1 | /** |
__task_rq_lock 锁定任务所在的运行队列(rq)
1 | /* |
__task_rq_unlock 解锁任务所在的运行队列(rq)
1 | static inline void __task_rq_unlock(struct rq *rq, struct rq_flags *rf) |
update_rq_clock_task 更新运行队列的时钟
1 | /* |
update_rq_clock 更新运行队列的时钟
1 | void update_rq_clock(struct rq *rq) |
enqueue_task 将任务添加到运行队列
1 | void enqueue_task(struct rq *rq, struct task_struct *p, int flags) |
activate_task 将任务从休眠状态激活为可运行状态
- 将task入队调度器队列中,并设置任务状态为 TASK_ON_RQ_QUEUED
1 | void activate_task(struct rq *rq, struct task_struct *p, int flags) |
ttwu_state_match 检查给定任务的状态是否与指定的状态匹配
1 | static __always_inline |
ttwu_runnable 检查任务是否仍然处于可运行状态
1 | /* |
ttwu_do_activate 将任务从休眠状态激活为可运行状态
1 | static void |
ttwu_queue 检查任务是否可以直接加入唤醒列表
1 | static inline bool ttwu_queue_wakelist(struct task_struct *p, int cpu, int wake_flags) |
ttwu_do_wakeup 将任务标记为可运行
1 | /* |
try_to_wake_up 唤醒一个线程
- 任务不在运行队列中 ttwu_queue 将任务加入运行队列
1 | /** |
wake_up_process 唤醒一个特定的进程
1 | /** |
wake_q_add 将唤醒排队等待“稍后”唤醒
1 | /** |
wake_q_add_safe 安全地将唤醒操作排队以“稍后”唤醒。
1 | /** |
__wake_q_add 将任务安全地加入唤醒队列
1 | /* |
wake_up_q 唤醒队列中的所有任务
1 | /** |
wake_up_new_task 首次唤醒新创建的任务
1 | /* |
check_class_changing 通知调度类将要切换到另一个调度类
1 | /* |
check_class_changed 通知调度类从一个调度类切换到另一个调度类
1 | /* |
wait_task_inactive 等待一个线程变为非调度状态
- wait_task_inactive是一个在内核中用于同步等待一个指定的任务p进入一个非运行(inactive)状态的核心函数。非运行状态由调用者通过match_state掩码来定义(例如TASK_UNINTERRUPTIBLE, TASK_PARKED等)。
它的核心作用是:向调用者保证,当此函数成功返回一个正数时,目标任务p不仅当前不在任何CPU上运行,而且也已经从任何运行队列中被彻底移除,处于一个稳定的睡眠或停止状态。
1 | static __always_inline |