- 操作系统
- 驱动开发
【免费下载链接】darwin-xnu
Legacy mirror of Darwin Kernel. Replaced by https://github.com/apple-oss-distributions/xnu
本文基于开源 darwin-xnu 仓库中 osfmk/kern/sched_clutch.md 设计文档,结合 osfmk/kern/sched_clutch.c 与 osfmk/kern/sched_clutch.h 的源码实现,系统讲解 Clutch Scheduler 的三层层次化调度架构、根桶 EDF 选择与 warp 机制、线程组优先级计算,以及线程级 Mach timesharing 衰减模型。读完本文,你将理解 XNU 如何通过"调度桶 → 线程组 → 线程"三层结构同时保证高 QoS 工作负载的低延迟与低 QoS 批量任务的防饿死,并能在源码中精准定位每个机制的实现位置。
背景:为什么传统 Mach 调度器难以满足现代负载
XNU 内核运行在多种平台上,需要在两类截然不同的需求之间取得平衡:
- 延迟敏感型工作负载:UI 交互、多媒体录制/播放等,需要快速获得 CPU;
- 批量型低优先级工作负载:照片同步、源码编译等,需要防饿死保证,但不能抢占关键任务。
传统 Mach 调度器通过给系统中每个线程打一个优先级数字来近似这两类需求:高优先级视为交互型、低优先级视为批量型,并使用基于优先级衰减的 timesharing 模型——线程消耗 CPU 越多、优先级衰减越多,从而实现 fairshare 与防饿死。这种"线程中心"的调度方式存在两个根本性缺陷:
- 不精确的记账(Inaccurate accounting):线程级 CPU 记账会激励系统创建更多线程;在 GCD 与 workqueue 时代线程被快速创建与销毁,线程级记账既不精确,还容易导致过度 CPU 消耗。
- 糟糕的隔离性(Poor isolation):timesharing 通过按全局系统负载衰减线程优先级实现,这会导致同级别或更低级别出现突发活动时,App/UI 线程也被衰减,造成性能与响应性下降。调度器几乎无法在延迟敏感的 UI 工作负载与大批量非敏感操作之间提供隔离。
更深层的问题在于:线程级调度丢失了线程与更高层用户工作负载之间的关联关系,调度器无法把工作负载当作一个整体来推理——而这恰恰是最终用户真正关心的。线程级 timesharing 的另一个产物是,同一优先级下的线程无论服务于哪个用户工作负载都被一视同仁,常常导致非最优决策;各子系统为了防饿死与避免同其他无关线程 timesharing,还会不断抬升自身优先级,最终造成平台范围内的优先级通胀(priority inflation)。
Clutch Scheduler 总体设计:三层层次化调度
为了解决上述问题,Clutch Scheduler 不再调度单个线程,而是调度线程组(thread group)。它打破传统的单层调度模型,实现了一个层次化调度器,在多个线程聚合层级上做出最优决策。当前实现包含 3 个层级:
- Scheduling Bucket Level(调度桶层)——最顶层,决定选择哪一类线程执行;
- Thread Group Level(线程组层)——决定一个桶内选择哪个线程组;
- Thread Level(线程层)——最底层,决定线程组内选择哪个线程。
调度桶由线程的 base/scheduling 优先级决定,粗略映射到 OS runtime 使用的 QoS 类(用于定义各类工作的性能预期)。同一调度桶内的所有可运行线程在顶层由单个条目代表,该条目在实现中称为root bucket(根桶)。
第一层:Scheduling Bucket Level(调度桶层)
调度桶层的目标:为高 QoS 类提供低延迟的 CPU 访问,同时为低 QoS 类保证防饿死。
EDF 根桶选择算法
调度桶层使用Earliest Deadline First(EDF)算法决定下一个执行哪个根桶。每个有可运行线程的根桶作为条目进入一个按 deadline(截止时间)排序的优先队列;选择算法直接选出 deadline 最早的根桶。根桶的 deadline 基于其 first-runnable 时间戳与该桶预定义的Worst Case Execution Latency(WCEL,最坏执行延迟)计算。
WCEL 的取值参考了 Mach timesharing 算法的衰减曲线,使得新调度器在更高层面看与旧调度器行为接近。其定义见 osfmk/kern/sched_clutch.c:
static uint32_t sched_clutch_root_bucket_wcel_us[TH_BUCKET_SCHED_MAX] = { SCHED_CLUTCH_INVALID_TIME_32, /* FIXPRI */ 0, /* FG */ 37500, /* IN (37.5ms) */ 75000, /* DF (75ms) */ 150000, /* UT (150ms) */ 250000 /* BG (250ms) */ };各调度桶的含义见 osfmk/kern/sched.h 的sched_bucket_t枚举:TH_BUCKET_FIXPRI(固定优先级)、TH_BUCKET_SHARE_FG(高于BASEPRI_DEFAULT的 timeshare 线程)、TH_BUCKET_SHARE_IN(BASEPRI_USER_INITIATED与BASEPRI_DEFAULT之间)、TH_BUCKET_SHARE_DF、TH_BUCKET_SHARE_UT、TH_BUCKET_SHARE_BG(最底层的 throttled/后台区间)。
关键行为:
- 当根桶从 non-runnable 变为 runnable 时,其 deadline 被设置为
(now + WCEL[bucket]),保证即使在重度负载系统中,该桶也会在WCEL[bucket]时间内被调度到; - 一旦根桶被选中执行,其 deadline 被向后推迟
WCEL[bucket]。
注意TH_BUCKET_FIXPRI的 WCEL 是SCHED_CLUTCH_INVALID_TIME_32(~0),表示"无效时间",因为 FIXPRI 桶被特例处理(见下文 AboveUI 特例)。
实现中,微秒单位通过sched_clutch_us_to_abstime()(osfmk/kern/sched_clutch.c)在启动时统一转换为绝对时间单位:无效值映射为SCHED_CLUTCH_INVALID_TIME_64,其余通过clock_interval_to_absolutetime_interval(us_vals[i], NSEC_PER_USEC, ...)转换,最终存入sched_clutch_root_bucket_wcel[]。
根桶 warp 机制:应对突发负载
基本 EDF 实现有一个重大问题:在重度负载系统中,高桶近期可能已消耗了足够多的 CPU,导致其 deadline 落在低桶之后。此时若出现一小波用户关键工作负载,高桶必须等低桶跑完才能获得 CPU,可能引发性能问题。
为此,调度桶层实现了根桶 warp(扭曲/前跳)机制:每个桶被赋予一个 warp 值,每当桶因 deadline 到期而被选中时刷新。定义见 osfmk/kern/sched_clutch.c:
static uint32_t sched_clutch_root_bucket_warp_us[TH_BUCKET_SCHED_MAX] = { SCHED_CLUTCH_INVALID_TIME_32, /* FIXPRI */ 8000, /* FG (8ms)*/ 4000, /* IN (4ms) */ 2000, /* DF (2ms) */ 1000, /* UT (1ms) */ 0 /* BG (0ms) */ };根桶选择逻辑(sched_clutch_root_highest_root_bucket()):
- 找到 deadline 最早的桶(EDF 桶);
- 检查是否有自然优先级顺序更高的桶还有 warp 剩余(通过
scr_unbound_warp_available/scr_bound_warp_available位图查找); - 若有,则选择该桶并打开一个 warp 窗口——warp 窗口期间调度器持续选择这个 warp 桶而忽略更低的桶(
scrb_warped_deadline = timestamp + scrb_warp_remaining); - 当 warp 桶被耗尽(drain)或 warp 窗口到期后(
scrb_warped_deadline <= timestamp),调度器回到按 deadline 顺序调度(此时将该桶的scrb_warp_remaining置 0 并从 warp 可用位图中清除)。
该机制给高层桶提供有界的优势,使它们在突发负载下保持响应性。warp 只在同类型(bound/unbound)根桶之间生效,因为 warp 本质上是相对低 QoS 根桶的调度优势。
AboveUI(FIXPRI)桶特例
FIXPRI 桶包含对延迟极其敏感的线程,被特殊处理(源码注释见 osfmk/kern/sched_clutch.c,实现见sched_clutch_root_unbound_select_aboveui()与sched_clutch_root_bound_select_aboveui()):
- 由于 AboveUI 与 FG timeshare 桶的优先级范围重叠,必须在这两个桶之间维持某种原生优先级顺序;
- 策略:比较两个桶的最高 clutch bucket(unbound 情形),若 AboveUI 桶更高则立即调度它;否则回落到基于 deadline 的调度;
- bound 情形则直接比较两个桶中最高可运行线程的
highq。
这一设计允许 AboveUI 线程获得极低延迟的 CPU 访问,同时支持"高优先级 timeshare 线程与低优先级固定优先级线程竞争"的场景(某些媒体工作负载中可观察到)。由于 timeshare 桶消费 CPU 后优先级会自然下降,该模型为 UI 之上的 timeshare 线程提供了期望行为。
位图与空层次检查
调度桶层还维护一个可运行根桶的位图(scr_unbound_runnable_bitmap/scr_bound_runnable_bitmap),用于快速检查层次是否为空以及根级优先级计算。相关数据结构定义于 osfmk/kern/sched_clutch.h 的struct sched_clutch_root:它维护所有可运行根桶的优先队列(scr_unbound_root_buckets/scr_bound_root_buckets,均为 deadline 最小堆)、根级优先级scr_priority、根级 urgencyscr_urgency、可运行线程总数scr_thr_count,以及绑定/未绑定根桶的存储数组。
为什么这一层选 EDF
文档给出的理由非常明确,可直接用于理解设计取舍:
- 基于 deadline 的调度允许调度器为所有调度桶定义严格的最坏执行延迟上界;
- EDF 算法基于桶的可运行性与选择是动态的;由于所有 deadline 更新计算开销都很低,算法可以在无明显开销的情况下维持最新信息;
- 高效实现"高桶低调度延迟 + 低桶防饿死"的双重目标;
- 桶层调度器最坏情况下只面对固定且数量很小的可运行桶,定义 deadline、warp 等参数非常容易配置。
第二层:Thread Group Level(线程组层)
线程组层决定一个桶内选择哪个线程组执行。线程组(thread group)是随 AMP 调度器引入的机制,代表"为一个特定工作负载服务的线程集合"。每个桶内具有可运行线程的线程组在本层由条目代表,实现中称为clutch bucket(离合桶)。本层目标:在各种用户工作负载之间共享 CPU,且偏好交互式应用而非计算密集型批量负载。
ULE 变体:clutch bucket 优先级队列
线程组层实现的是 FreeBSD ULE 调度器的一种变体:每个有可运行线程的 clutch bucket 作为条目进入一个按 clutch bucket 优先级排序的 runqueue,选择算法直接取最高优先级的 clutch bucket。优先级计算基于三个因素:
- clutch bucket 内最高可运行线程:clutch bucket 维护一个优先队列(
scb_clutchpri_prioq),按线程的 promoted(提升后)或 base 优先级排序——哪个属性使线程有资格进入该 clutch bucket 就用哪个。使用 base 与 sched 两种优先级,让调度器能够尊重来自用户空间的 SPI 优先级指定、turnstile 等优先级继承机制带来的优先级提升,以及其他核心调度器之外的优先级影响机制; - 交互性得分(Interactivity score):基于 clutch bucket 整体"自愿阻塞时间 / CPU 使用时间"的比值计算,让调度器偏好高度交互的线程组而非批量计算型线程组;
- 线程组类型(Thread Group Type):为改善 AMP 设备电池续航,OS 将守护进程线程组标记为 "Efficient"。这些线程组通常代表与用户请求工作负载无直接关系的任务。调度器将其因素计入优先级计算,从而使其排在别的工作之后。
优先级计算细节见下文"Clutch Bucket 优先级计算"一节。数据结构上,struct sched_clutch_bucket(osfmk/kern/sched_clutch.h)保存线程组的 runqueue(scb_thread_runq)、clutchpri 优先队列、桶编号scb_bucket、桶优先级scb_priority与线程数scb_thr_count等;struct sched_clutch_bucket_group(同文件 #L301-L338)则维护该线程组在该调度桶上的 timesharing 属性(优先级 shift、CPU 使用/阻塞数据、interactivity 数据),并内嵌每个 pset 一个的scbg_clutch_buckets[MAX_PSETS]。
runqueue 的两点精细设计
- 插入队首:当 clutch bucket 中的线程被抢占(preempt)时,该 clutch bucket 被插入 runqueue 的队首,使被抢占的线程保持其在队列中的顺序;
- 轮转(rotate):当从 clutch bucket 选出一个线程执行时,runqueue 会把该 clutch bucket 轮转到同优先级层的队尾,从而在同一优先级的多个 clutch bucket 之间高效 round robin——特别是在高度争用、CPU 数量少的系统上。
为什么这一层适合交互性得分算法
- 基于近期行为,它允许线程组之间公平共享 CPU;由于只看近期 CPU 使用历史,能快速适应变化的行为;
- 优先级计算相当廉价,调度器能维护所有线程组的最新信息,从而做出更优决策;
- 线程组为"共同服务于一个用户工作负载的线程"提供了方便的抽象,基于该抽象做调度决策,系统可以做出有趣的选择,例如优先 App 而非 daemon——这通常更有利于系统响应性。
第三层:Thread Level(线程层)
最底层决定一个 clutch bucket 内选择哪个线程执行。clutch bucket 内每个可运行线程作为条目进入按schedpri组织的 runqueue(scb_thread_runq,一个 stable max 优先队列),线程选择算法直接取队列中最高优先级线程。
schedpri基于传统 Mach 调度算法计算,用负载与 CPU 使用量衰减线程优先级。线程衰减模型在这层比在全局调度器更合适,因为负载计算只统计同一 clutch bucket 内的线程。由于同一 clutch bucket 内所有线程属于同一线程组和调度桶,该算法能为 clutch bucket 内延迟敏感的线程提供快速 CPU 访问,而不影响系统中其他无关线程。
实现:Mach timesharing + 每桶 quantum
线程层实现 Mach timesharing 算法:clutch bucket 内所有可运行线程按 schedpri 插入 runqueue;调度器根据 clutch bucket 内可运行线程数与单个线程的 CPU 使用量计算 schedpri。负载信息每个 scheduler tick 更新,线程随 CPU 消耗用其做优先级衰减计算。衰减算法奖励突发型交互线程、惩罚 CPU 密集型线程。
线程被选中运行后获得一个基于其调度桶的 quantum(时间片),静态定义如下(非 OSX 目标,见 osfmk/kern/sched_clutch.c;OSX 目标下 IN/DF 也是 10ms):
static uint32_t sched_clutch_thread_quantum_us[TH_BUCKET_SCHED_MAX] = { 10000, /* FIXPRI (10ms) */ 10000, /* FG (10ms) */ 8000, /* IN (8ms) */ 6000, /* DF (6ms) */ 4000, /* UT (4ms) */ 2000 /* BG (2ms) */ };每桶 quantum 使调度器能够为"被高优先级线程饿死的低优先级线程"界定最坏执行延迟。此外,struct sched_clutch_root(osfmk/kern/sched_clutch.h)的注释还提到根级维护"上次调度的 root bucket"信息以实现桶级 quantum,桶级 quantum 允许低优先级桶即使包含一堆短执行线程也有"公平"机会使用 CPU。
调度器优先级计算
根优先级(Root Priority)计算
调度器为层次维护一个根级优先级,用于做抢占(pre-emption)与线程选择决策;线程插入/移出层次时更新。根级同时维护 urgency 位辅助抢占决策。伪代码:
Root Priority Calculation: * If AboveUI bucket is runnable, * Compare priority of AboveUI highest clutch bucket (CBUI) with Timeshare FG highest clutch bucket (CBFG) * If pri(CBUI) >= pri(CBFG), select CBUI * Otherwise find the (non-AboveUI) highest priority root bucket that is runnable and select its highest clutch bucket * Find the highest priority (promoted or base pri) thread within that clutch bucket and assign that as root priority Root Urgency Calculation: * On thread insertion into the hierarchy, increment the root level urgency based on thread's sched_pri * On thread removal from the hierarchy, decrement the root level urgency based on thread's sched_pri源码中对应sched_clutch_root_priority()与sched_clutch_root_urgency()(声明见 osfmk/kern/sched_clutch.c),根结构scr_priority与scr_urgency均在sched_clutch_root_init()中初始化为NOPRI/ 0(osfmk/kern/sched_clutch.c)。
根桶优先级计算
根桶优先级就是根桶的 deadline:
root-bucket priority = now + WCEL[bucket]即把桶的 WCEL 加到桶变为 runnable 的时间戳上(对应sched_clutch_root_bucket_deadline_calculate(),osfmk/kern/sched_clutch.c)。
Clutch Bucket 优先级计算
如前所述,clutch bucket 优先级由最高可运行线程、交互性得分与线程组类型三个因素决定,伪代码:
* Find the highest runnable thread (promoted or basepri) in the clutch bucket (maxpri) * Check if the thread group for this clutch bucket is marked Efficient. * If not, assign a positive boost value (clutch_boost) * Calculate the ratio of CPU blocked and CPU used for the clutch bucket. * If blocked > used, assign a score (interactivity_score) in the higher range. * Else, assign a score (interactivity_score) in the lower range. * clutch-bucket priority = maxpri + clutch_boost + interactivity_score线程组 boost 的基础在 osfmk/kern/sched_clutch.h:SCHED_CLUTCH_TG_PRI_LOW/MED/HIGH枚举,当前实现给 HIGH 与 MED 线程组小幅 boost,实际效果就是在 AMP 系统上把标记为 "Efficient" 的守护进程线程组降权。交互性得分的实现为sched_clutch_bucket_group_interactivity_score_calculate()(osfmk/kern/sched_clutch.c):对 FIXPRI 桶强制标记为交互(2 * sched_clutch_bucket_group_interactive_pri,因为 AboveUI 根桶选择依赖 clutch bucket 优先级);对其他桶先按 pending 时间与桶负载做 CPU 统计老化(sched_clutch_bucket_group_pending_ageout()),再用 CPU used/blocked 数据算出得分(sched_clutch_interactivity_from_cpu_data())。
CPU 使用/阻塞数据由sched_clutch_bucket_cpu_data_t联合体(osfmk/kern/sched_clutch.h)统一原子维护:64 位平台用unsigned __int128打包scbcd_cpu_used与scbcd_cpu_blocked,32 位平台则用 64 位打包,保证并发更新的一致性。
线程优先级计算
线程优先级基于 Mach timesharing 算法:
* Every scheduler tick, snapshot the load for the clutch bucket * Use the load value to calculate the priority shift values for all threads in the clutch bucket * thread priority = base priority - (thread CPU usage >> priority shift)每个 scheduler tick 对 clutch bucket 负载做快照,用负载值计算桶内所有线程的 priority shift(存储在sched_clutch_bucket_group的scbg_pri_shift,见 osfmk/kern/sched_clutch.h),线程优先级为 base priority 减去其 CPU 使用量右移 shift 位的结果——这正是 Mach 衰减曲线在该层的内化体现,且负载只统计同一 clutch bucket,隔离性由此而来。
关键数据结构与并发模型速览
理解 Clutch Scheduler 还需要了解三个核心对象及其关联(全部定义在 osfmk/kern/sched_clutch.h):
| 结构 | 含义 | 与上层的关系 |
|---|---|---|
struct sched_clutch_root | 层次根,一个 pset 一个 | 管理所有可运行根桶的 EDF 优先队列与位图 |
struct sched_clutch_root_bucket | 根桶 | 所有同一调度桶的 unbound 线程(跨线程组)或 bound 线程;含 clutch bucket runqueue / bound runq、warp 状态与防饿死窗口 |
struct sched_clutch | 一个线程组 1:1 的调度对象 | 内嵌全部sched_clutch_bucket_group[TH_BUCKET_SCHED_MAX] |
struct sched_clutch_bucket_group | 线程组在某调度桶上的聚合 | 维护 timesharing 属性与每 pset 的 clutch bucket |
struct sched_clutch_bucket | clutch bucket(线程组 × 调度桶 × cluster) | 线程 runqueue + clutchpri 队列 + CPU 统计 |
层次结构挂在 pset 上,因此受pset lock保护(sched_clutch_hierarchy_locked_assert()断言pset_assert_locked(root_clutch->scr_pset),osfmk/kern/sched_clutch.c);头文件中用(P)标注 pset lock 保护、(A)标注原子更新、(I)标注仅初始化后不变。跨 cluster 迁移相关的计数(如sched_clutch.sc_thr_count)用原子类型支持。
此外,SCHED_CLUTCH_THREAD_ELIGIBLE(thread)宏(osfmk/kern/sched_clutch.h)规定:绑定到特定处理器(bound_processor != PROCESSOR_NULL)的线程不进入 clutch 层次;在CONFIG_SCHED_EDGE下,cluster bound 线程(ECORE/PCORE only)会走独立的 bound 根桶路径,这也是实现中反复出现 bound/unbound 两套根桶与位图的原因。
从源码验证设计:关键实现位置索引
- 三层架构总述与背景:设计文档 osfmk/kern/sched_clutch.md;
- 根桶 WCEL / warp / 线程 quantum 配置数组:osfmk/kern/sched_clutch.c;
- EDF + warp + AboveUI 特例的根桶选择主流程:
sched_clutch_root_highest_root_bucket(),osfmk/kern/sched_clutch.c; - 根桶 deadline 计算:
sched_clutch_root_bucket_deadline_calculate(),osfmk/kern/sched_clutch.c; - 交互性得分计算:
sched_clutch_bucket_group_interactivity_score_calculate(),osfmk/kern/sched_clutch.c; - 调度桶枚举与含义:osfmk/kern/sched.h;
- 全部核心数据结构与锁协议标注:osfmk/kern/sched_clutch.h。
小结
Clutch Scheduler 用"调度桶 → 线程组 → 线程"三层层次化模型,把调度决策从孤立线程提升到用户工作负载的粒度:顶层 EDF + warp 保证高 QoS 桶的低延迟与低 QoS 桶的防饿死;中间层基于最高线程优先级、交互性得分与线程组类型(Efficient 标记)在应用与守护进程之间公平分配 CPU;底层则沿用 Mach timesharing 衰减模型,但负载计算被隔离在同一 clutch bucket 内,从而同时获得交互响应性与工作负载隔离。这一设计从根本上缓解了传统 Mach 调度器的记账失真、隔离缺失与优先级通胀问题,也解释了现代 Apple 平台上"前台 App 流畅、后台守护不抢 CPU"体验背后的调度层原理。
- 操作系统
- 驱动开发
【免费下载链接】darwin-xnu
Legacy mirror of Darwin Kernel. Replaced by https://github.com/apple-oss-distributions/xnu
相关推荐
Nomad 调度器深度解析:从评估(Evaluation)到分配计划(Plan)的完整调度流水线
Nomad 调度器深度解析:从评估(Evaluation)到分配计划(Plan)的完整调度流水线 本篇技术指南以 scheduler/README.md htt
任务调度云原生运维后端Linux 内核初始化第八部分:调度器(Scheduler)初始化深度解析
Linux 内核初始化第八部分:调度器(Scheduler)初始化深度解析 导读 本文基于 linux insides 项目 Initialization 章节
文档教程操作系统mistral.rs 架构解析:三层分层、引擎线程与连续批处理调度机制
mistral.rs 架构解析:三层分层、引擎线程与连续批处理调度机制 本篇技术指南以 mistral.rs 开发者文档中的 架构说明 https://link
推理引擎模型推理服务AI Agent多模态
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考