IRQ Affinity —— 中断亲和性:把中断绑到指定 CPU
"网卡中断落在了跑 Redis 的核上,每次收包都打断 Redis,延迟抖动 10 倍。" —— 这是低延迟系统中 IRQ affinity 最经典的痛。中断亲和性(IRQ affinity)就是决定哪个(哪些)CPU 核负责处理某个硬件中断,通过
/proc/irq/N/smp_affinity位图控制。对高吞吐场景,把中断打散到多核可提升并发;对低延迟场景,把中断从关键业务核挪走可消除抖动。
本文涉及的 APIC 寄存器和中断投递硬件机制,速查见 x86-64-registers。中断控制器进化史前身(8259A PIC——IRR/ISR/IMR 三寄存器协作+ICW-OCW+INTA 握手+级联)见 8259。
一、IRQ affinity 要解决什么问题?
1.1 中断对业务核的"降维打击"
时间线(没有 IRQ affinity):
───────────────────────────────────────────────────────────────
CPU 0: [Redis 处理请求] [Redis 处理请求] [Redis 处理请求]
↑ 网卡中断到达!
↑ CPU 0 被迫停下 Redis,切到内核态处理中断
↑ 中断处理:存现场 → NAPI poll → 收包 → 软中断 → 唤醒
↑ 若干 μs 后,CPU 0 继续跑 Redis
问题:Redis 的请求延迟本来 50μs,被中断打断后变成 80μs(多了 30μs 中断处理)
如果每秒几千次中断,Redis 几乎每微秒都被打断 → 延迟严重抖动1.2 IRQ affinity 的解决思路
时间线(有 IRQ affinity):
───────────────────────────────────────────────────────────────
CPU 0: [Redis 处理请求] [Redis 处理请求] [Redis 处理请求] ... ← 不被打断!
CPU 1: [idle] [网卡中断→收包] [idle] [网卡中断→收包] ... ← 专门处理中断核心思想:把中断处理集中到专用的 CPU 核,让关键业务核不被异步打断。
1.3 两种策略的权衡
| 策略 | 做法 | 适用场景 | 效果 |
|---|---|---|---|
| 集中 | 所有中断绑到同一个核 | 低延迟应用(Redis/HFT/实时系统) | 关键业务核不被打断,延迟稳定 |
| 打散 | 不同 IRQ 绑到不同核 | 高吞吐(Nginx/Envoy/网关) | 中断处理并行化,整体吞吐更高 |
| 默认(irqbalance) | 自动分散 | 通用场景 | 中庸方案 |
二、硬件层面:中断是怎么到达特定 CPU 的?
2.1 APIC 中断投递系统
x86-64 上,中断投递由两级 APIC 控制:
2.2 IO-APIC 重定向表条目(RTE)
每个 IRQ 在 IO-APIC 中有一个 64 位的重定向表条目(Redirection Table Entry):
IO-APIC RTE (64 bits) 简化结构:
┌─────────────────────────────────────────────────────────────┐
│ 63:56 │ 55:32 │ 31:17 │ 16 │ 15:8 │ 7:0 │
│ 目标 │ 保留 │ 保留 │ 屏蔽位 │ 向量 │ 投递模式+状态 │
└─────────────────────────────────────────────────────────────┘
↑
└── Destination Field (8 bits)
物理模式:APIC ID(4 bits 有效,最多 16 核)
逻辑模式:逻辑 APIC ID(8 bits 位图,最多 8 核)
投递模式(Delivery Mode):
000 = Fixed → 发给指定目标
001 = Lowest Priority → 发给目标集合中优先级最低的核
100 = NMI → 不可屏蔽中断
111 = ExtINT → 兼容 8259A 模式IRQ affinity 的硬件本质:修改 IO-APIC 重定向表中的 Destination Field 和 投递模式,告诉 IO-APIC"这个 IRQ 只能发给哪些 CPU"。
2.3 MSI/MSI-X 的中断亲和性
现代 PCIe 设备(网卡/NVMe/GPU)使用 MSI(Message Signaled Interrupts)或 MSI-X,不经过 IO-APIC,直接在 PCIe 事务层写 Local APIC 寄存器:
MSI 中断投递路径:
设备 → PCIe TLP (写 Local APIC 地址) → Root Complex → Local APIC → CPU
MSI-X 表条目(每个中断向量一条):
┌──────────────────────────────────────────┐
│ Message Address │ Message Data │ 控制 │
├──────────────────┼──────────────┼────────┤
│ 目标 APIC ID + │ 中断向量号 │ 屏蔽位 │
│ 投递模式 │ │ │
└──────────────────────────────────────────┘
修改 MSI-X 的亲和性 → 修改 Message Address 中的目标 APIC ID
内核接口:irq_set_affinity() → 设备驱动 → 写 MSI-X 表三、软件接口:/proc/irq/ 下的亲和性控制
3.1 核心文件
# 每个 IRQ 都有一个目录
ls /proc/irq/
# 1 3 4 7 8 9 10 11 12 13 14 15 ... 126 127 ...
# 每个目录下的亲和性相关文件
ls /proc/irq/126/
# smp_affinity ← 亲和性位图(要写的内容)
# smp_affinity_list ← 亲和性 CPU 列表(更友好的格式)
# node ← 该 IRQ 关联的 NUMA 节点3.2 smp_affinity:位图格式
# 查看 IRQ 126 当前绑定到哪些 CPU
cat /proc/irq/126/smp_affinity
# 00000000,00000000,00000000,00000000,00000000,00000000,00000000,000000ff
# ↑ 这是一个 256 位(8×32 位)的位图,支持最多 256 个 CPU
# 每 8 个十六进制字符对应 32 位
# 位图解读:
# 最低位 (bit 0) = CPU 0
# bit 1 = CPU 1
# ...
# 示例中的 ff = 11111111 (bit 0-7 全为 1)
# 表示 IRQ 126 可以被 CPU 0-7 处理常用操作:
# 把 IRQ 126 只绑到 CPU 2
echo 4 > /proc/irq/126/smp_affinity # 4 = 0b100,bit 2 = 1
# 把 IRQ 126 绑到 CPU 0-3
echo f > /proc/irq/126/smp_affinity # f = 0b1111,bit 0-3 = 1
# 对于超过 32 核的系统,用完整位图
echo 0,0,0,0,0,0,0,10 > /proc/irq/126/smp_affinity
# 上面的 10(十六进制)= 16(十进制)= bit 4
# 更友好的 smp_affinity_list
cat /proc/irq/126/smp_affinity_list # 0-3(当前在 CPU 0-3)
echo "2" > /proc/irq/126/smp_affinity_list # 只绑到 CPU 2
echo "0,2,4" > /proc/irq/126/smp_affinity_list # 绑到 CPU 0,2,43.3 实际操作:给网卡中断绑核
# 1. 找到网卡对应的 IRQ
ethtool -i eth0 | grep bus-info # 找到 PCIe 地址(如 0000:01:00.0)
# 2. 查看该网卡的所有中断向量
ls /proc/irq/ | while read irq; do
grep -l "eth0" /proc/irq/$irq/* 2>/dev/null && echo "IRQ $irq → eth0"
done
# 或者更直接:网卡的 MSI-X 向量通常在 /sys 下
ls /sys/class/net/eth0/device/msi_irqs/
# 126 127 128 129 130 131 132 133 (8 个 MSI-X 向量)
# 3. 查看当前亲和性
cat /proc/irq/126/smp_affinity_list # 比如 0-7(默认全核)
# 4. 逐个绑定
echo "2" > /proc/irq/126/smp_affinity_list # 队列 0 的中断 → CPU 2
echo "3" > /proc/irq/127/smp_affinity_list # 队列 1 的中断 → CPU 3
echo "4" > /proc/irq/128/smp_affinity_list # 队列 2 的中断 → CPU 4
echo "5" > /proc/irq/129/smp_affinity_list # 队列 3 的中断 → CPU 5
# ... 以此类推
# 5. 对于 NVMe 磁盘中断同理
ls /sys/class/nvme/nvme0/device/msi_irqs/3.4 脚本化:批量设置中断亲和性
#!/bin/bash
# set_irq_affinity.sh — 将 eth0 的中断均匀分散到 CPU 2-9
IRQ_BASE=2 # 从 CPU 2 开始
IFACE=eth0
for irq in $(ls /sys/class/net/$IFACE/device/msi_irqs/); do
echo "$IRQ_BASE" > /proc/irq/$irq/smp_affinity_list
echo "IRQ $irq → CPU $IRQ_BASE"
((IRQ_BASE++))
done四、irqbalance:自动管理的守护进程
4.1 irqbalance 的工作原理
irqbalance 是一个用户态守护进程,它自动根据系统负载决定每个 IRQ 的最佳目标 CPU。
irqbalance 的决策逻辑:
1. 识别设备类型
→ 网卡、NVMe、GPU、USB 等被分类为不同"设备类"
→ 同一设备类的 IRQ 尽量放在同一"调度域"(NUMA 节点)
2. 分析当前负载
→ 读取 /proc/stat 各 CPU 的 irq/softirq 占比
→ 计算每个 CPU 的"中断负载"
3. 选择目标 CPU
→ 从该设备所属 NUMA 节点的 CPU 中选负载最低的
→ 如果是多队列设备(网卡),将不同队列分到不同 CPU
4. 逐层分组
┌─ NUMA 节点 0 ────────────┐ ┌─ NUMA 节点 1 ────────────┐
│ cache domain (LLC 共享) │ │ cache domain (LLC 共享) │
│ ┌─ 核 0 ─┐ ┌─ 核 1 ─┐ │ │ ┌─ 核 8 ─┐ ┌─ 核 9 ─┐ │
│ │ IRQ 126│ │ IRQ 127│ │ │ │ IRQ 128│ │ IRQ 129│ │
│ └────────┘ └────────┘ │ │ └────────┘ └────────┘ │
└─────────────────────────┘ └─────────────────────────┘4.2 什么时候该关掉 irqbalance?
| 场景 | 建议 | 原因 |
|---|---|---|
| 低延迟应用(Redis/HFT) | 关掉 | irqbalance 可能把中断挪到业务核 |
| 自己手动绑核 | 关掉 | irqbalance 会覆盖你的设置 |
| 使用 isolcpus 隔离核 | 关掉 | irqbalance 不感知 isolcpus,可能把中断放到隔离核 |
| 通用服务器 | 开着 | 默认策略对大多数场景足够好 |
# 停止并禁用 irqbalance
systemctl stop irqbalance
systemctl disable irqbalance
# 查看 irqbalance 的状态和它做了哪些决策
irqbalance --debug # 前台运行,输出决策日志4.3 irqbalance 的局限性
irqbalance 不知道的事:
✗ 不知道 CPU 2 上跑着 Redis —— 它只看中断负载,不看业务负载
✗ 不知道某个网卡队列的中断和对应的应用线程应该同核(数据局部性)
✗ 不知道你的延迟目标(100μs p99)
✗ 不知道 isolcpus 的存在(可能把中断放到隔离核上)
因此:低延迟系统几乎总是需要手动设置 IRQ affinity + 关掉 irqbalance五、网卡多队列(RSS)与中断亲和性的配合
5.1 为什么网卡需要多个 MSI-X 向量?
现代网卡(10G/25G/100G)支持 RSS(Receive Side Scaling)——硬件层面将收到的包按哈希(五元组:src IP/dst IP/src port/dst port/protocol)分配到多个接收队列,每个队列有自己独立的 MSI-X 中断向量:
网卡硬件 RSS:
收到的包 → 哈希(五元组) → 队列 0 → MSI-X 向量 0 → CPU 2
队列 1 → MSI-X 向量 1 → CPU 3
队列 2 → MSI-X 向量 2 → CPU 4
队列 3 → MSI-X 向量 3 → CPU 5
...5.2 最佳实践:队列-中断-CPU 的一一对应
理想配置(8 队列网卡,CPU 2-9 专门处理网络):
CPU 2 ← IRQ 126 (队列 0 中断) ← 网卡队列 0
用户态线程 A (收队列 0 的包) → 绑到 CPU 2
CPU 3 ← IRQ 127 (队列 1 中断) ← 网卡队列 1
用户态线程 B (收队列 1 的包) → 绑到 CPU 3
...
好处:
1. 中断和业务线程在同一核 → 软中断处理完立即唤醒业务线程,无跨核唤醒开销
2. 数据在 L1/L2 cache 中 → 中断刚写完数据,业务线程就能从 cache 读到
3. 无锁 → 每个队列独占一个核,队列操作不需要锁这就是 DPDK 和 io_uring 高性能网络栈的核心思想:让中断、软中断、用户态处理都在同一个核上完成,数据不用跨核。
5.3 RSS 哈希与中断亲和性的配置
# 查看网卡队列数和当前 RSS 配置
ethtool -l eth0 # 队列数量
ethtool -x eth0 # RSS 哈希键和间接表
ethtool -S eth0 | grep rx_queue # 各队列收包统计
# 设置队列数(需要先 down 网卡)
ethtool -L eth0 combined 8 # 8 个收发队列
# 配置 RSS 哈希字段
ethtool -n eth0 rx-flow-hash tcp4 sdfn # TCP 用 src/dst IP + src/dst port
# 查看各队列中断是否均匀
cat /proc/interrupts | grep eth0
# 126: 12345678 ... IR-PCI-MSI eth0-0
# 127: 12400000 ... IR-PCI-MSI eth0-1
# 128: 12380000 ... IR-PCI-MSI eth0-2
# 各队列中断数应该大致均匀;如果不均匀,检查 RSS 哈希配置六、IRQ affinity 在不同设备上的实践
6.1 网卡
# 典型高吞吐配置:网卡中断分散到多个核
# CPU 0 留给系统(调度器/内核线程)
# CPU 1-8 处理网卡中断
# CPU 9-15 跑业务线程
for i in $(seq 0 7); do
cpu=$((i + 1))
irq=$(cat /sys/class/net/eth0/device/msi_irqs/ | head -n $((i+1)) | tail -1)
echo $cpu > /proc/irq/$irq/smp_affinity_list
done6.2 NVMe 磁盘
# NVMe 磁盘也支持多队列,但中断亲和性策略不同
# 磁盘 IO 的瓶颈通常在 IOPS/带宽而非 CPU
# 一般把 NVMe 中断绑到离该 NVMe 设备 PCIe 近的 NUMA 节点的核上
# 查看 NVMe 的 NUMA 节点
cat /sys/class/nvme/nvme0/device/numa_node # 0 或 1
# 查看 NVMe 的 MSI-X 向量
ls /sys/class/nvme/nvme0/device/msi_irqs/
# 通常 8-16 个向量(比网卡少)
# 绑到同 NUMA 节点的核
numactl -N 0 --cpu-list 2-9 # 确认 CPU 2-9 属于 NUMA 0
for irq in $(ls /sys/class/nvme/nvme0/device/msi_irqs/); do
echo "2-9" > /proc/irq/$irq/smp_affinity_list
done6.3 GPU
# GPU 中断:通常只需保证不被隔离核(isolcpus)处理即可
# 对 GPU 中断的延迟要求不高(毫秒级可接受)
# 查看 GPU 的中断
grep -i nvidia /proc/interrupts
# 或
grep -i amdgpu /proc/interrupts七、中断亲和性与 isolcpus/nohz_full 的配合
在极致低延迟场景中,IRQ affinity 需要与 isolcpus 和 nohz_full 协同使用:
# 内核启动参数
isolcpus=2-7 nohz_full=2-7 rcu_nocbs=2-7 irqaffinity=0-1
# isolcpus=2-7 → CPU 2-7 从调度器默认负载均衡摘出,只跑显式绑定的线程
# nohz_full=2-7 → CPU 2-7 只有一个任务时关掉调度时钟中断(tickless)
# rcu_nocbs=2-7 → RCU 回调不在 CPU 2-7 执行
# irqaffinity=0-1 → 所有非显式绑定的中断默认只去 CPU 0-1效果:
CPU 0-1 → 处理所有系统中断 + 内核线程 + 杂务
CPU 2-7 → 只跑你的关键业务线程,几乎不被打断
(只有显式绑到它们的网卡中断会来,这正是你想要的)八、观测与调试
# 1. 查看所有中断及其在各 CPU 上的计数
cat /proc/interrupts
# 2. 实时监控中断速率
watch -n1 'cat /proc/interrupts | head -20'
# 3. 用 mpstat 看各核的 %irq 和 %soft
mpstat -P ALL 1
# 4. 查看当前所有 IRQ 的亲和性
for irq in $(ls /proc/irq/); do
[ -f /proc/irq/$irq/smp_affinity_list ] || continue
name=$(cat /proc/irq/$irq/actions 2>/dev/null | head -1 || echo "unknown")
cpus=$(cat /proc/irq/$irq/smp_affinity_list)
echo "IRQ $irq → CPUs [$cpus] $name"
done | sort -t' ' -k2 -n
# 5. 查看 irqbalance 的决策日志
journalctl -u irqbalance --since "5 minutes ago"
# 6. 用 perf 看中断处理的热点
perf record -e irq:irq_handler_entry -a -g -- sleep 10
perf report
# 7. 统计某个 IRQ 在哪个 CPU 上被处理
cat /proc/irq/126/effective_affinity_list # 硬件实际投递到的 CPU
cat /proc/irq/126/smp_affinity_list # 你设置的亲和性
# 注意:effective_affinity 是 smp_affinity 的子集(硬件可能无法满足全部位)九、常见问题与排查
| 症状 | 可能原因 | 排查命令 |
|---|---|---|
| 设置了 smp_affinity 但不生效 | irqbalance 在运行,覆盖了设置 | systemctl status irqbalance |
| 中断全集中在 CPU 0 | 默认没有打散,irqbalance 也没开 | 手动设置或开启 irqbalance |
| effective_affinity 和 smp_affinity 不一致 | 硬件限制(如 IO-APIC 物理模式只支持前 16 核) | cat /proc/irq/N/effective_affinity_list |
| 某些核的 %soft 极高 | 软中断处理集中在该核 | 调整 RPS(Receive Packet Steering)或 RSS 队列数 |
| 延迟抖动(p99 远大于 p50) | 中断落到了业务核上 | 检查业务核的 /proc/interrupts 计数 |
十、与仓库其他文档的关系
- 中断处理全貌:interrupts —— 中断分类体系、完整处理流程、上半部/下半部机制、开销量化
- 线程亲和性:thread-affinity —— 线程绑核(CPU affinity),与本文的中断绑核互为补充:线程绑核管"谁在哪个核上跑",中断绑核管"中断去哪个核"
- 调度器:scheduling —— 调度域、负载均衡、isolcpus 如何影响中断处理
- 上下文切换:context-switch —— 中断打断用户线程后触发的切换开销
- 网络内核调优:kernel-tuning-net —— RSS/RPS/RFS 与中断亲和性的配合
- 网卡收包:interrupts §四 —— NAPI 与软中断的完整收包流程
十一、一句话总结
IRQ affinity 是把硬件中断绑定到指定 CPU 的能力——通过修改 IO-APIC 重定向表(或 MSI-X 的 Message Address)中的目标 APIC ID 实现,用户态通过
/proc/irq/N/smp_affinity位图控制。对低延迟系统,把中断从关键业务核挪走是消除延迟抖动的第一步(配合 isolcpus + nohz_full);对高吞吐系统,把网卡多队列中断均匀分散到多核可并行收包。irqbalance 提供自动管理但不懂你的业务语义,低延迟场景必须关掉它并手动设置。