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

联系方式:

1. 微信:Lxh_Chat

2. 邮箱:939958092@qq.com

上一篇我们讲了实时调度策略。但你可能已经发现,把任务设成 SCHED_FIFO 优先级 50,延迟抖动依然存在。这一篇我们来讲:优先级之外,还有哪些东西在干扰你的任务,怎么把它们赶走。

先看一个典型现象,我在培训里经常遇到这样的提问:

老师,机器 CPU 才 20%,业务线程也设了实时优先级,为什么 P99 延迟还是时不时飙一下?

这个问题的答案,往往不在调度策略里,而在”这个 CPU 上到底还有谁”。

为什么要关心 CPU 在哪里跑

线程迁移让缓存变冷

线程从 CPU 2 迁到 CPU 5,它在 CPU 2 的 L1/L2 里积累的热数据就用不上了。数据并没有”失效”,而是在新 CPU 上变冷了,要重新从内存或 L3 加载。这就是延迟抖动的一个来源。

中断会直接打断你

网卡收包、磁盘完成 I/O,都会产生中断。中断不看你的线程优先级,来了就打断。 你的实时线程再高优先级,也得让路。

同一个 CPU 上还住着别人

后台服务、监控 agent、kworker、ksoftirqd 都可能在你的 CPU 上排队。

两个概念要分清:

概念回答的问题性质
CPU 亲和性这个任务只能在哪些 CPU 上跑?限制任务自己
CPU 隔离别的东西能不能不要来这些 CPU?限制别人

很多人只做了前者就以为万事大吉,这是最常见的误区,后面会专门讲。

flowchart TB
    G["目标:关键线程在 CPU 2-3 上不被打扰"] --> A["业务线程绑核<br/>taskset / CPUAffinity / cpuset"]
    G --> B["其他用户态进程让开<br/>系统服务只用 CPU 0-1"]
    G --> C["中断让开<br/>smp_affinity / irqbalance / managed_irq"]
    G --> D["内核干扰降低<br/>nohz_full / rcu_nocbs"]
    G --> E["实测验证<br/>/proc/interrupts / rtla / cyclictest"]

这五件事缺一不可,后面的内容就是按这张图逐个展开。

先学会看 CPU 拓扑

CPU 列表与位掩码

Linux 里表示 CPU 集合有两种写法,很容易混淆。

列表写法,人类友好:

1
2
3
0-3        # CPU 0,1,2,3
0,2,4 # CPU 0,2,4
0-1,3-5 # CPU 0,1,3,4,5

十六进制位掩码,每一位对应一个 CPU,最低位是 CPU 0:

1
2
3
f   = 1111 → CPU 0-3
c = 1100 → CPU 2,3
4 = 0100 → CPU 2

这很重要,因为不同接口吃不同的格式:

接口格式
taskset -p 的输出掩码
taskset -cp 的输出列表
/proc/irq/N/smp_affinity十六进制掩码
/proc/irq/N/smp_affinity_list列表

写错格式的后果很有意思:往 smp_affinity 里 echo 2,你以为是 CPU 2,实际是掩码 0x2 = CPU 1。新手建议一律用 _list 结尾的接口,不容易算错。

例如:

1
2
3
4
[root@localhost ~]# taskset -p 1013
pid 1013's current affinity mask: f
[root@localhost ~]# taskset -cp 1013
pid 1013's current affinity list: 0-3

lscpu 看什么

1
lscpu
字段含义
CPU(s)逻辑 CPU 总数
Thread(s) per core每个物理核有几个逻辑核(超线程,大于 1 说明开了 SMT)
Core(s) per socket每个插槽有几个物理核
Socket(s)插槽数
NUMA node(s)NUMA 节点数
On-line CPU(s) list当前在线的 CPU

