Skip to content

第三十九章:时间片

结丹初期

涉及内核源码:

林小源在红黑树下观察调度仙子时,注意到一个细节:每个进程的光团上都刻着一个数字,但那不是"剩余时间"——而是一个不断增长的计数器。

"那是什么?"他指着一个 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。但我不建议你去改它——除非你非常清楚自己在做什么。默认参数是经过大量测试和调优的,适合绝大多数场景。"

林小源点了点头。他想起了自己在内核中看到的许多默认参数——它们看起来像随意的数字,但实际上每一个都是内核开发者多年经验的结晶。


c
/*
 * 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");

道藏笔记

内核启示

CFS 不使用固定时间片——按权重分配。

时间片计算:

slice = target_latency × (weight_i / total_weight)

关键参数:

  • target_latency — 目标延迟(调度周期),通常 6ms
  • min_granularity — 最小粒度,通常 0.75ms
  • — 进程权重(由 nice 值决定)

时间片用完时:

  1. scheduler_tick() 更新
  2. 检查是否需要抢占
  3. 设置 标志
  4. 在返回用户态时调用

唤醒抢占:

  1. 醒来的进程 被调整为
  2. 如果 < 当前进程的 ,触发抢占
  3. 保证交互式进程的低延迟

时间片不是固定的——它是按权重分配的比例。


破关试炼

时间片之试

本章说明 CFS 不再依赖固定时间片,而是用哪一个虚拟运行时间衡量公平?

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

以修仙之名,悟内核之道