1
2
3
4
5
6
7
作者:李晓辉

联系方式:

1. 微信:Lxh_Chat

2. 邮箱:939958092@qq.com
  1. Linux 性能调优系列(一) 建立科学的性能分析思路
  2. Linux 性能调优系列(二) 掌握基础性能监控命令
  3. Linux 性能调优系列(三) sysstat实现性能数据持久采集
  4. Linux 性能调优系列(四) 别只会用 sysstat,PCP 才是 Linux 性能监控的进阶玩法
  5. Linux 性能调优系列(五) 给你的服务器做一次全面体检
  6. Linux 性能调优系列(六) 想调内核参数?先搞懂 /proc、sysctl 和 sysfs
  7. Linux 性能调优系列(七) 还在手动改 sysctl?TuneD 智能性能调优实战来了
  8. Linux 性能调优系列(八) 进程太能吃资源怎么办?用 ulimit 和 systemd 给它加个"上限"
  9. Linux 性能调优系列(九) cgroup v2实战:防止一个烂进程干崩整台服务器
  10. Linux 性能调优系列(十) CPU 100% 到底是谁在忙?用 perf 把"元凶"揪出来
  11. Linux 性能调优系列(十一) 程序为什么卡住了?strace 和 ltrace 带你找到答案
  12. Linux 性能调优系列(十二) 服务器又卡了?SystemTap 带你杀到内核看看为啥
  13. Linux 性能调优系列(十三) 从 SystemTap 到 eBPF,Linux 性能追踪进入新时代
  14. Linux 性能调优系列(十四) CPU 这么忙,到底是谁决定“让谁先跑”?一文搞懂 Linux 调度器
  15. Linux 性能调优系列(十五) CPU 到底该先给谁?搞懂 Linux 实时调度
  16. Linux 性能调优系列(十六) Linux CPU 隔离实战,为什么实时线程绑核后延迟还会抖?
  17. Linux 性能调优系列(十七) CPU 占用不高却跑的慢?深挖 CPU 缓存性能瓶颈
  18. Linux 性能调优系列(十八) 程序申请了1GB内存,为什么系统却说它没用?
  19. Linux 性能调优系列(十九) 服务器硬盘慢得像蜗牛?先别急着换硬盘,这四招先把瓶颈找出来
  20. Linux 性能调优系列(二十) 磁盘 I/O 调度器怎么选?一文搞懂 mq-deadline、kyber、bfq 和 none
  21. Linux 性能调优系列(二十一) 磁盘为什么慢?从文件系统到 I/O 调优,一次讲透
  22. Linux 性能调优系列(二十二) Linux 网络调优别乱改 sysctl!先找瓶颈再调 TCP 缓冲区
  23. Linux 性能调优系列(二十三) MTU 从 1500 到 9000,巨帧真的能提升性能吗?
  24. Linux 性能调优系列(二十四) 从 CPU 省电到网卡卸载,聊聊 Linux 网络调优
  25. Linux 性能调优系列(二十五) 聊聊VirtIO、TuneD、virtio-mem 调优

前面我们已经学习了:

  • 硬件画像
  • Linux 监控工具
  • 内核可调参数
  • TuneD
  • cgroup
  • perf
  • strace
  • SystemTap
  • eBPF

这些工具解决的是不同层面的问题。比如:

1
top

可以告诉我们:

CPU 到底忙不忙?

1
perf

可以进一步告诉我们:

CPU 时间主要消耗在哪里?

1
runqlat

又可以帮助我们观察:

任务是不是因为没有 CPU 可用,一直在运行队列里排队?

但是到这里,一个更加底层的问题就出现了:

CPU 忙起来以后,Linux 内核到底是怎么决定“下一个让谁运行”的?

这就是今天要学习的:

Linux CPU 调度器(CPU Scheduler)

而如果你正在使用 RHEL 10,还需要特别注意一个变化:

RHEL 10 已经使用 EEVDF 调度器替代传统的 CFS。

EEVDF 全称:

Earliest Eligible Virtual Deadline First

中文通常可以理解为:

最早可运行虚拟截止时间优先

所以这一篇,我们就从最基础的:

1
2
3
4
5
6
7
任务
↓
运行队列
↓
调度器
↓
选择下一个任务

一路讲到:

1
2
3
4
5
6
7
8
9
CFS
↓
EEVDF
↓
nice / renice
↓
chrt
↓
上下文切换

什么是 CPU 调度?

先想象一个非常简单的场景。假设服务器只有 1 个 CPU 核心。现在同时有:

1
2
3
4
5
nginx
mysql
bash
java
python

5 个任务都想使用 CPU。但是:

一个 CPU 核心在同一时刻只能真正执行一个线程。

那么问题来了:

其他任务怎么办?

它们不能全部同时运行,只能等待。于是 Linux 内核需要维护一个非常重要的东西:

运行队列(run queue)

可以简单理解成:

flowchart TD
    CPU[CPU]
    Scheduler[Linux Scheduler<br/>调度器]
    RunQueue[Run Queue<br/>运行队列]

    Nginx[nginx 线程<br/>Runnable]
    MySQL[MySQL 线程<br/>Runnable]
    Java[Java 线程<br/>Runnable]

    CPU --> Scheduler
    Scheduler -->|选择下一个任务| RunQueue

    RunQueue --> Nginx
    RunQueue --> MySQL
    RunQueue --> Java

运行队列里面放的,是:

当前处于可运行状态、正在等待 CPU 的任务。

调度器要做的事情非常核心:

从这些可运行任务中,选择下一个应该运行的任务。


Linux 调度的最小单位:线程

这里有一个非常容易搞错的概念。很多人会说:

“Linux 调度的是进程。”

严格来说,这个说法并不准确。Linux 内核真正调度的是:

线程(thread)

也就是内核中的 task_struct 对应的调度实体。一个进程可以拥有多个线程:

1
2
3
4
5
6
进程 PID 1000
│
├── TID 1000
├── TID 1001
├── TID 1002
└── TID 1003

这些线程共享进程的大部分资源,例如:

1
2
3
虚拟地址空间
文件描述符
信号处理等

但是:

每一个线程都可以独立进入运行队列并被调度。

所以以后看到:

1
2
PID
TID

不要简单地把它们理解成两个不同的“进程编号”。可以简单记:

1
2
3
4
5
进程
│
├── 线程1 ──→ 可以被调度
├── 线程2 ──→ 可以被调度
└── 线程3 ──→ 可以被调度

也可以使用:

1
2
3
4
[root@localhost ~]# ps -L
PID LWP TTY TIME CMD
1214 1214 pts/0 00:00:00 bash
7825 7825 pts/0 00:00:00 ps

查看进程中的线程。


每个 CPU 都有自己的运行队列

如果服务器只有一个 CPU,理解起来还比较简单。但现在服务器可能有:

1
2
3
4
5
CPU0
CPU1
CPU2
CPU3
...

这时候就不能简单理解成:

所有任务都排在一个巨大的队列里。

Linux 调度器会为每个 CPU 维护自己的运行队列。可以简单理解成:

1
2
3
4
5
6
7
8
9
10
11
CPU0                         CPU1
┌──────────────┐ ┌──────────────┐
│ run queue │ │ run queue │
│ │ │ │
│ nginx │ │ java │
│ bash │ │ mysql │
│ python │ │ redis │
└──────┬───────┘ └──────┬───────┘
│ │
▼ ▼
CPU0 CPU1

所以 CPU 调度实际上还涉及一个重要问题:

任务应该在哪一个 CPU 上运行?

不过这个问题涉及 CPU 亲和性、任务迁移、中断等内容,我们后面的文章会单独展开。这一篇先把注意力放在:

一个 CPU 上,调度器到底怎么决定“谁先运行”。


Linux 调度器到底在做什么?

可以把调度器理解成一个非常忙碌的“CPU 排班员”。假设现在有:

1
2
3
4
任务A
任务B
任务C
任务D

全部都处于 Runnable 状态。调度器需要不断回答:

下一个运行谁?

而且这个决定不是只做一次。CPU 不断发生:

1
2
3
4
5
6
7
8
9
10
11
运行
↓
任务阻塞
↓
新任务唤醒
↓
任务被抢占
↓
重新选择
↓
继续运行

所以调度器实际上一直在工作。可以把整个过程理解成:

flowchart LR
    A[任务进入 Runnable] --> B[进入运行队列]
    B --> C[调度器选择任务]
    C --> D[CPU 执行]
    D --> E{任务状态变化}
    E -->|继续运行| C
    E -->|等待 I/O| F[阻塞]
    E -->|主动让出 CPU| B
    E -->|被抢占| B
    E -->|退出| G[任务结束]

这就是 CPU 调度最基本的工作流程。


Linux 有哪些调度类别?

Linux 并不是所有任务都使用同一种调度算法。内核定义了多个 Scheduling Class(调度类别)。可以先把它理解成:

不同类型的任务,进入不同的“排班体系”。

从调度类别的优先关系来看,可以粗略理解成:

1
2
3
4
5
6
7
8
9
Stop
↓
Deadline
↓
Real-time
↓
Fair
↓
Idle

也就是说:

高优先级调度类别中的可运行任务,会优先于低优先级调度类别中的任务。

对应关系可以画成:

flowchart TD
    A[Linux Scheduler] --> B[Stop]
    A --> C[Deadline]
    A --> D[Real-time]
    A --> E[Fair]
    A --> F[Idle]

    B --> B1[Stop 调度类]
    C --> C1[SCHED_DEADLINE]
    D --> D1[SCHED_FIFO]
    D --> D2[SCHED_RR]
    E --> E1[SCHED_OTHER]
    E --> E2[SCHED_BATCH]
    E --> E3[SCHED_IDLE]
    F --> F1[CPU 空闲线程]

这里先建立整体概念即可。后面的实时任务调度文章,我们会专门讲:

1
2
3
4
SCHED_FIFO
SCHED_RR
SCHED_DEADLINE
RT 内核

Fair 调度类:普通程序主要就在这里

我们平时启动的大多数程序,例如:

1
2
3
4
5
6
bash
nginx
java
python
mysql
redis

通常都属于普通的公平调度范畴。Fair 调度类包含:

1
2
3
SCHED_OTHER
SCHED_BATCH
SCHED_IDLE

其中:

1
SCHED_OTHER

是最常见的默认策略。Linux 内核源码中,SCHED_OTHER 实际对应的名称是:

1
SCHED_NORMAL

因此看到:

1
2
SCHED_OTHER
SCHED_NORMAL

不用把它们当成两种不同的调度策略。它们是同一个普通调度策略的不同名称。


三种常见的 Fair 调度策略

SCHED_OTHER

这是普通任务最常见的调度策略。

例如:

1
sleep 10
1
bash
1
python app.py

默认通常就是:

1
SCHED_OTHER

它会根据任务的调度属性和 nice 值参与公平调度。


SCHED_BATCH

SCHED_BATCH 面向的是:

CPU 密集型、非交互式的批处理任务。

例如:

1
2
3
后台数据处理
批量计算
离线任务

它和 SCHED_OTHER 一样属于普通调度范围,但是调度器会把它视为 CPU-intensive 任务,在唤醒行为上给予一定的调度惩罚。所以不要简单理解成:

“SCHED_BATCH 就是让任务运行更长时间片。”

更准确地说:

它告诉调度器:这个任务是批处理、非交互式任务,不需要特别照顾交互响应。

Linux sched(7) 对它的定义也是基于这种语义。


SCHED_IDLE

这个名字特别容易把人搞晕。因为 Linux 里面同时存在:

1
SCHED_IDLE

和:

1
Idle scheduling class

它们不是一回事。

SCHED_IDLE

这是一个可以用于任务的调度策略。它的优先级:

甚至低于 nice=19 的普通任务。

非常适合:

1
低优先级后台任务

Linux 文档也明确指出,SCHED_IDLE 的 nice 值不参与这种策略的调度决策,而且其优先级低于 SCHED_OTHER/SCHED_BATCH 下的 nice +19 任务。

Idle 调度类别

则是调度器本身用于:

CPU 没有其他可运行任务时运行的空闲线程。

所以一定要记住:

1
2
3
SCHED_IDLE
≠
Idle 调度类别

SCHED_OTHER、SCHED_BATCH、SCHED_IDLE 的关系

可以用一张图理解:

flowchart TD
    A[Fair 调度类] --> B[SCHED_OTHER]
    A --> C[SCHED_BATCH]
    A --> D[SCHED_IDLE]

    B --> B1[普通任务]
    B --> B2[受 nice 影响]

    C --> C1[批处理任务]
    C --> C2[CPU 密集型]
    C --> C3[降低交互式唤醒优先级]

    D --> D1[极低优先级]
    D --> D2[低于 nice +19]
    D --> D3[nice 对其无影响]