看老李的样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
[root@localhost ~]# lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 45 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 4
On-line CPU(s) list: 0-3
Vendor ID: GenuineIntel
BIOS Vendor ID: GenuineIntel
Model name: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
BIOS Model name: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz CPU @ 2.3GHz
BIOS CPU family: 2
CPU family: 6
Model: 141
Thread(s) per core: 1
Core(s) per socket: 2
Socket(s): 2
Stepping: 1
BogoMIPS: 4608.00
Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat p
se36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm c
onstant_tsc arch_perfmon rep_good nopl xtopology tsc_reliable nonst
op_tsc cpuid tsc_known_freq pni pclmulqdq vmx ssse3 fma cx16 pcid s
se4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f
16c rdrand hypervisor lahf_lm abm 3dnowprefetch ssbd ibrs ibpb stib
p ibrs_enhanced tpr_shadow ept vpid ept_ad fsgsbase tsc_adjust bmi1
avx2 smep bmi2 erms invpcid avx512f avx512dq rdseed adx smap avx51
2ifma clflushopt clwb avx512cd sha_ni avx512bw avx512vl xsaveopt xs
avec xgetbv1 xsaves user_shstk arat vnmi avx512vbmi umip pku ospke
avx512_vbmi2 gfni vaes vpclmulqdq avx512_vnni avx512_bitalg avx512_
vpopcntdq rdpid movdiri movdir64b fsrm avx512_vp2intersect md_clear
flush_l1d arch_capabilities
Virtualization features:
Virtualization: VT-x
Hypervisor vendor: VMware
Virtualization type: full
Caches (sum of all):
L1d: 192 KiB (4 instances)
L1i: 128 KiB (4 instances)
L2: 5 MiB (4 instances)
L3: 48 MiB (2 instances)
NUMA:
NUMA node(s): 1
NUMA node0 CPU(s): 0-3
Vulnerabilities:
Gather data sampling: Unknown: Dependent on hypervisor status
Indirect target selection: Mitigation; Aligned branch/return thunks
Itlb multihit: KVM: Mitigation: Split huge pages
L1tf: Not affected
Mds: Not affected
Meltdown: Not affected
Mmio stale data: Not affected
Old microcode: Not affected
Reg file data sampling: Not affected
Retbleed: Not affected
Spec rstack overflow: Not affected
Spec store bypass: Mitigation; Speculative Store Bypass disabled via prctl
Spectre v1: Mitigation; usercopy/swapgs barriers and __user pointer sanitizatio
n
Spectre v2: Mitigation; Enhanced / Automatic IBRS; IBPB conditional; PBRSB-eIBR
S SW sequence; BHI SW loop, KVM SW loop
Srbds: Not affected
Tsa: Not affected
Tsx async abort: Not affected
Vmscape: Not affected
[root@localhost ~]#

超线程兄弟核

如果开着超线程,两个逻辑核共享同一个物理核的执行单元。你隔离了 CPU 2,但它的兄弟核 CPU 6 上跑着别的负载,照样会干扰你。

1
2
3
4
5
6
7
8
[root@localhost ~]# cat /sys/devices/system/cpu/cpu2/topology/thread_siblings_list
2
[root@localhost ~]# lscpu -e # 看每个逻辑 CPU 对应的 CORE 和 NODE
CPU NODE SOCKET CORE L1d:L1i:L2:L3 ONLINE
0 0 0 0 0:0:0:0 yes
1 0 0 1 1:1:1:0 yes
2 0 1 2 2:2:2:1 yes
3 0 1 3 3:3:3:1 yes

生产建议:隔离的时候,要么把兄弟核成对隔离,要么干脆关闭 SMT。

NUMA:CPU 和内存是”邻居”关系

多路服务器上,每个插槽有自己的核心、缓存和本地内存。访问本地内存快,访问别的节点的内存慢。

现代 CPU 一个插槽内也可能划成多个 NUMA 节点(比如 AMD 的 NPS 模式、Intel 的 SNC),所以别想当然地认为”一个插槽 = 一个节点”,以 lscpu 为准。

1
2
3
4
5
6
7
8
9
[root@localhost ~]# dnf install numactl -y
[root@localhost ~]# numactl -H
available: 1 nodes (0)
node 0 cpus: 0 1 2 3
node 0 size: 7903 MB
node 0 free: 7248 MB
node distances:
node 0
0: 10

重点看每个节点有哪些 CPU、多少内存,以及节点之间的 distance。

这对绑核的意义是:绑核不能只看 CPU 编号。 你把线程绑到节点 1 的 CPU,但内存是在节点 0 上分配的,每次访问都要跨节点,延迟反而更差。

