第三十九章:时间片
结丹初期涉及内核源码:
一
林小源在红黑树下观察调度仙子时,注意到一个细节:每个进程的光团上都刻着一个数字,但那不是"剩余时间"——而是一个不断增长的计数器。
"那是什么?"他指着一个 shell 进程光团上的数字问。
调度仙子走过来,看了一眼:"那是它的 slice——时间片。但不要被名字骗了,CFS 的时间片不是固定的。"
她伸出手掌,掌心浮现出一个圆形的光幕。光幕被切成了几个扇形,每个扇形的大小不同。
"在一个 target_latency——目标延迟——内,所有可运行的进程都会获得至少一次运行机会。每个进程的实际运行时间与它的权重成正比。"
林小源看着那个光幕:"target_latency 是多少?"
"通常 6ms。"调度仙子说,"但如果进程数太多,每个进程分到的时间片会小于 min_granularity——0.75ms。这时候,target_latency 会被自动拉长到 nr_running × min_granularity。"
她在光幕上标注了公式:slice = target_latency × (weight_i / total_weight)。
"假设 target_latency 是 6ms,有两个 nice 0 的进程,权重都是 1024,total_weight 是 2048。每个进程分到 3ms。但如果其中一个进程是 nice 10,权重只有 110,那 nice 0 的进程会分到 5.4ms,nice 10 的进程只分到 0.6ms。"
"但 0.6ms 小于 min_granularity。"林小源说。
"对,所以 min_granularity 会兜底。nice 10 的进程至少运行 0.75ms。而 target_latency 也会被拉长,保证每个进程都能分到足够的 CPU 时间。"
二
"还有一个机制,"调度仙子说,"叫做唤醒抢占。"
她指向远处一个正在睡眠的进程。那是一个 vim 编辑器的光团,暗淡而安静,vruntime 的数字停滞不动。
"当用户按下键盘,vim 会被唤醒。这时候,它的 vruntime 会被调整——设置为红黑树中最小的 vruntime。"
"为什么要调整?"林小源问。
"因为 vim 已经睡了很久,"调度仙子解释道,"它的 vruntime 是'过时'的。如果不调整,它醒来的瞬间就会因为 vruntime 太大而排在红黑树的最右边——永远得不到 CPU。"
林小源想起了 shell 进程——它大部分时间都在等待用户输入,一旦输入到来,它需要立即响应。唤醒抢占正是为这种交互式场景设计的。
"但还有更厉害的,"调度仙子继续说,"如果醒来的进程的 vruntime 小于当前正在运行的进程的 vruntime,内核会设置 标志——触发抢占。当前进程被暂停,醒来的进程立即获得 CPU。"
"醒来比继续运行更有优势。"林小源喃喃道。
"这是设计的意图。"调度仙子点头,"交互式进程大部分时间在睡眠中等待。当它们醒来时,需要低延迟的响应。唤醒抢占保证了这一点。"
三
林小源坐在红黑树下,思考着时间片的设计。
"时间片太短会怎样?"他问。
调度仙子在他身旁坐下,长裙铺展在漆黑的地面上:"频繁的上下文切换。每次切换都有开销——保存寄存器、切换页表、刷新 TLB。如果时间片太短,CPU 会把大部分时间花在切换上,而不是执行实际的工作。"
"时间片太长呢?"
"响应延迟高。"她说,"交互式进程会卡顿——用户按下键盘,shell 要等很久才能响应。"
"所以 min_granularity 是一个折中。"
"对。"调度仙子说,"0.75ms 足够短,不会让交互式进程等太久。又足够长,不会导致频繁的上下文切换。这是内核开发者经过大量实验得出的经验值。"
林小源沉默了一会儿,然后问:"你有没有想过,如果 min_granularity 的值不对呢?"
调度仙子看了他一眼,目光中带着一丝惊讶:"你在质疑内核的默认参数?"
"不是质疑,"林小源说,"我只是想知道,这些参数是固定的,还是可以调整的。"
"可以调整。"调度仙子说,"通过 /proc/sys/kernel/sched_min_granularity_ns。但我不建议你去改它——除非你非常清楚自己在做什么。默认参数是经过大量测试和调优的,适合绝大多数场景。"
林小源点了点头。他想起了自己在内核中看到的许多默认参数——它们看起来像随意的数字,但实际上每一个都是内核开发者多年经验的结晶。
/*
* CFS 的时间片分配:
*
* target_latency = 目标延迟(调度周期)
* 通常为 6ms(可配置)
* 如果进程数 > target_latency / min_granularity,
* 则 target_latency = 进程数 × min_granularity
*
* 进程的时间片:
* slice = target_latency × (weight_i / total_weight)
*
* 例如:
* target_latency = 6ms
* 进程 A: weight = 1024 (nice 0)
* 进程 B: weight = 1024 (nice 0)
* total_weight = 2048
* A 的时间片 = 6ms × (1024/2048) = 3ms
* B 的时间片 = 6ms × (1024/2048) = 3ms
*/
#define TARGET_LATENCY 6000000 /* 6ms = 6,000,000 ns */
#define MIN_GRANULARITY 750000 /* 0.75ms = 750,000 ns */
struct sched_entity {
int pid;
char comm[16];
int nice;
int weight;
};
int nice_to_weight(int nice) {
static int weights[] = {
88761, 71755, 56483, 46273, 36291,
29154, 23254, 18705, 14949, 11916,
9548, 7620, 6100, 4904, 3906,
3121, 2501, 1991, 1586, 1277,
1024, 820, 655, 526, 423,
335, 272, 215, 172, 137,
110, 87, 70, 56, 45,
36, 29, 23, 18, 15,
};
return weights[nice + 20];
}
printf("=== 时间片的分配 ===\n\n");
struct sched_entity procs[] = {
{ 100, "shell", 0, 0 },
{ 200, "batch", 5, 0 },
{ 300, "vim", 0, 0 },
};
int nr = sizeof(procs) / sizeof(procs[0]);
/* 计算权重 */
int total_weight = 0;
for (int i = 0; i < nr; i++) {
procs[i].weight = nice_to_weight(procs[i].nice);
total_weight += procs[i].weight;
}
/* 计算 target_latency */
int target_latency = TARGET_LATENCY;
if (nr > target_latency / MIN_GRANULARITY) {
target_latency = nr * MIN_GRANULARITY;
}
printf("目标延迟 (target_latency): %d ns (%.2f ms)\n",
target_latency, target_latency / 1000000.0);
printf("最小粒度 (min_granularity): %d ns (%.2f ms)\n\n",
MIN_GRANULARITY, MIN_GRANULARITY / 1000000.0);
printf("%-8s %-6s %-8s %-12s\n", "进程", "nice", "权重", "时间片");
printf("%-8s %-6s %-8s %-12s\n", "---", "---", "---", "---");
for (int i = 0; i < nr; i++) {
unsigned long slice = (unsigned long)target_latency *
procs[i].weight / total_weight;
printf("%-8s %-6d %-8d %-10.2f ms\n",
procs[i].comm, procs[i].nice, procs[i].weight,
slice / 1000000.0);
}
printf("\n--- 唤醒抢占 ---\n");
printf("当一个进程从睡眠中醒来时:\n");
printf(" 1. 它的 vruntime 被调整为 min_vruntime\n");
printf(" 2. 如果它的 vruntime < 当前运行进程的 vruntime\n");
printf(" 3. 设置 TIF_NEED_RESCHED 标志\n");
printf(" 4. 当前进程被抢占\n\n");
printf("--- 时间片用完 ---\n");
printf("当进程的时间片用完时:\n");
printf(" 1. scheduler_tick() 更新 vruntime\n");
printf(" 2. 检查是否需要抢占\n");
printf(" 3. 如果有更优的进程,设置 TIF_NEED_RESCHED\n");
printf(" 4. 在返回用户态时调用 schedule()\n");#include <stdio.h>
/*
* CFS 的时间片分配:
*
* target_latency = 目标延迟(调度周期)
* 通常为 6ms(可配置)
* 如果进程数 > target_latency / min_granularity,
* 则 target_latency = 进程数 × min_granularity
*
* 进程的时间片:
* slice = target_latency × (weight_i / total_weight)
*
* 例如:
* target_latency = 6ms
* 进程 A: weight = 1024 (nice 0)
* 进程 B: weight = 1024 (nice 0)
* total_weight = 2048
* A 的时间片 = 6ms × (1024/2048) = 3ms
* B 的时间片 = 6ms × (1024/2048) = 3ms
*/
#define TARGET_LATENCY 6000000 /* 6ms = 6,000,000 ns */
#define MIN_GRANULARITY 750000 /* 0.75ms = 750,000 ns */
struct sched_entity {
int pid;
char comm[16];
int nice;
int weight;
};
int nice_to_weight(int nice) {
static int weights[] = {
88761, 71755, 56483, 46273, 36291,
29154, 23254, 18705, 14949, 11916,
9548, 7620, 6100, 4904, 3906,
3121, 2501, 1991, 1586, 1277,
1024, 820, 655, 526, 423,
335, 272, 215, 172, 137,
110, 87, 70, 56, 45,
36, 29, 23, 18, 15,
};
return weights[nice + 20];
}
int main() {
printf("=== 时间片的分配 ===\n\n");
struct sched_entity procs[] = {
{ 100, "shell", 0, 0 },
{ 200, "batch", 5, 0 },
{ 300, "vim", 0, 0 },
};
int nr = sizeof(procs) / sizeof(procs[0]);
/* 计算权重 */
int total_weight = 0;
for (int i = 0; i < nr; i++) {
procs[i].weight = nice_to_weight(procs[i].nice);
total_weight += procs[i].weight;
}
/* 计算 target_latency */
int target_latency = TARGET_LATENCY;
if (nr > target_latency / MIN_GRANULARITY) {
target_latency = nr * MIN_GRANULARITY;
}
printf("目标延迟 (target_latency): %d ns (%.2f ms)\n",
target_latency, target_latency / 1000000.0);
printf("最小粒度 (min_granularity): %d ns (%.2f ms)\n\n",
MIN_GRANULARITY, MIN_GRANULARITY / 1000000.0);
printf("%-8s %-6s %-8s %-12s\n", "进程", "nice", "权重", "时间片");
printf("%-8s %-6s %-8s %-12s\n", "---", "---", "---", "---");
for (int i = 0; i < nr; i++) {
unsigned long slice = (unsigned long)target_latency *
procs[i].weight / total_weight;
printf("%-8s %-6d %-8d %-10.2f ms\n",
procs[i].comm, procs[i].nice, procs[i].weight,
slice / 1000000.0);
}
printf("\n--- 唤醒抢占 ---\n");
printf("当一个进程从睡眠中醒来时:\n");
printf(" 1. 它的 vruntime 被调整为 min_vruntime\n");
printf(" 2. 如果它的 vruntime < 当前运行进程的 vruntime\n");
printf(" 3. 设置 TIF_NEED_RESCHED 标志\n");
printf(" 4. 当前进程被抢占\n\n");
printf("--- 时间片用完 ---\n");
printf("当进程的时间片用完时:\n");
printf(" 1. scheduler_tick() 更新 vruntime\n");
printf(" 2. 检查是否需要抢占\n");
printf(" 3. 如果有更优的进程,设置 TIF_NEED_RESCHED\n");
printf(" 4. 在返回用户态时调用 schedule()\n");
return 0;
}道藏笔记
内核启示
CFS 不使用固定时间片——按权重分配。
时间片计算:
slice = target_latency × (weight_i / total_weight)关键参数:
target_latency— 目标延迟(调度周期),通常 6msmin_granularity— 最小粒度,通常 0.75ms- — 进程权重(由 nice 值决定)
时间片用完时:
scheduler_tick()更新- 检查是否需要抢占
- 设置 标志
- 在返回用户态时调用
唤醒抢占:
- 醒来的进程 被调整为
- 如果 < 当前进程的 ,触发抢占
- 保证交互式进程的低延迟
时间片不是固定的——它是按权重分配的比例。
时间片之试
本章说明 CFS 不再依赖固定时间片,而是用哪一个虚拟运行时间衡量公平?