这一点记住以后,后面的 nice 和 chrt 就比较容易理解了。


Linux 内部的优先级到底是什么?

Linux 内核内部有一套静态优先级体系。普通任务和实时任务使用的优先级范围并不一样。对于实时任务:

1
1 ~ 99

数字越大,实时优先级越高。对于普通调度策略:

1
static priority = 100 ~ 139

对应:

1
nice = -20 ~ +19

可以粗略理解:

1
2
3
4
5
6
7
实时任务
1 ───────────────→ 99
低 高

普通任务
100 ──────────────→ 139
高 低

这里不要把:

1
2
3
4
nice
pri
priority
rtprio

混成一个东西。它们是不同层面的表示方式。


nice 到底是什么?

如果你使用 Linux 很久,一定见过:

1
nice

它并不是简单意义上的:

“CPU 优先级。”

更准确地说:

nice 是影响普通调度任务 CPU 调度权重的一个属性。

范围:

1
-20 ~ +19

其中:

1
-20

表示更有利于获得 CPU。而:

1
+19

表示更不利于获得 CPU。默认一般是:

1
nice = 0

Linux sched(7) 也明确规定了这个范围。可以简单记:

1
2
3
4
5
6
7
nice 越小
↓
越“希望”获得 CPU

nice 越大
↓
越“愿意”让 CPU 给别人

为什么叫 nice?

这个名字其实很好理解。假设服务器上正在运行:

1
MySQL

同时你想跑一个:

1
大规模压缩任务

如果压缩任务把 CPU 吃得非常厉害,就可能影响业务。这时候可以:

1
nice -n 19 tar ...

意思就是:

“这个任务我没那么着急,你先把 CPU 让给其他任务。”


使用 nice 启动程序

例如:

1
nice -n 19 ./long_task

就是让程序以:

1
nice = 19

启动。

如果使用负数:

1
nice -n -10 ./long_task

则表示希望提高该任务的调度优先程度。不过普通用户通常没有权限随意降低 nice 值,也就是不能随意设置负 nice。权限由相关资源限制和能力机制控制;具有适当权限的进程才可以提高自身调度优先程度。


renice:修改已经运行的任务

nice 主要用于:

启动程序时设置 nice。

而:

1
renice

用于:

修改已经运行任务的 nice 值。

例如:

1
renice 10 -p 372097

把 PID 372097 的 nice 设置为:

1
10

如果有足够权限,也可以降低 nice:

1
renice -20 -p 372097

一个非常重要的细节:Linux 的 nice 是线程属性

这里再补充一个很容易被忽略的地方。在 Linux 中:

nice 实际上是线程级属性。

也就是说,同一个进程中的不同线程,在 Linux 上可以拥有不同的 nice 值。例如:

1
2
3
4
5
PID 5000
│
├── TID 5000 nice=0
├── TID 5001 nice=5
└── TID 5002 nice=10

所以排查多线程程序时,不要只盯着 PID。这也是为什么我们前面强调:

Linux 调度的核心对象是线程。


systemd 服务怎么设置 nice?

如果程序是 systemd 管理的服务,可以通过 unit 配置:

1
2
[Service]
Nice=10

例如:

1
/etc/systemd/system/sshd.service.d/10-nice.conf

内容:

1
2
[Service]
Nice=10

然后:

1
2
systemctl daemon-reload
systemctl restart sshd

systemd 的 Nice= 就是用来设置服务进程的 nice 值;CPUSchedulingPolicy= 则可以指定 CPU 调度策略。这里有一个实用原则:

修改 unit 配置文件以后,通常需要 reload,然后重启服务,新的进程才能使用新的配置。


EEVDF:RHEL 10 的重要变化

终于到了今天最重要的部分:

EEVDF。

如果你以前学习 Linux 性能分析,经常会看到:

1
2
3
4
5
CFS
vruntime
sched_latency_ns
sched_min_granularity_ns
sched_wakeup_granularity_ns

但是在 RHEL 10 中:

CFS 已经被 EEVDF 替代。

Red Hat 官方文档明确说明:

1
2
3
CFS
↓
EEVDF

并且:

1
2
3
sched_min_granularity
↓
sched_base_slice

而:

1
sched_wakeup_granularity

在 EEVDF 中已经不再使用,因此被移除。所以以前很多针对 CFS 的调优文章,不能直接照搬到 RHEL 10。


EEVDF 到底是什么?

EEVDF 的全称是:

Earliest Eligible Virtual Deadline First

可以拆成三个关键词:

1
2
3
Eligible
Virtual
Deadline

不用第一次看到就被名字吓到。我们只需要先理解三个核心概念:

1
2
3
4
5
虚拟运行时间
↓
lag
↓
虚拟截止时间

然后调度器根据这些信息决定:

哪个任务现在更应该获得 CPU。


EEVDF 的核心思想

假设现在有两个任务:

1
2
Task A
Task B

它们都需要 CPU。调度器并不是简单地:

“你运行 10ms,我运行 10ms。”

而是会根据任务的:

1
2
3
权重
实际运行情况
应该获得的 CPU 时间

计算它们的调度状态。其中一个非常重要的概念就是:

lag

可以把它简单理解成:

任务实际获得的 CPU 时间,与按照自身权重应该获得的 CPU 时间之间的差距。

如果任务获得的 CPU 少于它应该获得的:

1
正 lag

它就处于“欠 CPU”的状态。如果已经获得了超过自身应得份额:

1
负 lag

则说明它暂时获得了更多 CPU。


什么是虚拟截止时间?

对于处于可调度状态的任务,EEVDF 会计算一个:

虚拟截止时间(virtual deadline)

调度器会重点考虑:

当前哪些任务已经具备运行资格,并且谁的虚拟截止时间更早。

于是可以把它简化成:

flowchart TD
    A[Runnable 任务] --> B[计算调度状态]
    B --> C{任务是否 Eligible?}

    C -->|否| D[暂时不选择]
    C -->|是| E[比较 Virtual Deadline]

    E --> F[选择更早的任务]
    F --> G[CPU 执行]

    G --> H[任务运行状态变化]
    H --> B

所以千万不要把 EEVDF 简单理解成:

“谁 deadline 最近谁运行。”

真正的机制还涉及:

1
2
3
4
eligible
lag
virtual deadline
weight

只是对于入门和性能排查来说,可以先抓住一个核心:

EEVDF 会综合任务的 CPU 份额与调度状态,为可运行任务计算虚拟截止时间,并据此选择任务。


为什么 EEVDF 对交互式任务比较友好?

举一个生活中的例子。

假设:

1
2
任务 A:CPU 密集型计算
任务 B:交互式程序

A 一直在计算:

1
2
3
4
计算
计算
计算
计算

而 B 经常:

1
2
3
4
5
6
7
8
9
运行一点
↓
等待 I/O
↓
再次唤醒
↓
运行一点
↓
再次等待

B 经常主动让出 CPU。当它再次变成 Runnable 时,调度器会考虑它之前的 CPU 获得情况。所以对于这种:

1
2
3
4
5
短时间运行
+
频繁睡眠
+
再次唤醒

的任务,EEVDF 的设计能够更好地处理其调度需求。这也是 EEVDF 相比传统 CFS 调度逻辑的重要变化之一。


EEVDF 的调优参数发生了什么变化?

这是 RHEL 10 管理员特别需要注意的地方。

以前经常看到:

1
2
3
kernel.sched_latency_ns
kernel.sched_min_granularity_ns
kernel.sched_wakeup_granularity_ns

但在新的内核中,部分旧参数已经发生变化。

其中:

1
2
3
sched_min_granularity
↓
sched_base_slice

Red Hat 官方明确说明:

sched_base_slice 定义了任务可以被延迟运行的最小时间。

在支持 debugfs 的环境中,可以看到:

1
cat /sys/kernel/debug/sched/base_slice_ns

也可以看到:

1
ls /sys/kernel/debug/sched/

需要注意:

不要简单把 base_slice_ns 理解成“任务固定运行这么长时间”。

它更准确的含义是:

调度器用于控制任务调度延迟/切片行为的基础参数。


migration_cost_ns 还存在吗?

这里也需要纠正一个很容易被旧资料误导的地方。很多旧文章会说:

1
kernel.sched_migration_cost_ns