让 CPU 和内存都限制在节点 0:

1
2
[root@localhost ~]# numactl --cpunodebind=0 --membind=0 sleep 100000 &
[1] 4879

看每个节点的内存分配情况:

1
2
3
4
5
6
7
8
9
[root@localhost ~]# numastat -p $(pidof sleep)

Per-node process memory usage (in MBs)
PID Node 0 Total
---------------- --------------- ---------------
4879 (sleep) 1.87 1.87 #看这一行
5265 (sleep) 1.97 1.97
---------------- --------------- ---------------
Total 3.84 3.84

--cpunodebind=0:只允许进程运行在 NUMA 节点 0 的 CPU 上。这是 CPU 亲和性,效果类似 taskset,但是按”节点”而不是按 CPU 编号来指定。

--membind=0:只允许进程从节点 0 的内存里分配。这是严格限制:如果节点 0 的内存用完了,不会去别的节点借,而是直接分配失败,严重时触发 OOM。

两个合起来的意思是:线程只在节点 0 的 CPU 上跑,内存也只从节点 0 分配,所以所有内存访问都是本地访问,没有跨节点的额外延迟。

flowchart LR
    subgraph N0["NUMA 节点 0"]
        C0["CPU 0-3"] --- M0["本地内存"]
    end
    subgraph N1["NUMA 节点 1"]
        C1["CPU 4-7"] --- M1["本地内存"]
    end
    P["进程<br/>cpunodebind=0<br/>membind=0"] --> C0
    P -.只用这里的内存.-> M0
    C0 -. 跨节点访问,慢 .-> M1

一个常见的坑:membind 太严格,--membind 是硬限制,节点内存不够就失败。如果你只是希望”优先用本地内存,不够再借别的节点”,改用:

1
numactl --cpunodebind=0 --preferred=0 sleep 10000 &
参数行为
--membind=0只能用节点 0 的内存,用完就失败
--preferred=0优先用节点 0,不够再用别的节点
--interleave=all内存在所有节点间轮流分配,适合需要大内存带宽、不在乎单次延迟的场景(比如数据库)
--localalloc在进程当前运行的 CPU 所在节点上分配内存

另一个相关写法,如果你想按具体 CPU 编号而不是节点来指定,用 --physcpubind(或 -C):

1
numactl --physcpubind=2,3 --membind=0 sleep 10000 &

这样 CPU 精确到 2、3,内存仍限定在节点 0。

还有一个常被忽略的点:网卡也挂在某个 NUMA 节点上。,处理这块网卡数据的业务线程,最好和网卡在同一个节点上。-1 的意思是:内核没有这块设备的 NUMA 节点归属信息,也就是”不属于任何特定节点 / 未知”。

1
2
3
4
5
[root@localhost ~]# ip -br a
lo UNKNOWN 127.0.0.1/8 ::1/128
ens160 UP 192.168.8.128/24 fe80::20c:29ff:fe29:c131/64
[root@localhost ~]# cat /sys/class/net/ens160/device/numa_node
-1

从网卡名 ens160 和 MAC 前缀 00:0c:29(VMware)来看,机器是 VMware 虚拟机,ens160 是虚拟网卡 vmxnet3,直接是-1,因为

  1. 虚拟机本身没有 NUMA 拓扑:只有一个 node 0,虚拟 PCI 设备也就没有”挂在哪个节点”的概念;
  2. 虚拟化层没有把这个信息传给客户机:即使宿主机是多路服务器,虚拟网卡也不是真实的物理 PCIe 设备,没有物理插槽归属;
  3. 物理机上偶尔也会出现:BIOS/ACPI 没有提供该设备的节点信息(比如 ACPI 表里缺少 _PXM,或者开启了节点交错 / NUMA 被关闭)。

taskset:快速验证的好工具

taskset 最适合”先试一下效果”。

查看,输出掩码

1
2
3
[root@localhost ~]# taskset -p 1013
pid 1013's current affinity mask: f

查看,输出列表(更直观)

1
2
3
[root@localhost ~]# taskset -cp 1013
pid 1013's current affinity list: 0-3

修改已运行的进程,注意要带 -p:

