2. Linux 内核调度器基础:CFS 调度器原理、时间片、nice 值、优先级映射关系

在 Android 系统中,CPU 资源的分配由 Linux 内核的调度器负责。从 2.6.23 内核开始,完全公平调度器(CFS,Completely Fair Scheduler)成为默认调度器。CFS 的设计哲学是:为每个任务提供对 CPU 时间的“公平”分配,而不是依赖传统的时间片轮转。理解 CFS 是进行 Android 内核调度调优的前提。

2.1 CFS 调度器核心原理

CFS 摒弃了传统 Unix 调度器中固定时间片和优先级计算复杂度的做法。它基于一个简单的模型:在一个理想的、完全多任务处理的 CPU 上,每个任务应该获得 1/N 的 CPU 时间(N 为可运行任务数)。CFS 通过 虚拟运行时间(vruntime) 来逼近这个理想模型。

核心概念:vruntime
vruntime 是 CFS 为每个可运行任务维护的一个度量值,表示任务在 CPU 上实际运行的时间,但会根据任务的优先级(nice 值)进行加权。vruntime 越小,表示任务受到的“不公平”越多,因此越需要被调度执行。

CFS 使用红黑树(Red-Black Tree)来组织所有可运行的任务。红黑树是一棵自平衡的二叉搜索树,其键值就是任务的 vruntime。调度器每次选择树的最左节点(即 vruntime 最小的任务)来运行。这种设计使得调度决策的时间复杂度为 O(log N),非常高效。

CFS 调度器核心架构图 CFS 调度器核心架构图 红黑树(按 vruntime 排序) vruntime: 100 vruntime: 80 vruntime: 120 vruntime: 60 ← 下一个运行 调度决策流程 1. 从红黑树取最左节点 2. 将任务切换至 CPU 运行 3. 任务运行时,vruntime 递增 4. 时钟中断触发调度点 5. 更新 vruntime 并重新入树 6. 再次选择最左节点

2.2 时间片与调度周期

CFS 没有传统意义上的固定时间片。它引入了 调度周期(sched_latency) 的概念。调度周期是 CFS 保证每个可运行任务至少运行一次的时间窗口。在这个周期内,所有任务按权重比例分享 CPU 时间。

每个任务获得的时间片长度由以下公式决定:

任务时间片 = 调度周期 × (任务权重 / 所有可运行任务总权重)

当可运行任务数量增多时,如果仍然使用固定的调度周期,每个任务获得的时间片会变得非常小,导致上下文切换开销过大。因此,CFS 引入了 最小粒度(min_granularity) 的概念。当计算出的时间片小于最小粒度时,CFS 会按比例放大调度周期,以确保每个任务至少获得最小粒度的运行时间。

参数 默认值(Android 常见内核) 说明
sched_latency_ns 6,000,000 (6ms) 目标调度周期。任务数少时,所有任务在此周期内至少运行一次。
sched_min_granularity_ns 750,000 (0.75ms) 任务运行的最小时间片。防止上下文切换过于频繁。
sched_wakeup_granularity_ns 1,000,000 (1ms) 唤醒抢占粒度。控制被唤醒任务能否抢占当前运行任务。
调优提示: 在 Android 中,对于延迟敏感的 UI 线程,可以适当减小 sched_min_granularity_ns,让调度器更频繁地切换回 UI 线程,减少卡顿。但过小的值会增加上下文切换开销。

2.3 nice 值与优先级映射

在 Linux 中,用户态通过 nice 值 来影响任务的优先级。nice 值的范围是 -20 到 +19,默认值为 0。nice 值越小,优先级越高;nice 值越大,优先级越低。

CFS 不直接使用 nice 值,而是将其映射为 权重(weight)。权重决定了任务在调度周期内获得 CPU 时间的比例。内核维护了一个从 nice 值到权重的映射表。