现在在较新的内核中,这类调度参数可能已经从:

1
/proc/sys/kernel/

迁移到:

1
/sys/kernel/debug/sched/

例如:

1
cat /sys/kernel/debug/sched/migration_cost_ns

当前 Linux 内核源码仍然提供这个 debugfs 调度参数。

因此不能简单写成:

“EEVDF 已经完全没有 migration_cost_ns。”

更准确的说法是:

部分传统 scheduler 参数的管理位置和语义发生了变化,需要根据实际内核版本确认。

这也是为什么生产环境调优不能直接复制十年前的:

1
sysctl -w kernel.sched_xxx=...

debugfs 中查看调度器参数

可以先确认 debugfs 是否挂载:

1
2
[root@localhost ~]# mount | grep debugfs
debugfs on /sys/kernel/debug type debugfs (rw,nosuid,nodev,noexec,relatime,seclabel)

然后查看:

1
2
3
4
[root@localhost ~]# ls /sys/kernel/debug/sched/
base_slice_ns features migration_cost_ns preempt
debug latency_warn_ms nr_migrate tunable_scaling
fair_server latency_warn_once numa_balancing verbose

例如:

1
2
[root@localhost ~]# cat /sys/kernel/debug/sched/base_slice_ns
2100000

以及:

1
2
[root@localhost ~]# cat /sys/kernel/debug/sched/migration_cost_ns
500000

如果你的系统没有这些文件,不要直接认为:

“我的 EEVDF 坏了。”

因为具体可用接口还取决于:

1
2
3
内核版本
内核配置
debugfs

所以实际环境中:

以当前系统实际暴露出来的接口为准。


为什么 debugfs 参数重启后会丢?

因为这些参数位于:

1
/sys/kernel/debug/

这个目录属于:

debugfs

它不是传统意义上的持久化配置文件。因此:

1
echo 3000000 > /sys/kernel/debug/sched/base_slice_ns

这种修改通常只是:

当前运行内核中的临时修改。

重启之后不会自动保留。如果确实需要在启动时设置,可以通过 systemd 的 oneshot 服务执行。

例如:

1
2
3
4
5
6
7
8
9
10
[Unit]
Description=Apply Scheduler Tunables
After=sys-kernel-debug.mount

[Service]
Type=oneshot
ExecStart=/bin/sh -c 'echo 3000000 > /sys/kernel/debug/sched/base_slice_ns'

[Install]
WantedBy=multi-user.target

然后:

1
2
systemctl daemon-reload
systemctl enable --now eevdf-tuning.service

不过这里强烈建议:

不要为了“学习 EEVDF”就随便修改生产服务器的调度参数。

调度器属于系统核心路径。除非你已经明确知道:

1
2
3
4
5
为什么修改
修改什么
预期改善什么
如何验证
出现问题如何恢复

否则不要为了追求某个所谓的“最佳值”而调整。


怎么看 Linux 调度器统计信息?

可以查看:

1
cat /proc/schedstat

它提供的是调度器相关的统计数据。不过:

/proc/schedstat 的具体字段会随着内核版本变化。

因此不要直接死记某一个字段。如果需要深入分析调度器行为,更应该结合:

1
2
3
4
5
6
7
/proc/schedstat
+
/proc/[pid]/sched
+
perf
+
eBPF

一起分析。


chrt:查看和修改调度策略

如果:

1
nice

主要用于调整普通任务的 nice,那么:

1
chrt

则可以直接查看和修改线程的调度策略。例如:

1
chrt -p 5467

查看某个线程的调度策略。常见参数:

chrt 参数调度策略
-oSCHED_OTHER
-bSCHED_BATCH
-iSCHED_IDLE
-fSCHED_FIFO
-rSCHED_RR
-dSCHED_DEADLINE

Linux 支持的普通调度策略包括:

1
2
3
SCHED_OTHER
SCHED_BATCH
SCHED_IDLE

而实时/截止时间相关策略包括:

1
2
3
SCHED_FIFO
SCHED_RR
SCHED_DEADLINE

普通策略的静态调度优先级必须为:

1
0

而 SCHED_FIFO 和 SCHED_RR 使用:

1
1 ~ 99

的实时优先级。


使用 chrt 修改普通任务

例如把线程设置为:

1
SCHED_BATCH

可以:

1
chrt -b -p 0 5890

查看:

1
chrt -p 5890

设置:

1
SCHED_IDLE

可以:

1
chrt -i -p 0 5890

恢复成普通:

1
SCHED_OTHER

可以:

1
chrt -o -p 0 5890

这里再次强调:

SCHED_IDLE 是一种极低优先级的调度策略,不等于 CPU 的 Idle 调度类别。


systemd 也可以设置调度策略

对于 systemd 管理的服务,可以使用:

1
2
[Service]
CPUSchedulingPolicy=idle

例如:

1
/etc/systemd/system/plocate-updatedb.service.d/10-idle.conf

写入:

1
2
[Service]
CPUSchedulingPolicy=idle

然后:

1
2
systemctl daemon-reload
systemctl restart plocate-updatedb.service

systemd 的 CPUSchedulingPolicy= 可以用于设置 CPU 调度策略。这里同样提醒:

不要把 idle 理解成“让服务停止运行”。

它表示的是:

让该服务采用 SCHED_IDLE 调度策略。


上下文切换是什么?

现在我们再来看一个 CPU 调度里非常重要的指标:

Context Switch(上下文切换)

假设:

1
任务 A 正在 CPU 上运行

突然调度器决定:

1
任务 B 应该运行

于是 CPU 就需要从:

1
任务 A

切换到:

1
任务 B

这个过程就涉及:

上下文切换。

可以简单理解为:

sequenceDiagram
    participant CPU
    participant A as 任务 A
    participant S as Scheduler
    participant B as 任务 B

    CPU->>A: 执行任务 A
    A->>S: 状态发生变化
    S->>A: 保存/切出
    S->>B: 选择任务 B
    B->>CPU: 开始执行

上下文切换本身不是错误。

Linux 系统中:

上下文切换是非常正常的事情。

真正需要关注的是:

上下文切换是不是异常频繁。


自愿上下文切换和非自愿上下文切换

Linux 会区分两类上下文切换。

1. voluntary context switch

也就是:

1
voluntary_ctxt_switches

任务主动放弃 CPU。例如:

1
2
3
4
等待 I/O
sleep
等待锁
等待其他资源

2. nonvoluntary context switch

也就是:

1
nonvoluntary_ctxt_switches

任务并不是主动让出 CPU,而是:

被调度器切走。

例如:

1
被其他任务抢占

可以查看:

1
grep -E 'voluntary|nonvoluntary' /proc/4350/status

输出类似:

1
2
voluntary_ctxt_switches:        123
nonvoluntary_ctxt_switches: 45

这里不要简单理解成:

“nonvoluntary 越多系统越差。”

上下文切换是否异常,需要结合:

1
2
3
4
5
6
7
CPU 使用率
任务数量
运行队列
系统调用
I/O
调度延迟
业务负载

一起判断。


把整个 CPU 调度过程串起来

到这里,我们可以把今天的知识串起来了。假设:

1
服务器上有 100 个线程

但是:

1
CPU 只有 8 个核心

于是大量线程可能处于:

1
Runnable

状态。它们进入不同 CPU 的运行队列。然后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Linux Scheduler
│
▼
判断哪些任务可以运行
│
▼
按照调度类别选择
│
▼
Fair / RT / Deadline ...
│
▼
对于 Fair 任务进一步考虑调度状态
│
▼
EEVDF
│
▼
选择下一个任务
│
▼
CPU 执行

可以用一张图总结:

flowchart TD
    A[大量 Runnable 线程] --> B[进入 CPU 运行队列]

    B --> C[Linux Scheduler]

    C --> D{调度类别}

    D -->|Stop| E[Stop 调度类]
    D -->|Deadline| F[SCHED_DEADLINE]
    D -->|Real-time| G[SCHED_FIFO / SCHED_RR]
    D -->|Fair| H[公平调度类]
    D -->|Idle| I[Idle 调度类]

    H --> J[SCHED_OTHER]
    H --> K[SCHED_BATCH]
    H --> L[SCHED_IDLE]

    J --> M[EEVDF]
    K --> M
    L --> M

    M --> N[选择下一个可运行任务]
    N --> O[CPU 执行]

CPU 调度问题应该怎么排查?

现在回到最开始的问题:

“服务器 CPU 很高,到底发生了什么?”

以前我们可能只会:

1
top

看到:

1
%CPU = 98%

然后说:

“CPU 很高。”

但是现在应该进一步问:

这些 CPU 时间到底被谁使用?

以及:

有没有任务因为 CPU 不够而排队?

这时候就可以结合我们前面已经学过的工具。

例如:

1
top

先看整体情况。

然后:

1
vmstat 1

观察系统整体运行状态。

再结合:

1
perf

分析 CPU 时间主要消耗在哪里。

如果怀疑:

大量任务正在等待 CPU。

就可以使用前面 eBPF 文章学过的:

1
/usr/share/bcc/tools/runqlat

观察:

任务在运行队列中等待 CPU 的时间。

所以整个思路可以变成:

flowchart TD
    A["CPU / Load 异常"] --> B["top / vmstat"]
    B --> C{"进一步观察"}

    C -->|"CPU 时间被程序大量消耗"| D["perf"]
    C -->|"任务排队等待 CPU"| E["runqlat"]
    C -->|"调度策略 / nice 异常"| F["chrt / ps"]
    C -->|"上下文切换异常"| G["/proc/PID/status"]

    D --> H["继续定位具体问题"]
    E --> H
    F --> H
    G --> H

这时候你会发现:

CPU 调度并不是一个孤立的知识点。

它正好把我们前面学过的很多性能工具串了起来。


几个非常容易混淆的概念

最后把这一篇最容易混淆的几个概念集中放在这里。

1. 进程 ≠ 调度单位

Linux 真正调度的是:

1
线程

不是我们平时口语中的“进程”。


2. SCHED_IDLE ≠ Idle 调度类

1
SCHED_IDLE

是一个极低优先级的任务调度策略。

而:

1
Idle scheduling class

属于调度器内部的空闲类别。两者完全不是一回事。


3. nice ≠ 实时优先级

1
nice

主要影响普通调度任务。

而:

1
rtprio

对应实时调度优先级。不要混在一起。


4. CFS ≠ EEVDF

在传统 Linux 内核中:

1
CFS

是经典公平调度器。而现代 Linux 内核已经逐渐转向:

1
EEVDF

RHEL 10 官方已经明确采用 EEVDF。所以以后看到:

1
2
3
sched_min_granularity_ns
sched_latency_ns
sched_wakeup_granularity_ns

不要直接套用到 RHEL 10。


这一篇真正应该记住什么?

如果今天的内容全部忘掉,只记住下面这张图:

flowchart TD
    A[线程 Runnable] --> B[CPU Run Queue]
    B --> C[Linux Scheduler]

    C --> D{Scheduling Class}

    D --> E[Stop]
    D --> F[Deadline]
    D --> G[Real-time]
    D --> H[Fair]

    H --> I[SCHED_OTHER]
    H --> J[SCHED_BATCH]
    H --> K[SCHED_IDLE]

    I --> L[EEVDF]
    J --> L
    K --> L

    L --> M[选择下一个任务]
    M --> N[CPU 执行]

    N --> O{状态变化}
    O -->|等待 I/O| P[阻塞]
    O -->|被抢占| B
    O -->|继续运行| M
    O -->|退出| Q[结束]

然后记住几个最关键的命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 查看线程
ps -L

# 查看调度策略
chrt -p PID

# 查看 nice
ps -eo pid,tid,ni,pri,rtprio,policy,comm

# 修改 nice
renice 10 -p PID

# 查看上下文切换
grep -E 'voluntary|nonvoluntary' /proc/PID/status

# 查看调度器统计
cat /proc/schedstat

# 查看 EEVDF 相关调度参数
ls /sys/kernel/debug/sched/
cat /sys/kernel/debug/sched/base_slice_ns

如果服务器出现 CPU 负载高的问题,也不要再停留在:

1
2
3
4
5
top
↓
CPU 99%
↓
CPU 很高

而应该逐渐形成这样的思维:

1
2
3
4
5
6
7
8
9
10
11
12
13
CPU 很高
↓
是谁在消耗 CPU?
↓
任务有没有排队?
↓
调度延迟怎么样?
↓
是什么调度策略?
↓
nice / 调度策略是否合理?
↓
上下文切换是否异常?

这才是真正开始理解:

Linux CPU 调度。