找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2646

积分

0

好友

342

主题
发表于 2026-2-12 08:34:30 | 查看: 31| 回复: 0

1965年,假设你是一名操作系统工程师。当时,计算机正从单任务时代迈向多任务时代——一台机器可以同时运行多个程序了。

但你立刻面临一个棘手的问题:只有一个CPU,却有10个程序都想运行,到底该让谁先执行?

这问题看似简单,实则复杂。如果处理不好,各种古怪的问题都会涌现:

  • 重要的程序等待太久,无法推进。
  • 用户的交互操作(如键盘输入)响应迟缓。
  • 某些程序可能永远无法获得CPU时间。

为此,你必须设计一个“调度器”来分配宝贵的CPU时间。

第一次尝试:按到达顺序排队

最朴素的想法是:先来先服务(FIFO)。就像在银行排队,谁先到谁先办业务。

void schedule()
{
    if (head != tail) {
        struct task *t = &queue[head++];
        t->run();  // 执行任务
    }
}

这个调度器实现简单,你很快将其部署。但运行半天后,用户开始抱怨:敲击键盘输入字符后,要等很久才有反应。

你排查发现,系统中有一个需要运行10分钟的科学计算程序。只要它开始运行,调度器就会按排队顺序让它一直执行完,其他任务(包括响应用户输入的终端)根本得不到机会。

你意识到:简单的排队机制,根本无法满足真实的计算需求!

引入优先级机制

经过思考,你想出了一个改进方案:给每个任务分配一个优先级数字,数字越小代表越重要,总是让最重要的任务先执行。

void schedule()
{
    struct task *highest = NULL;

    // 找到数字最小(最重要)的任务
    for (int i = 0; i < num_tasks; i++) {
        if (highest == NULL || tasks[i].priority < highest->priority) {
            highest = &tasks[i];
        }
    }

    if (highest) {
        highest->run();
    }
}

这个数字就是优先级(Priority)

新系统部署后,你期待着它能解决先前的问题。但仅半天,你就收到了严重的错误报告:一个日志写入程序已经等待了2小时,仍未获得CPU时间。

你排查发现,一个视频解码程序拥有更高的优先级,一直在运行。调度器总是选择它,导致低优先级的日志程序永远轮不到!

这就是所谓的饥饿(Starvation)问题:低优先级任务可能永远得不到执行机会。

1990年代:给每个任务分配固定配额

你继续思考:问题的根源是什么?关键在于,高优先级任务会无限期地独占CPU!

于是你想出了新方法:给每个任务分配一个固定的“运行配额”,配额用完后就必须让出CPU,给其他任务机会。

void schedule()
{
    struct task *current = get_highest_priority_task();

    if (current->quota > 0) {
        current->run();
        current->quota -= 1;  // 消耗1ms配额
    } else {
        // 配额用完,放入等待队列
        current->quota = 计算新配额(current->priority);
        移到等待队列(current);
    }
}

这个“运行配额”,就是时间片(Time Slice)

新系统上线后,监控数据显示:低优先级任务终于能获得CPU时间了!饥饿问题似乎得到了解决。

但很快,你发现了一个新的恼人问题:敲击键盘后,字符在屏幕上显示会有明显的卡顿感。

你分析原因:终端程序大部分时间都在等待用户输入(处于睡眠状态)。只有当用户敲键盘时,它才需要CPU,且只需几微秒就能处理完。但如果此时有其他任务正在排队,它就必须等待。这类交互式任务本应被优先响应,但你的调度器根本无法有效区分它们。

你尝试修补:增加一套启发式规则来猜测哪些任务是交互式的,并给予更高优先级。

void 检测交互式任务(struct task *t) {
    // 如果任务经常主动让出CPU(等待IO),就认为它是交互式的
    if (t->睡眠时间 > t->运行时间) {
        t->is_interactive = 1;
        t->priority -= 5;   // 给它更高优先级
    }
}

这个方法有时有效,有时却会误判。例如,一个频繁读写磁盘的数据库程序被误判为“交互式任务”,优先级被提高,反而饿死了真正的终端程序。

你不断增加判断条件:睡眠次数、平均运行时间、IO等待比例……代码越来越复杂,效果却时好时坏。

你开始怀疑:是不是这整个设计思路,从根本上就有问题?

从头开始思考:究竟想要什么?

时间来到2004年,在一个深夜,你决定推倒所有过往设计,从最根本的问题重新开始:我到底想要一个什么样的调度器?

