引言
多核时代,「进程该在哪个 CPU 上跑、跑多久、谁先跑」由内核的调度器裁决。主调度器 CFS 用虚拟时钟实现「看似公平」,nice/优先级表达相对轻重,SCHED_FIFO/SCHED_RR 给实时任务插队权;cgroup cpu 子系统则把「CPU 份额」做成可按组分配的资源。本文从进程状态与调度实体讲起,拆解 CFS 的 vruntime 原理,覆盖 nice/实时优先级、cgroup shares/quota、负载均衡与亲和性、isolcpus 隔离,最后用 top/ps/pidstat 解读 CPU 指标,并实战「限流一个失控的高 CPU 进程」。
前置:/linux-process-management/(进程管理与监控基础)。性能方法论见 /linux-performance-tuning/,cgroup 隔离见 /linux-containers-isolation/。
目录
- 1. 进程状态与调度实体
- 2. CFS:完全公平调度器与虚拟时钟
- 3. nice 与优先级
- 4. 实时调度:SCHED_FIFO 与 SCHED_RR
- 5. cgroup cpu 子系统:shares 与 quota
- 6. 负载均衡与 CPU 亲和性
- 7. isolcpus 与 CPU 隔离
- 8. top、ps、pidstat 解读
- 9. 实战:限流一个高 CPU 进程
- 10. 速查表
- 延伸阅读
1. 进程状态与调度实体
调度器只关心「可运行」的进程——理解状态才能读懂 top/ps 里那些字母:
ps -eo pid,stat,comm | head
# 状态字母:R 运行、S 睡眠、D 不可中断睡眠、Z 僵尸、T 停止
| 状态 | 含义 | 调度含义 |
|---|---|---|
| R | 可运行/正在运行 | 排队等 CPU |
| S | 可中断睡眠 | 等事件,不在运行队列 |
| D | 不可中断睡眠 | 等磁盘/内核,杀不掉! |
| Z | 僵尸 | 已结束等父进程收尸 |
| T | 停止 | 被 SIGSTOP/调试暂停 |
调度实体(sched_entity)是调度器的操作对象:每个进程/线程对应一个实体,挂在运行队列(runqueue)上,内核按调度类从中挑选下一个运行的实体。
记忆:top 里大量 D 状态 + 高 iowait = 磁盘瓶颈,不是 CPU 问题;Z 是父进程没 wait 回收,不是资源问题。先读懂状态,别把锅全甩给 CPU。
# 看各 CPU 的运行队列长度与负载
cat /proc/loadavg
# 详细调度统计
grep cpu /proc/schedstat
2. CFS:完全公平调度器与虚拟时钟
CFS 的目标不是「轮流」,而是「按权重瓜分 CPU」——用虚拟时钟 vruntime 记录每个进程的「欠账/盈余」:
每个进程维护 vruntime(越小 = 欠的 CPU 时间越多)
调度器每次选「vruntime 最小」的进程运行
nice 值影响 vruntime 增长速度:nice 越大涨得越快 → 得到的 CPU 越少
运行时间多的进程 vruntime 涨得快 → 自动让位给「欠账」的进程
# 查看每个进程的调度统计(vruntime 相关)
cat /proc/<pid>/sched | head
# 每个 CPU 的调度器运行统计
cat /proc/schedstat
# 调度器调试信息
sudo cat /proc/sched_debug | head -40
心智:CFS 是「加权公平」,不是「时间片轮转」。它没有固定时间片,而是持续比较 vruntime 做动态抢占——这也让「公平」在进程数波动时依然成立。
3. nice 与优先级
nice 值(-20~19)表达「相对谦让程度」——nice 越大越谦让、拿到的 CPU 越少;默认 0:
# 以低优先级启动
nice -n 10 ./heavy_task
# 调整运行中进程的 nice
renice 10 -p 1234
# 查看 nice 与内核优先级
ps -o pid,ni,pri,comm -p 1234
| nice | 内核优先级 pri | 含义 |
|---|---|---|
| -20 | 高(如 130+) | 最不谦让,抢更多 CPU |
| 0 | 默认(120) | 普通进程 |
| 19 | 低(如 100) | 最谦让,几乎让出 CPU |
铁律:普通用户只能把 nice 调大(更谦让),调小(更霸道)需要 root。对后台批处理任务用
nice让路,比直接杀死它更体面,也避免抢走在线服务的 CPU。
# 调整实时任务/低延迟任务的优先级另见 chrt(下一章)
# nice 只影响 CFS 类进程,对实时调度类无效
4. 实时调度:SCHED_FIFO 与 SCHED_RR
实时调度让关键任务「插队」在普通进程之前——两种策略:FIFO 不被打断(除非自让/被更高优先级的抢占),RR 同优先级轮转:
# 查看当前调度策略与优先级
chrt -p 1234
# 设为 SCHED_FIFO、优先级 80(需 root)
chrt -f 80 -p 1234
# 设为 SCHED_RR、优先级 90
chrt -r 90 -p 1234
# 启动时指定
chrt -f 50 ./latency_app
| 策略 | 行为 | 典型用途 |
|---|---|---|
| SCHED_OTHER | CFS 默认 | 普通进程 |
| SCHED_FIFO | 实时,同优先级先来先跑 | 音频/工业控制 |
| SCHED_RR | 实时,同优先级时间片轮转 | 多个实时任务均衡 |
| SCHED_BATCH | 批处理,低抢占 | 后台计算 |
心法:实时优先级 1~99,数字越大越优先。把普通进程误设成高优先级实时任务,可能饿死系统所有进程——含 SSH 登录,属于高危操作,生产慎用。
5. cgroup cpu 子系统:shares 与 quota
cgroup 把 CPU 从「按进程」变成「按组」管理——两个维度:shares 定比例、quota 定上限:
# cgroup v2:配额(该组最多 0.5 核)
echo "50000 100000" > /sys/fs/cgroup/app/cpu.max # 50ms/100ms
# cgroup v1 两个文件
echo 50000 > /sys/fs/cgroup/cpu/app/cpu.cfs_quota_us
echo 100000 > /sys/fs/cgroup/cpu/app/cpu.cfs_period_us
| 机制 | 文件 | 语义 |
|---|---|---|
| shares(相对权重) | cpu.weight | 竞争时按比例分,空闲时可用满 |
| quota(硬上限) | cpu.max | 无论多空闲,最多用这么多核 |
| 绑核 | cpuset.cpus | 只允许在这些 CPU 上跑 |
# systemd 风格控制服务 CPU
systemctl set-property my.service CPUWeight=100 CPUQuota=50%
记忆:shares 管「抢」、quota 管「封顶」、cpuset 管「在哪跑」。防失控用 quota(硬顶),多租户公平分配用 shares。容器
--cpus=1.5底层就是写 quota。
6. 负载均衡与 CPU 亲和性
多核调度器会持续做负载均衡(把进程从忙核搬到闲核),但「搬家」有缓存/迁移成本——亲和性让你手动钉住进程:
# 查看/设置进程允许运行的 CPU 集合
taskset -pc 1234
taskset -pc 0,2 1234 # 只允许在 CPU0 和 CPU2
# 启动时绑定
taskset -c 0,1 ./app
# top 按核查看
top -1
| 概念 | 含义 |
|---|---|
| 负载均衡 | 内核在核间迁移 runnable 进程 |
| 软亲和 | 默认尽量留在原核(缓存友好) |
| 硬亲和 | taskset/cpuset 强制核集合 |
| NUMA 亲缘 | 进程倾向与内存同 node(见内存专题) |
心法:高并发服务别把线程全钉一个核——反而制造热点。亲和性主要用于「实时关键线程不被打扰」「NUMA 本地性」「多租户隔离」,普通场景信任内核均衡即可。
7. isolcpus 与 CPU 隔离
isolcpus 把指定核从内核调度器中「拿走」——上面的进程不受系统扰动,适合低延迟关键负载:
# 内核命令行:CPU2/3 隔离出通用调度
GRUB_CMDLINE_LINUX="isolcpus=2,3 nohz_full=2,3"
# 重启后验证
cat /proc/cmdline
# 把关键进程钉到隔离核
taskset -c 2,3 ./low_latency_app
# 更精细的运行时隔离:cpuset 子系统
echo "2-3" > /sys/fs/cgroup/cpuset/rt/cpuset.cpus
echo "0-1" > /sys/fs/cgroup/cpuset/rt/cpuset.mems
# 把进程放进该 cpuset
echo <pid> > /sys/fs/cgroup/cpuset/rt/cgroup.procs
| 方式 | 特点 |
|---|---|
| isolcpus(启动参数) | 隔离核不进调度,需重启生效 |
| cpuset(运行时) | 细粒度分组绑核,可动态 |
| nohz_full | 隔离核关闭周期时钟中断,减少抖动 |
记忆:隔离 ≠ 更快,是「减少打扰」。隔离出的核如果空闲,是一种浪费——只有确认「抖动伤害大于核浪费」时才值得,典型场景是 DPDK、音频、高频交易。
8. top、ps、pidstat 解读
读懂 CPU 指标先分清「进程 %CPU、核利用率、load average」是三个不同概念:
top -1 # 每核利用率 + 进程表
top -p 1234 # 盯单个进程
ps -eo pid,pcpu,pmem,stat,comm --sort=-pcpu | head
pidstat -u 1 5 # 每秒 CPU 使用率
| 指标 | 含义 | 陷阱 |
|---|---|---|
| 进程 %CPU | 占单核百分比(多线程可超 100%) | >100% 是正常的 |
| 核利用率 | 该核忙的比例 | 与进程数无关 |
| load average | 可运行+不可中断进程数(1/5/15 分钟) | 不等于 CPU 利用率 |
| TIME+ | 累计 CPU 时间 | 排障长任务很有用 |
铁律:load average 高 ≠ CPU 忙——D 状态进程(等磁盘)也计入 load。
top里「D 多 + iowait 高」是磁盘问题;「R 多 + 核利用率高」才是 CPU 饱和。
9. 实战:限流一个高 CPU 进程
场景:某进程把核打满拖垮整机——在不杀进程的前提下分三步「降温」:
# 第一步:确认是谁
top -o %CPU
# 第二步:确认目标进程号(如 1234)
ps -p 1234 -o pid,pcpu,comm
# 第三步:先降 nice(让它让路)
renice 10 -p 1234
如果仍不够,用 cgroup v2 硬性封顶到 0.5 核:
# 建组并放进程,限额 50ms/100ms = 0.5 核
mkdir /sys/fs/cgroup/limit
echo "50000 100000" > /sys/fs/cgroup/limit/cpu.max
echo 1234 > /sys/fs/cgroup/limit/cgroup.procs
# 验证是否生效
cat /sys/fs/cgroup/limit/cpu.stat
pidstat -u 1 3 -p 1234
# 必要时把进程钉到空闲核,避免与在线服务争抢
taskset -pc 7 1234
# 恢复:echo 1234 移出该组、renice 0
记忆:限流三板斧 = renice 让路 → cgroup quota 封顶 → taskset 钉核。优先级与配额是「软约束」,进程自己不知道也不在乎——比 kill 重启温和,且可随时撤销。
10. 速查表
| 需求 | 命令 |
|---|---|
| 查看进程状态 | ps -eo pid,stat,comm |
| 低优先级启动 | nice -n 10 ./task |
| 调整运行中 nice | renice 10 -p <pid> |
| 查看调度策略 | chrt -p <pid> |
| 设实时 FIFO | chrt -f 80 -p <pid> |
| 设实时 RR | chrt -r 90 -p <pid> |
| cgroup 封顶核数 | echo "50000 100000" > cpu.max |
| 按权重分 CPU | echo 100 > cpu.weight |
| 绑核 | taskset -pc 0,2 <pid> |
| 隔离核(重启) | isolcpus=2,3 nohz_full=2,3 |
| 看每核负载 | top -1 |
| 每秒 CPU 采样 | pidstat -u 1 |
一句话记忆:CFS 用 vruntime 按权重公平分 CPU;nice 表谦让、chrt 表插队(实时);cgroup 里 shares 管抢、quota 管顶、cpuset 管在哪跑;排查看状态、辨 iowait、不信 load average 单指标;限流三板斧 renice → quota → taskset。
延伸阅读
- /linux-process-management/ — 进程状态与监控基础
- /linux-performance-tuning/ — CPU 调优与性能排查方法论
- /linux-containers-isolation/ — cgroup 资源限制与容器
- /linux-kernel-tuning/ — 内核调度与 sysctl 参数
- /linux-cron-scheduled-tasks/ — 定时任务中的 nice 与优先级
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。