1
2
3
4
[root@localhost ~]# taskset -cp 2 1013
pid 1013's current affinity list: 0-3
pid 1013's new affinity list: 2

启动新进程时不带 -p:

1
2
3
4
5
6
[root@localhost ~]# taskset -c 2 sleep 10000 &
[2] 9398
[root@localhost ~]# taskset -p 9398
pid 9398's current affinity mask: 4
[root@localhost ~]# taskset -cp 9398
pid 9398's current affinity list: 2

这里和上一篇的 chrt 一样:改已有进程用 -p,启动新进程不用。 如果把 taskset -c 2 1013 当成改 PID,它会去执行一个叫 1013 的命令然后报错。

多线程程序的坑

taskset -cp 2 1013 只修改了线程 1013 自己,也就是主线程。进程里已经存在的其他线程不受影响。加 -a,修改进程内所有已存在的线程

1
taskset -acp 2 1013

另外,新创建的线程会继承创建者的亲和性,子进程也一样。

亲和性是硬限制

taskset 设置的是硬限制:线程只能在这些 CPU 上跑,即使别的 CPU 全空闲也不行。所以别绑得太死。如果你只绑一个 CPU,那个 CPU 上一旦有人占着,你的线程就只能等。taskset 的修改是临时的,进程重启就没了,只适合验证。

systemd:让绑核”留下来”

生产环境里的服务应该交给 systemd 管理。

1
2
[root@localhost ~]# mkdir /etc/systemd/system/sshd.service.d/
[root@localhost ~]# vi /etc/systemd/system/sshd.service.d/override.conf

在打开的编辑器里写:

1
2
[Service]
CPUAffinity=0-1

然后:

1
2
[root@localhost ~]# systemctl daemon-reload
[root@localhost ~]# systemctl restart sshd

验证

1
2
3
4
[root@localhost ~]# systemctl show sshd -p CPUAffinity
CPUAffinity=0-1
[root@localhost ~]# taskset -acp $(systemctl show sshd -p MainPID --value)
pid 11861's current affinity list: 0,1

这个例子其实很有代表性:**把 sshd 这类”非关键”服务绑到 CPU 0-1,就是在给隔离核让路。**还可以做全局设置。编辑 /etc/systemd/system.conf:

1
2
[Manager]
CPUAffinity=0-1

这样 systemd 启动的所有服务默认只用 CPU 0-1,需要关键 CPU 的服务再单独覆盖。改完建议重启,对新启动的服务才完整生效。

注意:CPUAffinity 只约束服务自己。它不会把别人赶出你的 CPU,也不处理中断。

cgroup v2 cpuset:按组划分 CPU

如果要给”一组任务”划地盘,用 cpuset。先确认是 cgroup v2:

1
2
[root@localhost ~]# stat -fc %T /sys/fs/cgroup
cgroup2fs

手工体验一遍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 第一步:让根 cgroup 把 cpuset 控制器交给子 cgroup
echo +cpuset > /sys/fs/cgroup/cgroup.subtree_control

# 第二步:创建子 cgroup
mkdir /sys/fs/cgroup/rt_group

# 第三步:设置可用 CPU
echo 2-3 > /sys/fs/cgroup/rt_group/cpuset.cpus

# 第四步:把进程放进去
echo 13400 > /sys/fs/cgroup/rt_group/cgroup.procs

# 验证
[root@localhost ~]# grep Cpus_allowed_list /proc/13400/status
Cpus_allowed_list: 2-3

第一步最容易漏。 不启用控制器,rt_group 目录里根本不会有 cpuset.cpus 文件。

关于内存节点,v2 里 cpuset.mems 默认继承父级,不是必须设置(v1 才是)。需要限制内存节点时再设:

1
echo 0 > /sys/fs/cgroup/rt_group/cpuset.mems

独占:用 partition,不是 cpu_exclusive

cpuset.cpu_exclusive 是 v1 的接口。v2 里要用 cpuset.cpus.partition:

1
2
3
4
[root@localhost ~]# echo isolated > /sys/fs/cgroup/rt_group/cpuset.cpus.partition
[root@localhost ~]# cat /sys/fs/cgroup/rt_group/cpuset.cpus.partition
isolated

它有三种取值:

值含义
member普通成员,默认值
root分区根,这些 CPU 独占给本组,组内仍然做负载均衡
isolated独占,并且关闭组内负载均衡,和 isolcpus 的效果类似,但可以动态调整

读回来如果看到 isolated invalid (...),括号里是失败原因,说明条件不满足,比如 CPU 被兄弟组占用了。

isolated 的一个后果:组内不再自动做负载均衡,你的线程不会自己跑到另一个 CPU 上去。所以组内线程要自己明确绑核,否则可能全挤在一个 CPU 上。

在 systemd 系统上更推荐

RHEL 上 cgroup 树是 systemd 管理的,直接手工 mkdir 容易和 systemd 打架。更稳妥的方式是让 systemd 来设:

1
2
3
[Service]
AllowedCPUs=2-3
AllowedMemoryNodes=0

或者临时调整:

1
systemctl set-property --runtime my_app.service AllowedCPUs=2-3

中断亲和性:让 IRQ 绕开你

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 看中断分布,每一列是一个 CPU 的累计次数
cat /proc/interrupts

# 看某个中断(比如 12)当前允许去哪些 CPU
[root@localhost ~]# cat /proc/irq/12/smp_affinity_list
0-3

# 实际生效的位置
[root@localhost ~]# cat /proc/irq/12/effective_affinity_list
0


# 修改
echo 0-1 > /proc/irq/12/smp_affinity_list

这里把中断绑到 CPU 0-1,也就是”系统核”,给 CPU 2-3 清净。还有一个容易忽略的参数:

1
2
[root@localhost ~]# cat /proc/irq/default_smp_affinity
ffffffff,ffffffff,ffffffff,ffffffff

它决定新注册的中断默认去哪,修改已有中断不会影响它。

为什么有时候改了没用

  1. irqbalance 在后台”纠正”你。 你手动改完,它下个周期又给你调回去了;
  2. managed IRQ。 现代多队列网卡和 NVMe,内核按 CPU 拓扑自动管理它们的中断,用户态改不了,写入通常会报 I/O 错误;
  3. 部分中断有设备或平台约束。

对于 managed IRQ,要靠内核参数 isolcpus=managed_irq,... 让内核自己避开隔离核,这点下面讲 tuned 时会再提到。

irqbalance:自动平衡的”双刃剑”

irqbalance 的目标是把中断均匀撒在各个 CPU 上,这对通用服务器很好,但和”隔离”的目标正好相反。

1
2
3
4
5
6
7
8
9
[root@localhost ~]# systemctl status irqbalance
○ irqbalance.service - irqbalance daemon
Loaded: loaded (/usr/lib/systemd/system/irqbalance.service; enabled; preset: enabled)
Active: inactive (dead)
Condition: start condition unmet at Sun 2026-10-04 12:40:16 CST; 2min 31s ago
└─ ConditionCPUs=>1 was not met
Docs: man:irqbalance(1)
https://github.com/Irqbalance/irqbalance

处理方式有两种。

方式一:直接停掉,完全手工管理

1
systemctl disable --now irqbalance

方式二:留着,但告诉它哪些 CPU 别碰

编辑 /etc/sysconfig/irqbalance:

1
IRQBALANCE_BANNED_CPULIST=2-3
1
systemctl restart irqbalance

这里禁的是 CPU:irqbalance 不会再往 CPU 2-3 上放中断。如果你想禁的是某个中断,用另一个参数:

1
IRQBALANCE_ARGS="--banirq=12"

这两个别混。很多文章把”禁 CPU”说成”禁中断”,导致配了半天不生效。

另外,如果你用了下面要讲的 tuned cpu-partitioning,它会自己处理 irqbalance 的禁用 CPU 列表,不要再手工去改,也不用手动停掉,两边都改容易互相覆盖。

tuned cpu-partitioning:生产环境的推荐姿势

前面这些手工操作,零散、容易漏、重启后要重做。生产环境推荐用 tuned 的 cpu-partitioning profile 一次性做到位。

它做了什么