你在纸上写下答案:我想要每个任务都获得公平的CPU时间。

什么是“公平”?你设想了一个理想情况:如果有10个同等重要的任务,它们应该各自获得10%的CPU时间。

那么,为什么不直接追踪每个任务“已经获得了多少CPU时间”?总是让“获得时间最少的”任务先执行,不就自然实现公平了吗?

你在白板上画出新的设计:

struct task {
    int pid;
    uint64 runtime;  // 这个任务已经运行了多久
};

void schedule()
{
    // 找到运行时间最短的任务
    struct task *next = 找到runtime最小的任务();

    uint64 start = now();
    next->run();
    uint64 delta = now() - start;

    // 更新运行时间
    next->runtime += delta;
}

思路非常清晰:

  • 不再预先分配固定的时间片配额。
  • 只记录每个任务 “已经运行了多久”
  • 总是让“累计运行时间最短”的任务先执行。

你将这个原型部署到测试系统,效果惊人:10个任务的CPU占用时间差异变得非常小!

接着,你做了一个关键测试:在后台编译一个大型项目的同时,打开终端输入命令。

你屏住呼吸,敲下键盘——

字符瞬间就显示出来了!

你愣了一下,随即恍然大悟:

编译程序:一直在疯狂使用CPU
  → runtime 增长很快 → 比如已经累积到 5000ms

终端程序:大部分时间在等待你敲键盘(睡眠状态)
  → runtime 几乎不增长 → 比如只有 3ms

当你敲下键盘,终端程序从睡眠中醒来。调度器发现它的运行时间只有3ms,而编译程序已经积累了5000ms——于是终端程序立刻获得了CPU!

你突然明白:之前那套复杂且不可靠的“启发式交互式检测”,在这套新方案里根本不需要了!

不需要猜测哪个任务是交互式的,不需要任何特殊规则。IO密集型任务因为频繁睡眠,其“已运行时间”天然增长得慢,醒来后自然就排在队列最前面。这不是一个刻意设计的特性,而是 “追踪已用时间并补偿最少者” 这一核心思路带来的自然结果。

引入“虚拟时间”处理优先级

然而,现实中的任务确实需要区分重要程度。比如,数据库进程理应比日志备份进程获得更多的CPU时间。

该如何在公平的基础上实现优先级呢?能否在“记账”时做些手脚?

这就像不同货币有不同的汇率:

  • 重要任务:实际运行10秒,账本上只记1秒(“汇率”高,时间增值慢)。
  • 普通任务:实际运行1秒,账本上记1秒。
  • 不重要任务:实际运行1秒,账本上记10秒(“汇率”低,时间增值快)。

这样,重要任务的“账面时间”增长得慢,自然会更频繁地被调度——并且,这仍然遵循同一套“谁的时间少谁先上”的规则,无需额外机制!

你修改代码,为每个任务引入一个“权重”作为汇率:

struct task {
    int pid;
    int nice;        // 用户设置的重要程度(-20到+19)
    u64 vruntime;    // 换算后的运行时间
    int weight;      // 权重(由nice值决定)
};

void schedule()
{
    struct task *next = 找到vruntime最小的任务();

    u64 start = now();
    next->run();
    u64 delta = now() - start;  // 实际运行时间

    // 用权重换算后再记账
    next->vruntime += delta * 1024 / next->weight;
}

这个 “换算后的运行时间”,就是虚拟运行时间(Virtual Runtime,简称 vruntime)

至此,一套全新的调度机制诞生了。它不再基于固定的时间片轮转,也不再依赖不可靠的启发式规则,而是通过持续追踪和比较每个任务的 vruntime,来逼近一种理想的公平状态。这套机制,就是如今 Linux 内核 中大名鼎鼎的 完全公平调度器(Completely Fair Scheduler, CFS)

从先来先服务的朴素,到优先级的引入,再到时间片轮转的修补,最终回归“公平记账”的本质,CFS的演进历程深刻地揭示了操作系统核心机制的设计哲学。想了解更多关于 操作系统 和底层原理的深度讨论,欢迎访问云栈社区,与更多开发者一同探索技术的本质。其中,进程调度计算机基础领域永恒的话题之一。




上一篇:智谱AI GLM-5大模型正式发布:编程能力对标Claude Opus,开源SOTA
下一篇:2026年五大Arch Linux发行版推荐:兼具高性能与惊艳桌面的选择
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-23 12:58 , Processed in 0.675361 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表