Skip to content

第五十二章:调度延迟

结丹圆满

涉及内核源码:

林小源在调度竞技场中,听到了一个声音——滴答、滴答、滴答。

那不是时钟中断的声音,而是"等待"的声音。一个进程刚刚从睡眠中醒来,它需要 CPU,但 CPU 正被另一个进程占据。它在等待。

一个身穿灰色长袍的老者从暗处走出。他的长袍上绣着一个巨大的秒表,秒表的指针在飞速旋转。他的脸上刻满了皱纹,每一道皱纹都代表着一次漫长的等待。

"我是调度延迟,"老者说,"从进程变为可运行到它实际获得 CPU 的时间。我是调度器的敌人——越低越好,但永远无法消除。"

林小源看着老者,问道:"延迟从哪里来?"

老者抬起手,掌心浮现出四道光芒:"第一,调度器本身的开销——pick_next_task() 和 context_switch() 的执行时间,通常一到十微秒。第二,当前进程的时间片未用完——需要等到时钟中断或进程主动让出,通常零到六毫秒。第三,更高优先级的进程在运行——需要等到高优先级进程让出 CPU,时间不确定。第四,CPU 亲和性限制——亲和的 CPU 都在忙,需要等待负载均衡。"

林小源跟着老者走过一条长长的走廊。走廊的两侧是无数个房间,每个房间里都有一个进程在等待。

"有没有办法降低调度延迟?"林小源问。

老者停下脚步,指着一个房间里的进程:"看,这是一个 shell 进程。用户刚刚敲了一个按键,shell 需要立即响应。如果它要等当前进程的时间片用完——可能六毫秒——用户就会感到卡顿。"

"那怎么办?"

"唤醒抢占,"老者说,"当一个进程从睡眠中醒来时,调度器会检查它是否应该抢占当前进程。如果醒来的进程的 vruntime 小于当前进程的 vruntime 减去一个阈值,就设置 TIF_NEED_RESCHED,当前进程被抢占,醒来的进程立即获得 CPU。"

林小源看着那个 shell 进程——它的 vruntime 很小,因为它大部分时间在睡眠。而当前进程的 vruntime 很大,因为它一直在运行。条件满足,TIF_NEED_RESCHED 被设置,当前进程被抢占,shell 进程获得了 CPU。

"唤醒抢占降低了交互式进程的延迟,"老者说,"但它也增加了上下文切换的频率。如果唤醒抢占太激进,频繁的上下文切换反而会降低吞吐量。这是一个权衡。"

林小源站在走廊的尽头,看着不同应用对延迟的不同需求。

"音频处理需要极低的延迟——低于十毫秒,"老者指着一个房间,"否则会出现杂音。视频播放需要中等延迟——低于三十三毫秒,否则会掉帧。网络服务器需要极低延迟——低于一毫秒,否则会丢包。交互式应用需要较低延迟——低于一百毫秒,否则会感觉卡顿。"

"每种应用的需求都不同,"林小源说。

"没错,"老者说,"调度器不可能同时满足所有需求。低延迟意味着高切换频率,高切换频率意味着低吞吐量。调度器需要在延迟和吞吐量之间找到平衡——这个平衡点取决于应用场景。"

林小源看着老者长袍上的秒表,心中感慨。调度延迟是调度器的敌人,但它也是调度器存在的理由——如果不需要低延迟,就不需要调度器。

"延迟是响应性的度量,"林小源说,"越低越好,但不能没有代价。"

老者露出一丝赞许的微笑:"你理解了。"


c
/*
 * 调度延迟的来源:
 *
 * 1. 调度器本身的开销
 *    pick_next_task() + context_switch()
 *    通常 1-10 微秒
 *
 * 2. 当前进程的时间片未用完
 *    需要等到时钟中断或进程主动让出
 *    通常 0-6 毫秒
 *
 * 3. 更高优先级的进程在运行
 *    需要等到高优先级进程让出 CPU
 *    不确定
 *
 * 4. CPU 亲和性限制
 *    亲和的 CPU 都在忙
 *    需要等待负载均衡
 *
 * 优化调度延迟的方法:
 *   - 唤醒抢占(wake-up preemption)
 *   - 减小时间片
 *   - 提高进程优先级
 *   - 绑定到空闲的 CPU
 */

struct latency_source {
    const char *name;
    unsigned long typical_ns;
    unsigned long worst_ns;
};

printf("=== 调度延迟 ===\n\n");

struct latency_source sources[] = {
    { "调度器开销",          5000,     20000 },
    { "时间片等待",        300000,   6000000 },
    { "高优先级进程运行", 1000000, 100000000 },
    { "CPU 亲和性限制",   100000,   1000000 },
};
int nr = sizeof(sources) / sizeof(sources[0]);

printf("调度延迟的来源:\n");
printf("%-20s %-15s %-15s\n", "来源", "典型延迟", "最坏延迟");
printf("%-20s %-15s %-15s\n", "---", "---", "---");

for (int i = 0; i < nr; i++) {
    printf("%-20s %-12.2f ms %-12.2f ms\n",
           sources[i].name,
           sources[i].typical_ns / 1000000.0,
           sources[i].worst_ns / 1000000.0);
}

printf("\n--- 唤醒抢占 ---\n");
printf("当一个进程从睡眠中醒来时:\n");
printf("  1. 计算醒来的进程是否应该抢占当前进程\n");
printf("  2. 如果是,设置 TIF_NEED_RESCHED\n");
printf("  3. 当前进程被抢占\n");
printf("  4. 醒来的进程立即获得 CPU\n\n");
printf("唤醒抢占的条件:\n");
printf("  醒来进程的 vruntime < 当前进程的 vruntime - 候补阈值\n");
printf("  这保证了交互式进程的低延迟\n");

printf("\n--- 延迟敏感的应用 ---\n");
printf("音频处理: 延迟 < 10ms\n");
printf("视频播放: 延迟 < 33ms\n");
printf("网络服务器: 延迟 < 1ms\n");
printf("交互式应用: 延迟 < 100ms\n");

道藏笔记

内核启示

调度延迟是进程从变为可运行到实际获得 CPU 的时间。

调度延迟的来源:

  1. 调度器本身的开销(~5μs)
  2. 当前进程的时间片未用完(~3ms)
  3. 更高优先级的进程在运行(不确定)
  4. CPU 亲和性限制(~100μs)

降低调度延迟的方法:

  • 唤醒抢占 — 醒来的进程可以抢占当前进程
  • 减小时间片 — 减少等待时间
  • 提高进程优先级 — 更容易被选中
  • 绑定到空闲的 CPU — 避免等待

唤醒抢占的条件:

  • 醒来进程的 < 当前进程的 - 候补阈值
  • 保证交互式进程的低延迟

调度延迟是"响应性"的度量——越低越好。


破关试炼

调度延迟之试

从任务变为可运行到真正获得 CPU 的这段等待时间,本章称为什么?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道