flowchart TB
    T["tuned-adm profile cpu-partitioning"] --> K["内核启动参数<br/>isolcpus / nohz_full / rcu_nocbs"]
    T --> S["systemd 层面<br/>系统服务默认只用 housekeeping CPU"]
    T --> I["中断<br/>调整 irqbalance 禁用列表,迁移可迁移的 IRQ"]
    T --> Y["一批 sysctl 调整<br/>减少干扰"]
    K --> R["重启后生效"]

操作步骤

先装包,注意 profile 在单独的子包里,具体以 tuned-adm list 为准:

1
2
[root@localhost ~]# dnf install tuned tuned-profiles-cpu-partitioning -y

先写变量,再启用 profile,编辑 /etc/tuned/cpu-partitioning-variables.conf:

1
isolated_cores=2-3
1
2
3
[root@localhost ~]# systemctl enable tuned --now
[root@localhost ~]# tuned-adm profile cpu-partitioning
[root@localhost ~]# reboot

验证

1
2
3
[root@localhost ~]# tuned-adm active
[root@localhost ~]# tuned-adm verify
[root@localhost ~]# cat /proc/cmdline

cmdline 里应该能看到类似下面的内容,具体 flag 随 tuned 版本略有差异:

1
isolcpus=managed_irq,domain,2-3 nohz_full=2-3 rcu_nocbs=2-3

这几个内核参数到底干什么

参数含义
isolcpus=domain把这些 CPU 从调度域中拿掉,调度器不再往上做负载均衡,普通任务不会被自动迁过来
isolcpus=managed_irq让内核尽量不把 managed IRQ 放到这些 CPU 上
nohz_full当 CPU 上只有一个可运行任务时,停掉周期性调度 tick(忙碌时的 tick 抑制,不是空闲时)
rcu_nocbs把 RCU 回调处理从这些 CPU 卸载到别的 CPU 的内核线程上

nohz_full 有两个前提,不然不会生效:

  • 该 CPU 上同时只能有一个可运行任务;
  • 系统里必须留有 housekeeping CPU 来承接被卸载的工作(所以 CPU 0 通常不要隔离)。

一个很重要的认知:它不会帮你把业务绑过去

cpu-partitioning 只是清出了隔离核,不会把你的业务放进去。普通进程默认会被限制在 housekeeping CPU 上,你的关键业务必须显式绑核:

1
2
taskset -c 2-3 ./my_app
# 或者 systemd 里:CPUAffinity=2-3

tuna:交互式调整

tuna 适合现场排查和临时调整,可以同时看线程、中断,并移动它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[root@localhost ~]# dnf install tuna -y

[root@localhost ~]# tuna show_threads
thread ctxt_switches
pid SCHED_ rtpri affinity voluntary nonvoluntary cmd
1 OTHER 0 0,1 3625 5705 systemd
2 OTHER 0 0,1 375 3 kthreadd
3 OTHER 0 0,1 51 2 pool_workqueue_release
4 OTHER 0 0,1,2,3 2 0 kworker/R-rcu_gp
5 OTHER 0 0,1,2,3 2 0 kworker/R-sync_wq
6 OTHER 0 0,1 2 0 kworker/R-kvfree_rcu_reclaim
7 OTHER 0 0,1,2,3 2 0 kworker/R-slub_flushwq
8 OTHER 0 0,1 2 0 kworker/R-netns
9 OTHER 0 0 347 22 kworker/0:0-xfs-conv/dm-0
10 OTHER 0 0 2794 48 kworker/0:1-xfs-conv/dm-0
11 OTHER 0 0 4 0 kworker/0:0H-events_highpri
14 OTHER 0 0,1,2,3 2 0 kworker/R-mm_percpu_wq
15 OTHER 0 0 667 14 ksoftirqd/0

[root@localhost ~]# tuna show_irqs
# users affinity
0 timer 0,1,2,3
1 i8042 0,1
8 rtc0 0,1
9 acpi 0
12 i8042 0,1
14 ata_piix 0,1
15 ata_piix 0,1
16 vmwgfx 0

注意写法:先用 --threads / --irqs 选对象,再用 --move 执行动作。

1
2
3
4
5
6
7
# 把线程 1026 移到 CPU 2
[root@localhost ~]# taskset -acp $(systemctl show sshd -p MainPID --value)
pid 1026's current affinity list: 0,1
[root@localhost ~]# tuna move --cpus 2 --threads 1026

