《进程调度与 CPU:CFS、优先级与 cgroup CPU 控制》

深入 Linux 进程调度与 CPU 管理:进程状态与调度实体、CFS 完全公平调度器与虚拟时钟、nice/优先级与 SCHED_FIFO/SCHED_RR 实时调度、cgroup cpu 子系统(shares/quota)、负载均衡与 CPU 亲和性、isolcpus 隔离,以及 top/ps/pidstat 解读与高 CPU 进程限流实战。

引言

多核时代,「进程该在哪个 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. 进程状态与调度实体

调度器只关心「可运行」的进程——理解状态才能读懂 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_OTHERCFS 默认普通进程
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
调整运行中 nicerenice 10 -p <pid>
查看调度策略chrt -p <pid>
设实时 FIFOchrt -f 80 -p <pid>
设实时 RRchrt -r 90 -p <pid>
cgroup 封顶核数echo "50000 100000" > cpu.max
按权重分 CPUecho 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 与优先级

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「linux」更多文章

  1. 《Linux 网络虚拟化:namespace、veth、网桥与虚拟交换机》
  2. 《ELF 二进制与动态链接:从符号到加载》
  3. 《eBPF 与可观测性:bpftrace、追踪与动态插桩》