03.04 CPU 调度:可运行、等待与 IM 连接负载
返回第三卷 · 前置:03.03 Linux 进程与资源 · IM 学习路线
03.04 CPU 调度:可运行、等待与 IM 连接负载
DeepTutor 初稿经技术和教学审阅后的静态课程。时间线、表格、CPU 数字和命令结果都是带假设的纸上模型;本次没有运行 Go、Linux 调度实验、性能基准、站点构建或 IM 服务。
一个看似矛盾的 IM 现象
03.02 已区分进程、线程、goroutine 与系统调用;03.03 已学会用 ps 看一次进程状态快照。现在设想未来的教学 IM 网关有许多空闲连接,CPU 使用不高;另一次只有少量连接,却因群消息处理而 CPU 很忙。连接数为什么不能直接当作“正在占用 CPU 的任务数”?用户说“消息慢”,又怎样判断是在等 CPU、等 I/O、等锁,还是等远端响应?
本章先讲操作系统调度的最小模型,再把它迁移到未来 IM 服务。当前课程已经实现和讨论的仍是虚构本地工具;网关、群聊扇出与连接行为只是后续项目的需求情景,不表示当前项目或 OpenIM 某个版本实际采用了这里的线程模型。所有 c-a/u-a/m-a 都是教学身份。
| 第一次阅读 | 第二次阅读 | 个人练习要留下什么 |
|---|---|---|
| 状态、调度选择、单核时间线 | 指标、多核、Go 运行时层次与业务诊断 | 两张可复核时间线和两种故障假设 |
先修为 03.02 的进程/线程和 03.03 的只读进程观察;不要求先会编写 goroutine、网络服务或性能工具。03.09 才系统讲就绪通知与阻塞 I/O,05 卷讲 Go 并发与运行时,11 卷讲真实观测与容量评估。这里的数字模型不会替代未来测量。
一、连接存在,不表示有线程正在用 CPU
把一个执行任务看成随时间改变的状态:
新任务 → 可运行(有工作,等 CPU) → 正在运行(此刻占一个 CPU 执行槽)
↑ │
└──── 被抢占或让出 ─────┘
↑ │
└──── 事件完成后唤醒 ← 等待事件正在运行指它此刻得到 CPU 执行指令;可运行指已有工作、具备运行条件,但可能还在等 CPU;等待指当前无法继续,需 I/O、定时器、锁条件或其他事件。等待的任务不是在 CPU 可运行队列中持续“烧掉一个核心”。一条网络连接作为描述符和缓冲等资源可以存在很久,而处理它的任务可能大部分时间在等待数据;连接的实际线程/goroutine组织方式还要看具体实现,不能从连接数直接推断。
在 Linux ps 的状态缩写里,R 合并表示 running 或 runnable;一次快照不能仅凭一个 R 字母区分“正在某个核心运行”与“在运行队列里等待”。S 表示可被中断的睡眠,通常在等事件;D 表示不可中断睡眠,常与 I/O 等待有关,但单个字母仍不能证明具体等待对象。状态会瞬间改变,03.03 已提醒记录观察时刻和权限范围。
| 未来 IM 现象 | 可能的执行状态 | 这一步还不能确定 |
|---|---|---|
| 一万条连接多为空闲 | 很多连接没有待处理消息 | 进程究竟用了多少内存、FD,是否有心跳工作 |
| 收到一条群消息需生成很多目标操作 | 一批任务可能变成可运行 | CPU 是否真是瓶颈,还要看下游等待 |
| 消息查询等外部存储响应 | 某些任务等待返回 | 存储慢、网络慢还是锁争用 |
所以“CPU 不满”只排除了某些持续占满所有相关 CPU 槽位的情况,不能证明系统有足够连接数、内存、文件描述符或下游容量。反过来,“CPU 很忙”也不能只用连接数解释,少量计算密集任务就可能产生高占用。
二、调度器怎样分配有限的 CPU 时间
在只有一个 CPU 核心的简化模型里,同一时刻最多有一个普通执行任务在该核心上运行。若 A、B、C 都可运行,操作系统调度器要决定下一段时间让谁执行。被选中的任务运行一段时间后,可能主动进入等待,也可能因调度策略被抢占,把执行机会交给别人。安排给一个任务的有限运行窗口常用“时间片”解释,但真实 Linux 的策略、优先级、权重和调度条件比这里的课堂模型复杂。
单核模型,时刻 t:
可运行集合 = {A, B, C}
CPU 执行槽 = 1 个
调度决策 = 从可运行集合中选择一个任务;其余并非同时占用该核心上下文切换发生在一个执行任务停止占用 CPU、另一个任务接续运行时,需要保存/恢复足以继续执行的状态;切换还可能影响缓存等性能条件,代价不是课程可凭空给出的固定“几毫秒”。进入内核态和上下文切换不是同一个动作:同一线程可以进入内核完成系统调用,再返回继续运行,而不必换成另一线程;系统调用若等待事件,也可能触发调度。不能用“发生了一次 os.Open”直接断言“必然有一次线程切换”。
时间片过长,短交互任务可能迟迟拿不到首次运行机会;时间片太短,切换和调度开销可能占比增大。不同业务会在响应速度、吞吐和公平之间取舍;本章先用两个极简算法展示这种取舍,不称它们为当前 Linux 的完整实现。
三、单核纸上实验:先来先服务与轮转
写清条件,才能检查每个数字:A、B 同时在 t=0 到达且可运行,各需要 4 ms 的纯 CPU 时间;只有一个核心;没有 I/O;调度切换成本按 0 处理;A 在同一时刻按约定先排队。任务一旦拿到所需 CPU 时间便完成。以下时间线是本章自定模型,不是 Go 或 Linux 实测。
先来先服务(FCFS)先让 A 完成,再让 B 完成:
时间 ms 0 4 8
CPU |---- A ----|---- B ----|轮转(round robin,RR)假定每次最多运行 2 ms,未完成者回到可运行队列末尾:
时间 ms 0 2 4 6 8
CPU |- A -|- B -|- A -|- B -|从图上逐一读取,而不是凭“轮转更公平”直接猜结果:
| 模型 | A 首次运行 | B 首次运行 | A 完成 | B 完成 | A 等 CPU | B 等 CPU |
|---|---|---|---|---|---|---|
| FCFS | 0 | 4 | 4 | 8 | 0 | 4 |
| RR,片 2 ms | 0 | 2 | 6 | 8 | 2 | 4 |
这里“等 CPU”只算任务已经可运行但未运行的时间。RR 中 A 在 t=2..4 等 2 ms;B 在 t=0..2 和 t=4..6 共等 4 ms。A 的完成时刻由 4 推迟到 6,但 B 的首次响应由 4 提前到 2;B 完成仍是 8。总 CPU 需求都为 8 ms,且本模型忽略切换开销,所以最后完成时刻相同。若有真实切换成本,总耗时可能变化;此表不能直接作为哪种算法“更快”的现实结论。
加入一次等待事件
再改假设:仍单核,A 与 B 同时到达;A 从 t=0 运行 1 ms 后请求 I/O,直到 t=6 才被唤醒,尚需 3 ms CPU;B 需 4 ms CPU 且一直可运行;切换开销仍按 0。调度可以是:
t=0..1 A 用 CPU;t=1 A 进入等待
t=1..5 B 用 CPU 并完成
t=5..6 两者都没有可运行工作,CPU 可空闲
t=6..9 A 被唤醒后用完剩余 3 ms CPUA 从到达到完成共 9 ms,其中 4 ms 真正用 CPU,5 ms 等 I/O;它没有在 t=1..6 占着 CPU。B 首次运行的响应时间是 1 ms、完成时间是 5 ms。若同一机器还有其他可运行任务,t=5..6 也可能由它们运行;“CPU 可空闲”只针对本题仅 A/B 的模型。
这个变式解释了为什么大量等待网络消息的连接不必让 CPU 百分比很高,也说明“服务请求很慢”未必是操作系统没给 CPU。还要找等待的是 I/O、锁、定时器、远端服务,还是应用自己排队。
四、别把四种时间和业务延迟混在一起
设任务到达时刻为 arrival,第一次得到 CPU 为 firstRun,完成为 finish。本章的首次响应时间 = firstRun - arrival;周转时间 = finish - arrival。CPU 服务时间是实际占 CPU 的片段之和。对没有 I/O 的例子,CPU 就绪等待时间 = 周转时间 − CPU 服务时间;一旦有 I/O,就必须另记等待 I/O 的时间,不能把所有周转时间减 CPU 服务时间都叫“等 CPU”。
| 指标 | 本章定义 | A/B RR 模型中的值 | 使用限制 |
|---|---|---|---|
| 首次响应 | 到达 → 首次运行 | A 0 ms,B 2 ms | 不是用户看到第一字节的延迟 |
| 完成时刻 | 在本题零点之后结束 | A 6 ms,B 8 ms | 不是远端消息送达时刻 |
| 周转 | 到达 → 结束 | A 6 ms,B 8 ms | 含执行与等待 |
| CPU 服务 | 真正占 CPU 的时间 | 各 4 ms | 不包含等 I/O 或排队 |
| CPU 就绪等待 | 可运行但未获 CPU | A 2 ms,B 4 ms | 需排除阻塞等待 |
若 B 的请求到达时刻改成 t=1,就不能继续把“B 首次运行 t=2”直接叫 2 ms 响应;应计算 2−1=1 ms。明确时间零点和统计对象是避免算错的第一步。
吞吐可表达为单位时间内完成多少任务。此单核模型在 8 ms 完成两项,写作“2 项 / 8 ms”,只对本题的任务大小、无切换成本和持续供应条件有意义。把它换成真实 IM 的每秒消息数,还需要真实消息体、群规模、网络与存储成本,不能照抄模型比例。
公平性关心多个可运行任务是否能持续获得合适的机会、是否有人长期饥饿;并不等于每人严格相同毫秒数。优先级、权重、截止时间等策略可能给不同任务不同份额。真实用户感知的消息延迟还包含应用队列、CPU、网络、存储、协议确认与客户端显示;调度模型只解释其中可能的一段。
五、多核与 Go:两层调度不要画成一一对应
若机器有多个可用核心,多个线程可以在不同核心上同时执行;但“有 8 核”不意味着某个任务自动快 8 倍。任务可能只有一段不能并行的工作,也可能受 I/O、锁、内存带宽或配额限制。反过来,一个进程可以有多个线程,操作系统为可运行线程分配 CPU;用户写了多少个 Go goroutine,不直接等于有多少个核心或线程。
在 Go 程序里还有一层安排:Go 运行时管理 goroutine,并让可运行的 Go 任务在可用于执行 Go 代码的操作系统线程上前进;操作系统再调度那些线程。一次 goroutine 等待 I/O,不必让一个核心持续运行它。具体的 G/M/P 结构、网络轮询器、阻塞系统调用、抢占与 GOMAXPROCS 的细节会在 05 卷专门学习。本章只建立两个调度层的关系,不把“每条连接一个 goroutine”或“每个 goroutine 一个线程”当成当前实现事实。
应用需求:读消息、校验、群聊扩散、写历史
↓ 由程序组织任务
Go 运行时:管理可运行/等待的 goroutine
↓ 在可用的线程上执行 Go 代码
操作系统:在 CPU 核心上调度可运行线程
↓
CPU 核心:同一核心某一瞬间执行一个普通线程的指令流(简化)runtime.GOMAXPROCS 关联可同时执行用户 Go 代码的并行度限制,但它不是“进程最多能有几个线程”,更不是“最多能保留几个连接”。其默认值和与容器资源的关系可能随 Go 版本及运行环境变化,设计与诊断时要核对实际版本、配置和可用 CPU 资源。03.11 会进一步解释容器 CPU 配额,所以不要只看主机有几核就推断容器中的执行能力。
多核还引入共享数据问题:两个线程或 goroutine 可能同时操作会话索引。调度器使它们获得运行机会,不会自动保证 map 更新无数据竞争、消息顺序或一次发送只保存一份。同步、所有权和竞态检测是 05 卷与 10.07 的主题;本章不通过时间线给出并发正确性保证。
六、用观测区分“等 CPU”与“等别的东西”
03.03 的 ps STAT 是某一瞬间的状态线索。R 可以是正在运行或排队可运行;S 表示可中断的等待;D 通常与不可中断等待有关。一次 ps 输出既没有给出完整请求链,也不自动告诉你任务已等了多久、在等哪个远端、是否发生锁争用。CPU 使用率同样依赖采样时间窗、所看进程/线程范围和核数口径。把一个时刻的状态或一个百分比直接写成原因,证据太弱。
| 观察 | 至少两种可能解释 | 下一步要区分什么 |
|---|---|---|
| 请求慢,相关 CPU 使用不高 | 等存储或网络;等锁、定时器或应用队列 | 分阶段耗时、队列长度、I/O 与锁等待 |
| 连接很多,CPU 不高 | 多数连接空闲;下游阻塞导致任务无法运行 | 活跃消息量、连接资源、等待点 |
| 连接很少,CPU 很高 | 少量计算密集工作;循环或重复序列化 | 每阶段 CPU 时间、输入规模、热点 |
某次 ps 显示 R | 某刻正在运行或可运行排队 | 持续样本、运行队列和 CPU 时间 |
某次 ps 显示 S | 当前在等某事件 | 事件类型、唤醒时机、业务时间线 |
这里的“下一步”是待设计的个人观察,不是本轮已执行的系统诊断。要把慢请求从“感觉慢”变成可比较数据,至少为每条测试请求记录开始、进入应用队列、开始处理、外部 I/O 发起与返回、响应完成等时间点,并写明采用什么时钟与统计窗口。重复多次,保存输入规模、代码版本和运行环境;没有这些条件,不能拿两个不同时刻的 CPU 百分比直接比较优化效果。
用户态/内核态时间、CPU 占用、等待 I/O、应用队列等待又是不同账目。一次 read 可以进入内核,但如果它随后等待数据,整个请求的墙上时间可能很长而 CPU 服务时间不大。反过来,解码或扇出计算可在用户态消耗 CPU。03.09 会解释 I/O 就绪与事件通知,11 卷再设计可观测性、负载实验和容量指标。
七、迁移到 IM 业务:闲置连接与群聊扇出
现在把前面的概念用在未来教学 IM 的两个需求情景。它们只用于决定需要什么证据,不描述 OpenIM 或任何公司系统的实际实现。
情景 A:许多连接,多数没有消息
一条连接可能占用描述符、缓冲和会话状态;空闲时它不必持续产生可运行的 CPU 工作。若所有连接都需要心跳或超时管理,也会有周期性任务,但频率、实现和成本必须测量或从具体设计取得,不能由“连接存在”自动算成一个占用核心的线程。
要回答“还能容纳更多连接吗”,至少分三本账:连接资源账(描述符、缓冲和内存)、CPU 账(真正活跃的校验、编码、加密等工作)、下游账(网络带宽、存储或其他服务)。CPU 低只说明在所选窗口里相关 CPU 未被持续占满;如果描述符或内存先达到限制,CPU 低也不能让新连接成功。
情景 B:一条群消息触发许多目标操作
群消息到达后,校验、选择目标、编码或准备发送可能产生大量可运行工作;若目标写入慢,任务又可能等待网络或应用队列。假设同一时刻有 100 个待处理目标,而只有 2 个核心,不能说“100 个任务同时在 CPU 上执行”;也不能仅凭“两个核心很忙”断定每个目标最终收到消息。
| 阶段 | 可能需要 CPU | 可能在等待 | 业务还要核对 |
|---|---|---|---|
| 输入校验与目标选择 | 消息解析、规则判断 | 权限查询等依赖 | 谁允许发送给谁 |
| 为目标准备数据 | 编码、复制、索引处理 | 应用队列、锁 | 是否遗漏/重复目标 |
| 写到连接或持久层 | 调用准备与数据处理 | 网络、存储、背压 | 哪个确认点算成功 |
这是需求分解,不是对某个项目线程/队列结构的事实陈述。若CPU 高且可运行工作持续积压,可以提出“计算或调度供给不足”假设;若CPU 低但目标队列增长,要考虑 I/O、下游限速、锁和应用级限制。每个假设都需时间序列和阶段证据,不能由一行 ps 或单个总体百分比证明。
要做容量推演,可先设置明确假设:每条消息产生 fanout 个目标操作,每个目标的纯 CPU 成本为 c,输入速率为 r 条/秒,则理想化 CPU 需求约为 r × fanout × c 秒 CPU/秒。这个乘法只是没有共享优化、没有批处理、成本恒定时的预算模型;真实实现可能复用编码、分批、异步排队,也可能有额外网络和存储成本。把假设写出来,后续才能用测量修正。
例如虚构负载 r=40 条/秒、每条 fanout=25、每目标纯 CPU c=0.2 ms=0.0002 秒,则需求为 40×25×0.0002=0.2 CPU 秒/秒。这个数只描述题设中的目标操作;它没有计入连接心跳、日志、权限查询、序列化共享成本或运行时开销,不能据此断言“机器只需要 0.2 个核心”。若应用队列里有 1000 个待处理目标,也要问它们是尚未交给 Go 任务、已成为可运行 goroutine,还是正等网络写入;应用队列长度、Go 可运行队列和操作系统线程的运行队列不是同一个指标。
八、完整纸上实验与分层练习
个人练习按以下四轮进行。课程只给推演与反馈;学习者以后如具备自己的隔离实验环境,再记录真实执行,不在本项目中运行教学服务或性能基准。
- 状态图:用自己的话区分“正在运行、可运行、等待”。给空闲连接、消息校验、等待存储分别标注“可能状态”,说明未知条件。
- 单核表:独立重画 A/B 各 4 ms 的 FCFS 与 2 ms RR 时间线,分别算首次响应、完成、CPU 就绪等待;改变切换成本为非零时先解释模型哪里要补项,不随意编数字。
- 阻塞变式:A 在 1 ms 后等 I/O 到 t=6,B 需 4 ms;标出 t=5..6 在仅有 A/B 时 CPU 可空闲,解释 A 的 9 ms 周转由 4 ms CPU 与 5 ms I/O 等待组成。
- IM 迁移:给“1 万空闲连接”与“少量连接触发大量群目标”分别写假设、指标、可能反例和下一项个人实验,不用 CPU 百分比代替完整业务结论。
分层练习:先回答,再核对反馈
1. 有连接就一定有线程正在用 CPU 吗?
不一定。连接可长期存在而没有待处理消息;还需看实现和事件。
2. 可运行与正在运行有什么区别?
可运行表示有工作、具备条件但可能排队;正在运行表示此刻获得 CPU。
3. 等待 I/O 的任务会持续占用一个 CPU 核心吗?
不会因“正在等待”本身持续占用;之后事件完成才可能重新可运行。
4. `ps` 的 `R` 一定表示此刻在核心上执行吗?
不一定。Linux ps 把 running 和 runnable 放在同一个 R 状态中。
5. 单核心上能同时执行 A、B 两个普通线程的指令吗?
在本章单核简化模型里,同一瞬间只能有一个占用该核心;可快速轮换。
6. 进入内核态就必定发生线程上下文切换吗?
不必。同一线程可执行系统调用后返回继续运行,未必换成另一线程。
7. 本章 FCFS 的 B 首次响应是多少?
B 从 t=0 等到 t=4 才首次运行,因此是 4 ms。
8. 本章 RR 的 B 首次响应是多少?
B 从 t=0 等到 t=2 首次运行,因此是 2 ms。
9. RR 下 A 完成于何时?
A 在 0..2 和 4..6 各用 2 ms,总需 4 ms,完成于 t=6。
10. RR 下 B 的 CPU 就绪等待有几毫秒?
t=0..2 与 t=4..6,共 4 ms。
11. RR 的 A 比 FCFS 更早完成吗?
没有。A 在 FCFS t=4 完成,在本题 RR t=6 完成;改善的是 B 的首次响应。
12. A 阻塞变式的 9 ms 周转都是“排队等 CPU”吗?
不是。4 ms 用 CPU,5 ms 等 I/O,本题没有给出额外 CPU 就绪等待。
13. t=5..6 两任务都未运行,一定说明系统坏了吗?
不一定。在仅 A/B 的模型中 B 已完成、A 仍等 I/O,CPU 可空闲。
14. 响应时间等于完成时间吗?
不等于。首次响应看第一次获得 CPU,完成看所有工作结束;真实业务响应还含更多阶段。
15. 一个进程的 goroutine 数等于操作系统线程数吗?
不等于。Go 运行时在操作系统线程上调度多个 goroutine,具体映射会变化。
16. `GOMAXPROCS` 等于允许的连接数吗?
不等于。它关联可并行执行用户 Go 代码的限制,连接资源另算。
17. 两核心意味着任何单任务都自动快两倍吗?
不意味着。任务要能并行,还可能受 I/O、锁或配额限制。
18. CPU 使用低且请求慢,能直接断定“调度器有 bug”吗?
不能。还要区分 I/O、锁、应用队列、远端依赖和观察口径。
19. `ps` 一次显示 `S` 能证明在等哪台存储服务吗?
不能。它只是一刻的睡眠状态线索,需要请求阶段和等待对象证据。
20. 连接数大就一定先到 CPU 极限吗?
不一定。描述符、内存、缓冲或下游限制都可能先成为瓶颈。
21. 群聊目标数 100 意味 100 项 CPU 工作同时运行吗?
不意味。它们可排队、等待或由少量核心逐步执行,取决于实现和资源。
22. 仅凭两张静态时间线能证明线上 IM 消息已送达吗?
不能。它们只解释特定假设下的 CPU 调度;送达需要网络与确认点证据。
来源与下一步
- OSTEP:CPU Scheduling与作者课程目录:本章单核指标与经典策略的理论起点。
- Linux
ps(1)状态说明:R/S/D的观察含义与快照限制。 - Linux 调度设计文档:提醒读者实际 Linux 策略与纸上 FCFS/RR 模型不同,具体实现需按使用的内核版本核对。
- Go
runtime.GOMAXPROCS:Go 执行并行度与线程数量、连接数量的边界。
下一章先读04.01 应用通信与分层,再沿学习路线进入网络与 Go 并发;03.05–03.06 的虚拟内存与分配留到 S4 回访。完成本章前,请能解释一个“CPU 低但消息慢”的可能路径,并明确还缺哪项观察才能判断原因。