# 把中断 12 移到 CPU 2
tuna move --cpus=2 --irqs=12

还有一个很实用的动作,一键”清场”:

1
[root@localhost ~]# tuna isolate --cpus=2-3

它会把线程和中断尽量从 CPU 2-3 上挪走,挪不走的(比如每个 CPU 都有的内核线程)会保留。和 taskset 一样,tuna 的修改重启就丢,只用于测试,持久化交给 systemd 和 tuned。

内核线程到底赶不赶得走

这里要诚实地讲清楚一个很多资料不愿意说的事实:

隔离不等于清零。

内核里有些线程是每个 CPU 一个的,比如 migration/2、ksoftirqd/2、kworker/2:1。它们本来就是为这个 CPU 服务的,没法迁走。

所以在隔离核上执行 ps 看到这些线程是正常的。我们能做的是让它们少被触发:

  • 中断移走 → ksoftirqd 就没活干;
  • 业务不频繁触发内核工作 → kworker 就闲着;
  • nohz_full + rcu_nocbs → 减少 tick 和 RCU 的打扰。

低延迟场景的推荐流程

flowchart TD
    A["延迟抖动"] --> B{"确认是 CPU 竞争或中断造成的?<br/>先测量,不要盲目隔离"}
    B -- 是 --> C["规划:系统核 vs 业务核<br/>注意 SMT 兄弟核和 NUMA"]
    C --> D["tuned cpu-partitioning<br/>写变量 → 启用 → 重启"]
    D --> E["业务显式绑到隔离核"]
    E --> F["检查中断是否已避开"]
    F --> G["实测:cyclictest / rtla"]
    G --> H{"达标?"}
    H -- 否 --> I["查被忽略的干扰源<br/>回到第二步分析"]
    H -- 是 --> J["写进文档和配置管理"]

规划示例

1
2
CPU 0-1   系统核(housekeeping):系统服务、中断、监控
CPU 2-3 业务核:关键线程,注意是否是 SMT 兄弟

验证:不要只看配置,要看实际行为

看隔离核上有什么在跑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# psr 列是线程当前所在 CPU
[root@localhost ~]# ps -eLo psr,pid,tid,comm | awk '$1==2 || $1==3'
2 31 31 cpuhp/2
2 32 32 idle_inject/2
2 33 33 migration/2
2 34 34 ksoftirqd/2
2 35 35 kworker/2:0-slub_flushwq
2 36 36 kworker/2:0H-kblockd
3 38 38 cpuhp/3
3 39 39 idle_inject/3
3 40 40 migration/3
3 41 41 ksoftirqd/3
3 42 42 kworker/3:0-events
3 43 43 kworker/3:0H-events_highpri
2 65 65 kworker/2:1-events
3 72 72 kworker/3:1H
3 74 74 kworker/3:1-events
3 131 131 kworker/3:2
2 679 679 kworker/2:1H

除了你的业务线程和每 CPU 的内核线程,不该有别的。看中断有没有还往隔离核上打:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@localhost ~]# watch -d -n1 cat /proc/interrupts

CPU0 CPU1 CPU2 CPU3
0: 34 0 0 0 IR-IO-APIC 2-edge timer
1: 0 9 0 0 IR-IO-APIC 1-edge i8042
8: 0 0 0 0 IR-IO-APIC 8-edge rtc0
9: 0 0 0 0 IR-IO-APIC 9-fasteoi acpi
12: 15 0 0 0 IR-IO-APIC 12-edge i8042
14: 0 0 0 0 IR-IO-APIC 14-edge ata_piix
15: 0 0 0 0 IR-IO-APIC 15-edge ata_piix
16: 0 0 1334 0 IR-IO-APIC 16-fasteoi ehci_hcd:usb1, vmwgfx
18: 0 0 0 64 IR-IO-APIC 18-fasteoi uhci_hcd:usb2
19: 0 0 0 0 IR-IO-APIC 19-fasteoi snd_ens1371
24: 0 0 0 0 IR-PCI-MSI-0000:00:15.0 0-edge PCIe PME, pciehp
25: 0 0 0 0 IR-PCI-MSI-0000:00:15.1 0-edge PCIe PME, pciehp
26: 0 0 0 0 IR-PCI-MSI-0000:00:15.2 0-edge PCIe PME, pciehp

