1. 从一个真实的性能抖动说起前阵子帮一个朋友排查他服务器上的网络延迟问题现象很怪业务进程平时跑得好好的CPU占用也不高但每隔一段时间就会出现几毫秒到几十毫秒的延迟尖刺抓包看请求响应时间忽高忽低像是被什么东西突然打断了一样。用top看整体负载正常用perf top也没抓到什么异常的热点函数。折腾了大半天最后用cat /proc/interrupts一看好家伙某张网卡的中断号在几个CPU核心上的计数涨得飞快而且分布极不均匀几乎全压在了一个核心上。问题的根子就在Linux硬中断以及它的处理机制上。这件事让我意识到很多做应用层开发、甚至做运维的朋友对中断这个东西的理解是模糊的。大家知道它存在知道它和性能有关但真到了要定位问题、要调优的时候往往不知道从哪儿下手。所以我想借这个机会把Linux硬中断和中断处理这套东西从头到尾捋一遍既讲清楚它是什么、为什么这么设计也讲清楚实际工作中怎么观察它、怎么调它、踩过哪些坑。这篇文章适合所有需要和Linux性能打交道的读者——不管你是写后端服务的、做系统运维的还是单纯对操作系统底层好奇的都能从中拿到能直接用的东西。2. 硬中断到底是什么为什么它这么重要2.1 用生活化的类比理解中断先别急着上术语。你可以把CPU想象成一个正在专心写代码的程序员他手头有一堆任务要按顺序做。这时候电话响了、有人敲门、快递到了这些突发事件就是中断。程序员必须停下手里的活去处理这些突发情况处理完了再回来接着写。硬中断就是那些由硬件设备直接敲门触发的事件——网卡收到数据包了、键盘被按下了、磁盘读完数据了它们通过物理信号线向CPU发出请求。这个类比里有个关键点程序员停下手里的活去开门这个停和回来接着写的过程是有成本的。他得记住自己写到哪了、把当前的状态保存好处理完再恢复。CPU也一样中断处理涉及上下文保存和恢复这个开销就是中断处理成本的一部分。理解这一点后面很多性能问题就顺理成章了。2.2 硬中断的核心特征硬中断有几个绕不开的特征理解了它们才能理解后面的设计。第一是异步性中断什么时候来是不确定的CPU无法预测只能被动响应。第二是优先级不同硬件的中断有高低之分比如电源故障的中断优先级就比网卡收包高得多。第三是短暂性硬中断处理程序本身必须尽可能短因为它运行时会屏蔽同级甚至更低优先级的中断跑太久会导致其他中断丢失。这里要特别强调第三点它是整个Linux中断处理架构设计的出发点。硬件中断处理程序也就是常说的上半部top half如果干太多活比如直接在中断上下文里解析网络协议、拷贝大量数据那CPU就会被长时间占住其他设备的中断响应不了系统整体就卡了。所以Linux把中断处理拆成了两部分上半部只做最紧急、最必须马上做的事剩下的耗时工作推迟到下半部bottom half去完成。这个上下半部的拆分是理解Linux中断处理的一把钥匙。2.3 中断号和中断控制器的角色每个能产生中断的硬件设备在系统里都对应一个中断号IRQ number。你可以把它理解成设备的门牌号CPU收到中断信号后得知道是哪个设备在敲门才能调用对应的处理程序。在早期的系统里中断号直接对应物理连线比较简单。但现代系统设备多、中断多就引入了中断控制器这个中间层比如x86上常见的APIC高级可编程中断控制器。中断控制器干的事说白了就是前台接待它收集各个设备的中断请求按照优先级排队然后决定把哪个中断送给哪个CPU核心去处理。这个决定送给哪个核心的机制非常关键它直接关系到中断负载能不能均匀分摊到多个核心上。前面我朋友遇到的那个问题本质就是中断控制器把大量网卡中断都路由到了同一个核心导致那个核心疲于应付中断业务代码得不到足够的执行时间。3. 中断处理的分层设计上半部和下半部3.1 为什么必须拆分我刚开始学的时候也疑惑过为什么不干脆在中断处理程序里把所有事都干完非要拆成两半多麻烦后来自己写驱动、调性能才明白这个拆分是被逼出来的是被硬件特性和系统需求共同逼出来的。硬中断处理程序运行在中断上下文里这个上下文有几个要命的限制它不能睡眠不能主动让出CPU、不能调用可能阻塞的函数、不能访问用户空间。为什么因为中断上下文没有对应的进程一旦它睡眠了CPU调度器不知道该切换到谁整个核心就可能卡死。而很多耗时操作比如等待磁盘IO、申请内存恰恰是需要睡眠的。所以这些活没法在上半部干只能推迟。另外上半部执行时会关闭当前CPU上的中断响应至少是同级中断这段时间越长系统对突发事件的响应能力就越差。把耗时工作挪到下半部上半部就能飞快地返回中断响应延迟大大降低。这就是拆分的核心动机用最短的时间响应硬件用更宽松的环境处理后续逻辑。3.2 上半部快进快出上半部也就是硬中断处理程序本身它的职责非常明确确认中断、做最紧急的硬件操作、然后把后续工作登记到下半部最后尽快返回。举个网卡收包的例子上半部要做的是告诉网卡我收到你的中断了把网卡里的数据先搬到内核内存的缓冲区这一步通常用DMA完成CPU只是发起然后触发一个软中断NET_RX_SOFTIRQ去处理协议栈的解析。真正复杂的TCP/IP处理全在下半部。写上半部代码有个铁律能少干就少干。我见过一些新手写的驱动在上半部里直接做内存分配、打印日志、甚至加锁等待这些都是大忌。打印日志尤其坑printk在某些配置下可能触发大量操作放在中断上下文里会显著拉长中断处理时间。实测下来一个不当的printk能让中断处理时间从几微秒涨到几百微秒性能直接崩掉。3.3 下半部软中断、tasklet和工作队列下半部的实现机制在Linux历史上有过好几代现在主流的是三种软中断softirq、tasklet和工作队列workqueue。它们各有各的适用场景选错了会带来麻烦。软中断是下半部里最硬核的一种它本质上还是一段在内核里运行的代码但不在硬中断上下文里而是在一个专门的时机被调用。软中断的优点是执行快、开销小缺点是同一个软中断类型在多个CPU上可以并行执行写代码时必须自己处理并发问题。网络收包用的NET_RX_SOFTIRQ、定时器用的TIMER_SOFTIRQ都是软中断。软中断的数量是编译时固定的不能动态增加这也是它的一个限制。tasklet是建立在软中断之上的一层封装它保证同一个tasklet在任意时刻只会在一个CPU上运行天然避免了并发问题写起来省心。但正因为这个串行化保证它的并行能力不如软中断高并发场景下可能成为瓶颈。tasklet适合那种不需要并行、但需要推迟执行的中等耗时任务。工作队列则是把任务交给内核线程去执行运行在进程上下文里。这意味着工作队列里的代码可以睡眠、可以阻塞、可以调用大部分内核函数灵活度最高。代价是它涉及线程调度开销比软中断和tasklet大。所以工作队列适合那些确实需要睡眠的耗时操作比如需要等待IO的后续处理。机制运行上下文能否睡眠并行性典型场景软中断中断上下文否多CPU可并行网络收包、定时器tasklet中断上下文否同一tasklet串行中等耗时、无需并行工作队列进程上下文是由内核线程调度需要睡眠的耗时操作这张表建议存下来选下半部机制的时候对着看基本不会选错。4. 中断在Linux内核里的完整流转过程4.1 从硬件信号到处理函数一个中断从产生到被处理中间要经过好几个环节每个环节都可能出问题。我把它拆成一条链路来讲。硬件设备产生中断信号信号送到中断控制器。中断控制器根据配置决定把这个中断发给哪个CPU核心然后向该核心发送一个信号。CPU核心在当前指令执行完后检查到有中断请求就保存当前执行现场的寄存器状态然后根据中断号去查中断描述符表IDT找到对应的入口。这个入口是内核预先注册好的汇编代码它会进一步调用C语言写的中断处理框架最终找到设备驱动注册的那个处理函数。这条链路里中断控制器到CPU的路由是第一个关键点它决定了负载分布。IDT查找是第二个关键点它决定了调用哪个处理函数。处理函数执行是第三个关键点它决定了中断处理的实际耗时。排查中断相关问题时基本就是沿着这条链路一段段看。4.2 中断描述符表和中断向量IDT是CPU硬件层面的一个数据结构它把每个中断向量号映射到一段处理代码的地址。中断向量号是CPU架构相关的x86上有256个向量其中一部分留给异常比如除零、缺页一部分留给外部硬件中断。设备驱动注册中断时内核会分配一个中断号并把这个中断号和IDT里的某个向量关联起来。这里有个容易混淆的点中断号IRQ和中断向量vector不是一回事。IRQ是Linux内核层面给设备分配的编号vector是CPU硬件层面的编号。一个IRQ可能对应一个vector但多个IRQ也可能共享一个vector中断共享。理解这个区别在看/proc/interrupts和调试中断问题时很重要。4.3 中断共享与中断亲和性现代系统设备多中断线不够用所以支持中断共享多个设备可以共用一根中断线对应同一个IRQ。当这个IRQ触发时内核会依次调用所有注册在这个IRQ上的处理函数每个处理函数自己判断是不是我的设备触发的是就处理不是就返回。这就要求共享中断的处理函数必须能快速判断否则会拖慢其他设备。中断亲和性IRQ affinity是另一个重要概念它决定了某个IRQ的中断由哪些CPU核心处理。默认情况下内核会尽量把中断分散到各个核心但在某些场景下比如为了缓存局部性我们可能希望把某个网卡的中断固定到特定核心。通过/proc/irq/IRQ号/smp_affinity可以查看和设置亲和性。前面朋友的案例就是亲和性配置不当导致中断全压在一个核心上。5. 动手观察和调优中断5.1 用/proc/interrupts看清中断分布排查中断问题第一步永远是看/proc/interrupts。这个文件列出了每个IRQ在每个CPU上的中断计数是诊断中断分布最直接的工具。cat /proc/interrupts输出大概长这样我简化了CPU0 CPU1 CPU2 CPU3 1: 12 0 0 0 IO-APIC 1-edge i8042 24: 1234567 0 0 0 PCI-MSI xxx eth0-rx-0 25: 0 0 0 0 PCI-MSI xxx eth0-tx-0看这个输出重点看两件事一是哪个IRQ的计数涨得快说明那个设备中断频繁二是计数在CPU之间的分布如果全挤在一个核心上那就是亲和性问题。我朋友的问题就是eth0的rx中断全在CPU0上其他核心的计数几乎不动。提示/proc/interrupts的计数是累计值想看速率得隔几秒连续看两次做差。或者用watch -n 1 cat /proc/interrupts动态观察。5.2 调整中断亲和性发现中断分布不均就可以手动调整亲和性。假设要把IRQ 24的中断分到CPU2和CPU3上操作如下# 查看当前亲和性输出是一个十六进制掩码 cat /proc/irq/24/smp_affinity # 设置亲和性掩码0x0c表示CPU2和CPU3二进制1100 echo 0c /proc/irq/24/smp_affinity掩码的计算方式是第n位为1表示允许第n个CPU处理。CPU0是第0位CPU1是第1位以此类推。0x0c 的二进制是 1100对应CPU2和CPU3。设置完再cat /proc/interrupts观察就能看到中断开始往新核心上分了。不过要注意不是所有IRQ都允许随便改亲和性有些内核会拒绝。另外改亲和性只是手段根本目标还是让中断负载和业务负载错开避免互相抢CPU。我一般会把网卡中断绑到和业务进程不同的核心上减少干扰。5.3 用perf和ftrace追踪中断耗时光看计数还不够有时候需要知道中断处理到底花了多少时间。这时候perf和ftrace就派上用场了。用perf可以看中断相关的开销# 采样几秒看哪些函数在中断上下文里耗时 perf record -a -g sleep 5 perf report在报告里找irq相关的调用栈能看出中断处理里哪个环节最耗时。如果是软中断耗时通常会看到__do_softirq及其下游函数。ftrace更精细可以追踪单个中断处理函数的执行时间# 开启irqsoff tracer记录关中断的最长时间 echo irqsoff /sys/kernel/debug/tracing/current_tracer echo 1 /sys/kernel/debug/tracing/tracing_on # 跑一会儿业务 echo 0 /sys/kernel/debug/tracing/tracing_on cat /sys/kernel/debug/tracing/traceirqsoff这个tracer专门记录系统里关中断时间最长的那些片段对于定位为什么中断响应这么慢特别有用。如果看到某段代码关中断关了几毫秒那基本就是它导致的延迟尖刺。6. 那些年踩过的中断相关的坑6.1 中断风暴当设备疯狂敲门中断风暴是我遇到过最头疼的问题之一。现象是某个IRQ的计数在几秒内暴涨几百万CPU几乎全被中断处理占满系统卡到没法用。常见原因是设备故障或者驱动bug比如网卡在某种异常状态下不停地发中断而驱动又没能正确清除中断标志导致中断反复触发。排查中断风暴先看/proc/interrupts确认是哪个IRQ然后看dmesg有没有相关报错。如果是网卡可以尝试先ifconfig eth0 down把设备关掉看中断是否停止以此确认是不是这个设备的问题。根治还得靠更新驱动或者修复硬件。注意中断风暴发生时系统可能卡到连命令都敲不进去。这时候可以试试用nice提高shell优先级或者从另一个CPU核心登录。实在不行只能重启。6.2 软中断把CPU吃满软中断虽然不在硬中断上下文但它执行时也会占用CPU而且软中断的调度有个特点如果软中断处理不过来内核会启动ksoftirqd内核线程来兜底但ksoftirqd的优先级不高可能被其他进程挤占导致软中断积压。网络收包场景下如果流量特别大NET_RX_SOFTIRQ 可能长时间占满某个CPU。用top看的话会看到si软中断这一项特别高。这时候可以考虑用RPSReceive Packet Steering把软中断处理分散到多个核心或者用RFSReceive Flow Steering按流来分配提升缓存命中率。这些是网络调优的进阶话题但根源都是软中断的处理能力问题。6.3 中断上下文里睡眠导致的死机这个坑我在写第一个驱动的时候踩过。当时在中断处理函数里调用了一个会睡眠的函数结果系统直接卡死连panic都没打出来。后来才明白中断上下文里睡眠是绝对禁止的因为调度器无法处理中断上下文睡眠这种情况。判断一个函数能不能在中断上下文调用可以看它的实现里有没有might_sleep()或者schedule()之类的调用。内核文档里也会标注函数的上下文要求。写驱动的时候凡是拿不准的一律放到工作队列里执行宁可多一层开销也别冒死机的风险。6.4 中断号共享导致的误判共享中断的场景下一个IRQ触发时所有注册在这个IRQ上的处理函数都会被调用。如果某个处理函数判断逻辑写得不好明明不是它的设备触发它也返回已处理就会导致内核以为中断处理完了实际真正该处理的设备没被处理。这种bug很隐蔽表现为设备偶尔丢数据或者响应慢。正确的做法是处理函数先读设备的状态寄存器确认是不是自己的设备产生了中断是才处理并返回IRQ_HANDLED不是就返回IRQ_NONE。内核会根据返回值统计如果某个处理函数长期返回IRQ_NONE会在日志里警告这也是排查共享中断问题的一个线索。7. 中断处理机制背后的设计权衡7.1 延迟与吞吐的永恒博弈中断处理的设计本质上是在延迟和吞吐之间找平衡。上半部越短中断响应延迟越低但下半部积压的工作就越多整体吞吐可能受影响。反过来如果在上半部多干点活单次中断处理更完整但响应延迟上去了其他中断可能被耽误。这个权衡没有标准答案得看具体场景。实时性要求高的系统比如工业控制会尽量缩短上半部甚至用实时内核补丁把中断线程化让中断处理也能被调度器管理。而吞吐优先的系统比如大流量网关可能会容忍稍高的延迟换取更高的处理效率。理解这个权衡才能在调优时做出正确的取舍。7.2 为什么软中断数量固定软中断的数量在编译时就固定了不能动态增加。这个设计初看很死板但背后有深意。软中断的调度涉及一个位图每个软中断类型对应一位固定数量让这个位图可以用一个简单的整数表示检查和处理都非常快。如果允许动态增加位图就得用更复杂的数据结构调度开销会上升。这个设计体现了内核开发的一个哲学在关键路径上性能优先于灵活性。软中断是高频执行的路径任何一点开销都会被放大所以宁可牺牲灵活性。需要灵活性的场景用工作队列去解决那是另一套机制。7.3 中断线程化的利与弊较新的内核支持中断线程化就是把中断处理程序变成内核线程来运行。好处是中断处理也能被调度器管理可以设置优先级可以睡眠实时性更好。坏处是多了线程切换的开销而且线程化后中断处理的时机不再那么确定可能引入额外的延迟。是否开启线程化取决于你的场景。实时系统通常倾向于线程化普通服务器则看具体负载。可以通过内核启动参数threadirqs强制所有中断线程化也可以针对单个IRQ设置。这个特性用好了是利器用不好反而添乱建议在测试环境充分验证再上生产。8. 一套可复用的中断问题排查流程把前面讲的东西串起来形成一套我实际在用的排查流程。遇到中断相关的性能问题按这个顺序走基本能定位到根因。第一步看/proc/interrupts确认是哪个IRQ异常计数分布是否均匀。这一步能排除掉大部分明显的配置问题。第二步看top里的si和hi两项。hi是硬中断占用si是软中断占用。哪个高就往哪个方向查。hi高通常是中断太频繁或者上半部太长si高通常是下半部处理不过来。第三步用perf或ftrace定位具体耗时函数。irqsofftracer 看关中断时间perf record看热点函数。这一步能精确到代码级别。第四步根据定位结果采取措施。是亲和性问题就调亲和性是中断风暴就查设备是软中断积压就考虑RPS/RFS或者调整下半部机制。第五步改完再测对比前后数据。中断调优最忌讳凭感觉一定要有数据支撑。这套流程我在好几个项目里用过从网络延迟到磁盘IO抖动基本都能覆盖。关键是要有耐心一层层往下剥别一上来就瞎改参数。9. 一些零散但实用的经验最后分享几个零散的经验都是实际工作中攒下来的不一定系统但都挺有用。关于/proc/interrupts的读取如果系统核心特别多输出会很长可以用grep过滤比如grep eth0 /proc/interrupts只看网卡。另外这个文件是只读的改亲和性要写/proc/irq/下的文件别搞混了。关于中断亲和性的设置改完之后最好确认一下是否生效有些系统会拒绝不合法的掩码。另外把中断绑到某个核心后那个核心上的业务进程可能会受影响要综合考虑。我一般会把中断绑到相对空闲的核心或者和业务核心错开。关于软中断的观察/proc/softirqs这个文件能看到各类软中断的计数比/proc/interrupts更细。网络问题看 NET_RX定时器问题看 TIMER块设备看 BLOCK。这个文件平时关注的人不多但排查软中断积压时很有用。关于中断线程化的验证如果开了threadirqs可以用ps aux | grep irq看到一堆irq/开头的内核线程每个对应一个中断。这些线程的优先级可以调实时性要求高的场景可以把关键中断的线程优先级调高。关于测试方法中断调优一定要有可复现的测试负载。我一般用netperf或者iperf打网络流量用fio打磁盘IO观察中断指标的变化。没有稳定负载调优就是盲人摸象。这套东西讲下来从原理到实操从设计思路到踩坑经验基本覆盖了Linux硬中断和中断处理的主要方面。中断这个主题看着底层但它和上层性能的关系比很多人想象的要紧密得多。下次再遇到那种CPU不高但就是卡的怪问题不妨先去看看/proc/interrupts说不定答案就在那里。
阅读完成 · 觉得有帮助?