第五十二章:调度延迟
结丹圆满涉及内核源码:
一
林小源在调度竞技场中,听到了一个声音——滴答、滴答、滴答。
那不是时钟中断的声音,而是"等待"的声音。一个进程刚刚从睡眠中醒来,它需要 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。
"唤醒抢占降低了交互式进程的延迟,"老者说,"但它也增加了上下文切换的频率。如果唤醒抢占太激进,频繁的上下文切换反而会降低吞吐量。这是一个权衡。"
三
林小源站在走廊的尽头,看着不同应用对延迟的不同需求。
"音频处理需要极低的延迟——低于十毫秒,"老者指着一个房间,"否则会出现杂音。视频播放需要中等延迟——低于三十三毫秒,否则会掉帧。网络服务器需要极低延迟——低于一毫秒,否则会丢包。交互式应用需要较低延迟——低于一百毫秒,否则会感觉卡顿。"
"每种应用的需求都不同,"林小源说。
"没错,"老者说,"调度器不可能同时满足所有需求。低延迟意味着高切换频率,高切换频率意味着低吞吐量。调度器需要在延迟和吞吐量之间找到平衡——这个平衡点取决于应用场景。"
林小源看着老者长袍上的秒表,心中感慨。调度延迟是调度器的敌人,但它也是调度器存在的理由——如果不需要低延迟,就不需要调度器。
"延迟是响应性的度量,"林小源说,"越低越好,但不能没有代价。"
老者露出一丝赞许的微笑:"你理解了。"
/*
* 调度延迟的来源:
*
* 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");#include <stdio.h>
/*
* 调度延迟的来源:
*
* 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;
};
int main() {
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");
return 0;
}道藏笔记
内核启示
调度延迟是进程从变为可运行到实际获得 CPU 的时间。
调度延迟的来源:
- 调度器本身的开销(~5μs)
- 当前进程的时间片未用完(~3ms)
- 更高优先级的进程在运行(不确定)
- CPU 亲和性限制(~100μs)
降低调度延迟的方法:
- 唤醒抢占 — 醒来的进程可以抢占当前进程
- 减小时间片 — 减少等待时间
- 提高进程优先级 — 更容易被选中
- 绑定到空闲的 CPU — 避免等待
唤醒抢占的条件:
- 醒来进程的 < 当前进程的 - 候补阈值
- 保证交互式进程的低延迟
调度延迟是"响应性"的度量——越低越好。
调度延迟之试
从任务变为可运行到真正获得 CPU 的这段等待时间,本章称为什么?