盯着 CPU 2、CPU 3 那两列,数字不该持续增长。LOC(本地定时器)一行在 nohz_full 生效后增长会非常慢,大约每秒一次,这是正常现象。

用工具量化干扰:

1
[root@localhost ~]# dnf install realtime-tests rtla -y
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 测调度延迟
[root@localhost ~]# cyclictest -m -p 95 -a 2-3 -t -D 10m -q
# /dev/cpu_dma_latency set to 0us
^CT: 0 ( 6042) P:95 I:1000 C: 22116 Min: 3 Act: 291 Avg: 246 Max: 9639
T: 1 ( 6043) P:95 I:1500 C: 14748 Min: 4 Act: 226 Avg: 306 Max: 9691

# 测"噪声",也就是 CPU 被别的东西偷走了多少时间(rtla 包,以你的环境为准)
[root@localhost ~]# rtla osnoise top -c 2-3

Operating System Noise
duration: 0 00:00:12 | time is in us
CPU Period Runtime Noise % CPU Aval Max Noise Max Single HW NMI IRQ Softirq Thread
2 #11 11000000 125037 98.86330 19823 215 3197 0 6752 251 0
3 #11 11000000 123761 98.87490 17730 94 3036 0 6721 295 0

常见误区

误区一:绑了核就等于隔离

不是。taskset、CPUAffinity 只限制了自己。其他进程、中断、内核线程照样可以来。

误区二:隔离越多越好

隔离核上的东西都不能跑,系统管理、监控、中断处理全挤在剩下的 CPU 上,反而会拖垮整体。建议:housekeeping 核至少保留 1-2 个物理核,并且给它们留够处理中断的余量。

误区三:只改 isolcpus 就够了

isolcpus 只管调度器不往上放普通任务。完整隔离还要配合 nohz_full、rcu_nocbs、中断迁移,这也是推荐用 tuned 的原因。

误区四:隔离了就不管 SMT 兄弟核

隔离 CPU 2,它的兄弟核 CPU 6 上跑着别的,物理核的执行资源仍然被共享。要么成对隔离,要么关掉 SMT。

误区五:tuna / taskset 改了会保留

它们是临时的。要持久化就用 systemd、tuned。

误区六:改了配置就不用验证

隔离的效果必须靠 /proc/interrupts、ps、cyclictest、rtla 来验证。没有量化数据,就不要说”优化好了”。

命令速查

目的命令
看 CPU 拓扑lscpu -e
看 SMT 兄弟核cat /sys/devices/system/cpu/cpuN/topology/thread_siblings_list
看 NUMAnumactl -H
看网卡所在 NUMA 节点cat /sys/class/net/eth0/device/numa_node
查看进程亲和性taskset -cp PID
修改已有进程(所有线程)taskset -acp 2 PID
启动并绑核taskset -c 2 ./cmd
看实际允许的 CPUgrep Cpus_allowed_list /proc/PID/status
看中断分布cat /proc/interrupts
设置中断亲和echo 0-1 > /proc/irq/N/smp_affinity_list
启用 tuned 分区tuned-adm profile cpu-partitioning
验证 tunedtuned-adm verify
动态移动线程tuna move --cpus=2 --threads=PID
动态移动中断tuna move --cpus=2 --irqs=N
测延迟cyclictest -m -p 95 -a 2-3 -t -D 10m -q

课后思考

  1. 为什么 taskset -cp 2 PID 对一个 16 线程的程序可能”没效果”?应该怎么改?
  2. 一台 8 核 16 线程(开着 SMT)的机器,你想隔离 CPU 4-7。thread_siblings_list 显示 4 和 12 是一对,那应该隔离哪些 CPU 才合理?
  3. isolcpus 和 cpuset.cpus.partition=isolated 都能关闭负载均衡,它们的主要区别是什么?什么场景下该用哪个?
  4. 隔离核上 ps 能看到 ksoftirqd/3,这是不是说明隔离失败了?

参考手册

1
2
3
4
5
6
7
man taskset
man lscpu
man numactl
man tuned-adm
man tuna
man systemd.exec
man irqbalance