先聊一个我早年间做嵌入式驱动调试时遇到的真实场景一块网卡芯片每秒钟收到大量数据包CPU占用率在没有任何业务逻辑的情况下飙到接近100%用工具一看绝大部分时间都花在处理中断上。当时我就在想中断到底是怎么回事硬件中断能把CPU打断软件自己生成的中断又是什么机制两者的边界和配合在哪里这个题目看着是计算机基础课上的老生常谈但真到实战里能把这层窗户纸捅破的人并不多。硬中断和软中断软件中断不是一个先后关系也不是一个简单的高低优先级关系。它们是操作系统的两条腿一条管“响应外部事件”一条管“延后执行内部任务”。谁负责紧急的谁负责繁重的为什么这么分工出了问题怎么查我把我的理解和踩坑记录整理在下面。适合正在写驱动、做嵌入式、或者单纯想知道“CPU被打断时到底发生了什么”的开发者参考。1. 中断机制到底在解决什么问题1.1 从轮询到中断CPU的两种活法要理解中断得先理解没有中断会怎样。假设CPU需要知道网卡有没有收到新数据最简单的办法是轮询也就是死循环里反复读网卡状态寄存器。伪代码看起来是这么回事while (1) { if (nic_status() RX_READY) { handle_packet(); } /* 别的事什么也干不了 */ }轮询的问题是明摆着的CPU把大量时间花在“问”上而不是“做”上。你问一次状态很快但网卡可能平均要隔很久才有数据这中间的每一次查询都是纯浪费。而且查询间隔越短浪费越多间隔越长数据到达后处理的延迟就越大。这里存在一个性能和响应性的根本矛盾早年不少网络设备驱动真的是这么干的只有在CPU非常空闲的场景下才勉强能接受。中断就是用来打破这个死结的设计。外设有事的时候主动拉一根信号线CPU放下手里的活去处理一下处理完继续原来的工作。CPU不用主动去问省下来的时间全部可以干正事。这个思路放到现实生活里就相当于你坐在工位上不需要每五秒钟抬头看一眼门口有没有人找你而是等有人敲门再站起来去开门。敲门这个动作就是硬件中断。1.2 事件驱动与时间驱动的取舍轮询是典型的时间驱动中断是典型的事件驱动。事件驱动的好处是省CPU坏处是打断当前工作带来上下文切换开销。所以实际工程里两者经常混着用极高频的事件比如高吞吐网卡的收包让驱动在中断和轮询之间动态切换这就是后面要提到的NAPI之类的机制。低频事件比如按键、鼠标移动则老老实实用中断。理解这个背景很重要因为后面讨论的中断下半部、softirq、以及各种优化手段本质上都是在“省CPU”和“削延迟”之间找平衡。单纯的硬中断只负责响应真正的重活如果也堆在中断里做系统会废掉。所以才有了软中断、任务队列这一整套延后处理的架构。1.3 先厘清三个常被混用的名词很多文章把硬件中断、软件中断、软中断放在一起讲但对这些术语的边界并不清楚。我先把概念定准后面展开才不会乱。硬件中断Hardware Interrupt由外设通过中断控制器向CPU发出的异步信号典型代表是网卡收包、磁盘完成IO、定时器到期。它不依赖CPU的当前执行状态随时可能被触发。软件中断Software Interrupt由CPU执行特定指令主动触发的中断x86上就是INT指令。典型的代表是早期的系统调用入口int 0x80。它是同步的同一段代码里你执行到某条指令才触发不是外部主动打断的。软中断softirq这是Linux内核里一种延后处理机制的名字严格说属于“软件中断”的一种抽象实现但含义窄很多。在Linux语境下说“软中断”大家通常特指softirq、tasklet这套东西和x86指令级的中断不是一个层面的概念。把这三者的关系理清之后我们再来逐层拆解。2. 硬件中断CPU被外设拉响的警报2.1 谁在喊CPU常见硬件中断源硬件中断的触发源非常广泛几乎每一个外设都可以申请中断。在PC或服务器上最常见的有这么几类中断源典型场景特点定时器时钟节拍、调度周期性触发是系统的心跳网卡数据包到达、收发完成频率高、突发性强最容易引发中断风暴磁盘控制器IO完成、错误上报异步IO的完成通知键盘/鼠标用户输入低频但要求低延迟GPIO外设传感器、按钮嵌入式里非常常见电平或边沿触发不同中断源会分配到一个中断号IRQ这个编号不是CPU直接就能用的CPU内部有中断向量表IRQ会被中断控制器映射成对应的向量号再通过向量号找到处理函数。平时在Linux里敲cat /proc/interrupts看到的左列数字就是IRQ号。2.2 中断控制器从8259A到多核APIC/GIC早期的x86平台用8259A中断控制器两片级联最多支持15个IRQ。单核时代这么干够用但多核时代就不行了于是x86发展出了APIC架构每个CPU核心有自己的本地APIC系统里还有IO-APIC负责汇总外设的中断请求然后按配置分发到某一个或某几个CPU上。ARM平台对应的东西叫GIC现代手机和嵌入式板子上的中断基本都是靠GIC管理。中断控制器最重要的两个职责一是记录和管理中断请求pending二是按照优先级决定先给CPU报哪一个。对于程序员来说写驱动时大多不用直接跟它打交道但配置中断触发方式电平触发还是边沿触发、设置中断亲和性允许中断发到哪个CPU这类操作底层就是在操作中断控制器的寄存器。2.3 中断处理的完整流程现场保存与恢复当一个硬件中断到达CPU时CPU内部会发生一连串动作这个过程在不同架构上细节不同但宏观套路是一致的CPU检查中断是否被屏蔽如果中断允许标志比如x86上的IF位是关闭的请求会被缓存或忽略。CPU把当前正在执行的上下文关键寄存器、返回地址、标志寄存器等压入内核栈。CPU中断向量号查IDT中断描述符表找到对应的处理入口。切换到处理函数一般就是驱动注册的ISRInterrupt Service Routine。处理完后恢复现场回到被中断前的指令继续执行。这段流程里最关键的认识是中断处理是在被打断的进程上下文中抢出来的时间里执行的但是这个上下文既不是普通用户态进程上下文也不是一个独立的内核线程它叫中断上下文。在中断上下文里时钟可能还在走进程可能正处在任何一条指令上这直接决定了中断处理函数里面能干什么、不能干什么。2.4 中断处理函数的三条铁律我早年写驱动在ISR里调用了一个带互斥锁的函数结果整套系统隔三差五死锁后来查了小半天才意识到问题。中断上下文里有三条铁律违反任何一条都很危险不能睡眠。中断处理里不能用那些可能让出CPU的函数比如mutex_lock、kmalloc(GFP_KERNEL)、msleep等。因为中断没有进程上下文没法被调度器换进换出一旦睡眠这个CPU核心基本就废了。执行时间必须短。中断处理期间同级别的其他中断会被屏蔽或延迟拖得越久系统丢事件的风险越大。不能随便使用用户态数据。中断上下文和用户态进程没有绑定关系访问用户空间的指针没有意义内核也不会让你拥有对应的缺页异常处理通道。这就是为什么真正的中断处理会拆成两部分上半部硬中断ISR只做最紧急、最简短的事情确认中断、拷贝最快的数据、触发软中断繁重的处理统统延后。这也是软件中断机制存在的根本原因。2.5 实操怎么查看硬件中断发生了什么Linux下面看硬件中断最直接的工具就是/proc/interrupts。随便找台机器执行一下输出大概是这样的$ cat /proc/interrupts CPU0 CPU1 CPU2 CPU3 0: 120 0 0 0 IO-APIC timer 1: 3 0 0 0 IO-APIC keyboard 8: 0 0 0 0 IO-APIC rtc0 24: 12345 38921 210 98231 PCI-MSI eth0每一列是一个CPU核心每一行是一个中断号。注意最后一行eth0中断号24四个CPU核心上都有计数说明这个网卡的中断被分发到了多个核上。如果你发现某个设备的计数在某个CPU上疯狂上涨别的核是零说明中断亲和性没配好所有压力都堆到了一个核上。排查时我喜欢用两次快照做差值比如watch -d cat /proc/interrupts或者自己记录前后两个时间点的计数除以间隔秒数换算成“每秒中断次数”。如果某个设备每秒中断几万次甚至几十万次CPU跑在中断处理上都来不及那就是标准的中断风暴场景。3. 软件中断CPU自己拉响的闹钟3.1 用指令主动触发中断软件中断这个概念最早的字面意思就是“用软件指令制造一个中断”。x86上就是INT指令。开发者可以执行INT nCPU会像接到硬件中断一样走一遍向量表查处理函数只不过这个请求不是外设发起的是程序主动拉响的闹钟。这地方的用途很广最经典的例子是老一代Linux系统调用入口。x86平台用int 0x80用户态程序需要访问内核功能时把系统调用号放进寄存器然后执行一条INT指令CPU自动切到内核态按0x80号向量找到系统调用处理函数。后来因为性能问题x86_64换成了专门的syscall指令但原理上保留了“软件主动触发特权操作”的思想。软件中断和普通函数调用的区别在于它是有特权级别变化的。普通函数调用只是在同一个特权级内部跳转软件中断会把CPU从用户态切到内核态这意味着它拥有更高的权限也正是系统调用必须依赖它的原因。现代CPU甚至为这种切换提供了专用的快速通道尽量降低切换成本。3.2 内核里的软中断机制给重活找个去处Linux内核里被广泛称为“软中断”的东西其实是另一种意义上的软件中断是一套延后执行机制。为什么要延后回到网卡收包的例子硬中断ISR发现网卡有数据后按理说应该立刻把数据读完、解析协议栈、交给应用。但这一串操作如果全在硬中断里做一次中断可能就要几毫秒期间系统对更高优先级的响应全部瘫痪。所以内核把工作分成两段。前一段在硬中断里快速把网卡数据读到内存防止被覆盖标记一个软中断位。后一段在处理完硬中断之后、或者调度到ksoftirqd线程时执行真正的协议栈处理。这套延后机制在Linux里分层设计机制执行时机特点适用场景softirq硬中断返回前或ksoftirqd开销小、并发高、不能睡眠网络收发、块设备等高频路径tasklet软中断时机由softirq驱动同一tasklet不会并行一般驱动后半部分workqueue内核线程可以睡眠开销大复杂、低频、允许阻塞的工作这三个的层级关系务必搞明白tasklet是构建在softirq之上的workqueue则是完全独立的线程化机制。如果搞混了可能在需要睡眠的场景错误地用了softirq中枢直接报错。3.3 系统调用离应用开发者最近的软件中断对普通应用开发者来说软件中断离你最近的入口就是系统调用。你调read()、write()、open()libc帮你封装好参数最终触发一次陷入内核的操作。在老的内核上这个陷入就是一次软件中断。所以一次看似普通的文件读取背后其实经历了一次完整的用户态到内核态再到用户态的往返。这件事的代价在极高性能场景下不能忽略。一次系统调用可能有几百纳秒到几微秒的开销频繁调用会让这个开销被放大。这就是为什么很多高性能网络库会用io_uring之类的机制减少系统调用次数本质上就是想减少“软件中断”的次数。3.4 实操查看软中断分布查看软中断用/proc/softirqs$ cat /proc/softirqs CPU0 CPU1 CPU2 CPU3 HI: 0 0 0 0 TIMER: 1234567 1238901 1223456 1245678 NET_TX: 2134 1890 2211 2034 NET_RX: 4567890 234501 129038 872341NET_RX这个计数的分布非常重要它表示网络收包软中断在哪些CPU上跑得多。如果你发现所有NET_RX计数都集中在CPU0上而CPU0的用户态负载已经很高了那网络处理延迟肯定上不去。把网络软中断分散到多个CPU上或者用RPSReceive Packet Steering来摊薄压力就是从这个信息出发做的调优。4. 硬件中断与软件中断的协同实战4.1 一次网卡收包的完整中断旅程把硬中断和软中断揉在一起看一遍才能真正理解它们的配合关系。以Linux收一个网卡数据包为例子网卡硬件收到报文写入自己的接收队列DMA然后触发硬件中断。这个是纯粹的硬件行为异步发生。CPU响应硬件中断进入网卡驱动注册的ISR。ISR做的事情非常克制关掉该网卡的中断避免持续被打断、把报文从网卡队列摘到内核的sk_buff链表中、然后触发NET_RX_SOFTIRQ软中断、尽快退出。硬中断处理完后CPU检查有没有待处理的软中断。如果当前环境允许直接执行softirq处理函数如果软中断太多则唤醒ksoftirqd内核线程来处理。softirq里执行实际的协议栈逻辑从链表里取出报文、逐层解析IP、TCP/UDP、把数据送到socket缓冲区、唤醒等待的进程。应用进程被唤醒从socket缓冲区把数据读到用户空间回到业务代码。这个流程里有个细节很值得体会为什么ISR要把网卡中断关掉因为如果不关在数据量很大的时候网卡每收一个新包就立刻给你来一次硬中断CPU会一直挣扎在ISR里连处理数据的时间都没有。关掉中断、改用这种“积累一批再统一处理”的方式就是用数据缓冲换中断频率效率高得多。4.2 一张表看懂两者的核心差异很多面试题问“硬中断和软中断有什么区别”但如果只是背区别没有建立系统图景遇到实际问题还是抓瞎。我把核心差异整理成一张表配合上面的流程去理解会更有感觉维度硬件中断软件中断softirq触发者外设硬件内核代码主动标记触发方式异步、随时可能同步、在处理路径上主动触发运行上下文中断上下文中断返回路径或内核线程是否可屏蔽可屏蔽中断可关NMI不可通常用local_bh_disable屏蔽执行限制不能睡眠必须极短不能睡眠可稍重但仍需可控优先级高占用真实CPU低于硬中断高于一般进程典型代表网卡中断、定时器中断NET_RX、NET_TX、任务调度需要注意的是这里的“软件中断”指的是Linux的softirq不是x86的INT指令。真正的INT软件中断与硬件中断更相似都走中断向量表、都发生在特定上下文。而softirq更像是一个运行在“安全时机”的延后回调它不打断CPU的当前工作而是等在硬中断返回后的某个时刻自动执行。4.3 中断优先级与嵌套中断是有优先级的这个优先级由中断控制器管理。现代系统上常见的优先级划分包括可屏蔽中断普通设备、不可屏蔽中断NMI通常是硬件看门狗、严重错误、以及系统管理中断SMI常用于固件管理。越紧急的事件越要配高的优先级。中断嵌套指的是处理一个中断时又被另一个更高优先级的中断打断。比如网卡中断处理到一半内存纠错这类紧急中断来了CPU会先去处理更紧急的回来再继续原来的。但实际驱动开发里我们通常建议尽量在ISR里关掉本中断或本处理器的嵌套因为嵌套会显著增加堆栈压力和调试难度也容易引发竞态。5. 实战中的中断问题排查与调优5.1 中断风暴怎么定位、怎么止血中断风暴是生产环境里最头痛的问题之一。现象很简单某个设备疯狂触发中断CPU要么长时间处于硬中断处理状态通过top看到hi的占比很高要么长时间处理软中断si占比很高系统变得卡顿甚至无响应。我处理过的一次典型案例是一块网卡驱动和硬件配合出了问题硬件在特定速率下会频繁产生错误队列事件驱动里的ISR没做有效的错误合并每来一次错误就触发一次完整处理流程。CPU0的硬中断占比长期超过80%。定位步骤可以这样走用top或mpstat确认CPU时间花在硬中断hi还是软中断si上。这能直接区分是ISR路径还是softirq路径的问题。用cat /proc/interrupts连续快照查看哪个IRQ号的增幅异常。通常打出一个前10名异常设备的清单就能看到凶手。确认是硬中断频率高还是每次处理太重。这一步可以临时把对应设备的中断合并关掉或开起来对比也可以用perf top看ISR里的热点函数。如果是真实的业务流量造成的压力不是bug就做中断亲和性和合并这类优化如果是驱动或硬件bug那就得临时屏蔽或降级处理。止血技巧暴风时可以先通过设置/proc/irq/N/smp_affinity把异常中断挪到一个空闲CPU上保证其他业务核心不受影响再慢慢查根因。这招能帮你争取到排查时间生产环境里很实用。5.2 中断亲和性把压力摊到多个核上中断亲和性就是决定某个设备的中断可以在哪些CPU核心上被处理。默认情况下许多设备的中断会集中在某个核心上数据量一大那个核心就会被硬中断或软中断占满其余核心干瞪眼。手工设置方式# 将IRQ 24的中断允许CPU2和CPU3处理 echo 0xC /proc/irq/24/smp_affinity0xC是十六进制二进制对应1100也就是允许CPU2和CPU3。设置后再次看/proc/interrupts计数会逐渐分布在允许的CPU上。系统里通常还有irqbalance服务在自动调度这些亲和性设置但我个人的经验是在延迟敏感场景下自动调度的效果往往不如自己手工分配。尤其对网卡这类多队列设备最好让每个硬件队列的中断绑定到固定的CPU核心再配合RPS/RFS在软中断层面做进一步均衡能让吞吐和延迟都稳定不少。5.3 中断上下文里的锁与竞态最后说说中断机制引入的另一个常见坑并发与竞态。因为中断随时可能打断正在执行的代码所以共享数据被访问时必须考虑“正在访问时来了个中断中断里也访问了同一份数据”这种情形。应对方案在Linux里有一套约定俗成的组合若共享数据既被普通进程上下文使用也会被硬中断处理函数使用那么普通进程侧必须用spin_lock_irqsave()加锁中断侧用spin_lock()即可。_irqsave会把中断状态保存到变量里释放时再恢复防止死锁。若数据只在硬中断与软中断之间共享可以用local_bh_disable配合spin_lock_bh处理。不能图省事全局关中断local_irq_disable只关当前CPU的中断多核之间照样竞态而且全局关中断时间长了对系统时延影响极大。我遇到过一个特别隐蔽的问题某驱动的统计字段用普通int累加处理函数里使用spin_lock_irqsaveISR里用spin_lock看起来没错。但后来系统升级后同一字段被另一个设备的中断也访问了两个中断互相打断当晚就在高负载下出现数据错乱。教训是升级改变驱动访问路径后竞态边界会跟着变务必重新审视锁的粒度。6. 我对中断机制的一点真实体会中断这个话题的边界比想象中宽。很多人一开始以为搞懂了硬中断和软中断的区别就算掌握但真正写驱动、做性能调优时才会发现难的不是记住定义而是理解中断如何影响系统的每一条执行路径。我自己的经验是遇到任何CPU占用异常或者莫名其妙的高延迟先不要急着改业务代码先看一眼中断分布。很多时候问题根本不在应用层而是某个设备的中断把CPU打满了。反过来也有时候中段计数看起来正常但softirq耗时异常坑在驱动把过多的活儿留到了软中断里导致si占比居高不下。对这种基础机制建议大家不要只停留在概念层面找一台普通的Linux机器开一个网络流量反复观察/proc/interrupts和/proc/softirqs的变化再做个中断绑定实验你会比看十篇文章都更能建立手感。中断机制是操作系统的地基之一地基的纹路看清了很多上层的问题会豁然开朗。