nice 值与权重映射关系图 nice 值与 CFS 权重映射关系 nice 值范围:-20(最高优先级) → 0(默认) → +19(最低优先级) CFS 权重范围:88761(nice=-20) → 1024(nice=0) → 15(nice=+19) nice = -20 权重 = 88761 CPU 占比最高 nice = 0 权重 = 1024 默认优先级 nice = +19 权重 = 15 CPU 占比最低

内核中实际的权重映射表(位于 kernel/sched/core.c)如下:

static const int prio_to_weight[40] = {
 /* -20 */ 88761, 71755, 56483, 46273, 36291,
 /* -15 */ 29154, 23254, 18705, 14949, 11916,
 /* -10 */  9548,  7620,  6100,  4904,  3906,
 /*  -5 */  3121,  2501,  1991,  1586,  1277,
 /*   0 */  1024,   820,   655,   526,   423,
 /*   5 */   335,   272,   215,   172,   137,
 /*  10 */   110,    87,    70,    56,    45,
 /*  15 */    36,    29,    23,    18,    15,
};
重要理解: nice 值的差值并不代表 CPU 时间的线性差异。例如,nice 0 的权重是 1024,nice -5 的权重是 3121。这意味着一个 nice -5 的任务获得的 CPU 时间大约是 nice 0 任务的 3 倍(3121/1024 ≈ 3.05)。在 Android 中,前台应用的主线程通常被设置为 nice -2 或 -3,以获取更多的 CPU 资源。

2.4 优先级映射关系:从 nice 到调度实体

CFS 将 nice 值映射为权重后,进一步通过 调度实体(sched_entity) 来管理每个任务。调度实体是 CFS 调度的基本单位,它包含了 vruntime、权重等关键信息。

整个映射链路如下:

  1. 用户态设置 nice 值:通过 setpriority() 系统调用或 renice 命令。
  2. 内核转换为静态优先级:static_prio = MAX_RT_PRIO + nice + 20(其中 MAX_RT_PRIO 通常为 100)。
  3. 静态优先级映射为权重:通过 prio_to_weight[] 数组查找。
  4. 权重影响 vruntime 增长速率:vruntime += 实际运行时间 × (NICE_0_LOAD / 任务权重)。权重越大,vruntime 增长越慢,任务越容易被调度器选中。
优先级映射链路图 从 nice 值到 CFS 调度决策的完整映射链路 用户态 nice 值 -20 ~ +19 静态优先级 100 ~ 139 CFS 权重 15 ~ 88761 vruntime 增长速率 权重越大,增长越慢 最终效果:nice 值越小 → 权重越大 → vruntime 增长越慢 → 被调度器选中的概率越高

2.5 与 Android 性能调优的关联

在 Android 系统中,理解 CFS 的这些基础机制对于以下场景至关重要:

  • UI 线程响应优化:通过调整 UI 线程的 nice 值(通常设为 -2 或 -3),使其获得更大的权重,从而在 CPU 竞争中获得更多时间片,减少渲染卡顿。
  • 后台任务限制:将后台批量任务(如日志同步、数据备份)的 nice 值调高(如 +5 到 +10),降低其对前台交互的影响。
  • 驱动中断线程化:对于需要快速响应的驱动,可以将其中断处理线程的 nice 值设为负值,确保其能抢占普通用户态任务。
  • cgroup 与调度组:Android 使用 cgroup 对进程分组,CFS 的调度实体可以是单个任务,也可以是一个任务组。通过 cpu.shares 可以控制组间的 CPU 分配比例。
关键指标总结:
概念 对 Android 性能的影响
vruntime 决定下一个运行的任务。UI 线程需要较小的 vruntime 以快速获得 CPU。
调度周期 影响任务切换频率。过短增加开销,过长导致响应延迟。
nice 值 / 权重 直接控制 CPU 资源分配比例。是调优的主要手段。
最小粒度 防止任务切换过于频繁。对于微秒级响应的驱动,可能需要调小此值。