☰
中断、关中断与开中断:临界区、延迟与多平台实战
2026/10/1 13:03:31 网站建设 项目流程

1. 中断先用一句话讲清楚:它是 CPU 的一次"有事喊我"

几年前带一个做数据采集的同事排查过一个很典型的问题:设备空闲时串口收数据一切正常,可只要把定时器中断打开,接收缓冲区里就会零星少几个字节,偶尔还多出一个重复值。代码读起来没有任何毛病,接收也确实用了标准的中断服务函数。最后定位到的原因朴素得有点扎心——定时器中断里有一小段临界区关着中断没及时开回来,把串口的接收窗口给盖住了。这件事让我意识到,"中断、关中断、开中断"这三个词大家天天挂在嘴边,可真要追问一句"你关的到底是谁的中断、关了多久、什么时候必须开回来",能答清楚的人其实不多。这篇内容就想把这三个概念从最底层捋一遍,重点讲透关中断和开中断背后的动机与代价,以及在 x86、ARM Cortex-M、Linux 这些平台上究竟该怎么写。不管你是正在啃计算机操作系统原理的学生,还是写裸机驱动、调 stm32 中断的嵌入式工程师,或者只是被"linux 中断""中断服务函数"这类问题绊住过,下面这些内容应该都能对得上号。

1.1 从轮询到中断:CPU 的时间到底被谁浪费了

理解中断,最省事的办法是先看没有中断会怎样。CPU 想知道一个按键有没有被按下、串口有没有收到数据,只能不停地去读寄存器,这种办法叫轮询(polling)。轮询的问题不在于"读"这个动作本身,而在于它把 CPU 死死地绑在了"守株待兔"上:按键可能几个小时才按一次,可 CPU 为了不漏掉那一次,就得一直盯着。设备一多,轮询的循环就越拉越长,响应一个事件的延迟也随之上升——你盯着十个设备轮流问一圈,每个设备实际被检查的间隔就是这一圈的时间。

中断把这件事彻底反了过来。外设不再等 CPU 来问,而是自己主动"举手":数据到了、按键按下了、定时器数满了,就通过一根专门的中断请求线(IRQ)拉一下电平,告诉中断控制器"我有事"。CPU 正常执行主程序,收到这个信号后才暂停手头的活,跳过去处理,处理完再回来接着干。这里有个容易被忽略的点:中断不是让 CPU 变快了,而是让 CPU 把"等待"这段时间腾出来干别的。主循环里可以放心地去做计算、刷新屏幕,不用再担心漏掉串口的一个字节。所以判断一个场景该不该用中断,标准很简单——如果这件事"发生的时刻不可预测、但要求及时响应",那就该用中断;如果它"一定会按固定节奏来"且对延迟不敏感,轮询反而更简单、更好调试。

1.2 硬件中断、软件中断和异常走的是同一条通路

很多教材把硬件中断、软件中断、异常拆成三块讲,容易让人以为它们是三套独立机制。实际在 CPU 眼里,它们走的是同一扇门,区别只在于"谁来敲门"。硬件中断是外设发起的,比如串口接收完成、定时器溢出、按键电平变化,它们经中断控制器汇总后送到 CPU 的中断引脚;软件中断是程序主动发起的,最典型的就是系统调用——在 x86 上是一条int 0x80或syscall指令,在 ARM 上是一条svc(老架构写作swi)指令,程序执行它,就是在"假装"自己是个外设,请求 CPU 切换到内核态去做点特权操作;异常则是 CPU 在执行指令过程中自己发现的问题,比如除零、缺页、非法指令,它和中断最本质的差别是:中断发生在两条指令之间,异常往往发生在某条指令内部,而且异常处理完可能还要重新执行那条出错的指令。

把它们放在一起看有个好处:你会明白为什么操作系统内核里处理"缺页"和"系统调用"的入口在写法上如此相似——它们都要保存现场、切栈、进入内核例程、再恢复。理解了这一点,再看"软件中断和硬件中断"这类面试题,就不再是背概念,而是能说清楚它们共享了哪套保存与恢复的框架。

1.3 中断向量表与中断服务函数:CPU 凭什么知道跳到哪

中断来了,CPU 得知道该跳去哪段代码,答案就是中断向量表。它本质上是一张放在固定地址的表,每一项存着一个函数入口地址,下标就是中断号。x86 里这张表叫 IDT(中断描述符表),位置由 IDTR 寄存器给出,每一项除了入口地址还带权限位;Cortex-M 里它更直接,就是一块从 0x00000000 开始的地址数组,第一项是初始栈指针,第二项是复位向量,后面依次排着各种中断向量,运行时还可以通过 VTOR 寄存器把这整张表搬到别处。所谓中断服务函数(ISR),就是这张表里某个入口指向的那段代码,在 stm32 的启动文件里你看到的xxx_IRQHandler就是它。

拿 stm32 的中断配置举个例子:你想让某个引脚上的按键触发中断,大致要做这几件事——打开对应 GPIO 的时钟,把引脚配成输入并选好触发边沿(上升沿、下降沿还是双边沿),使能 EXTI 线上对应的中断屏蔽位,最后在 NVIC 里把这个中断通道的使能位置 1、设好优先级,同时把向量表里对应的EXTIx_IRQHandler实现出来。这一整套动作合起来就是常说的"中断配置"。我见过不少人卡在"中断函数写了但进不去",检查下来十有八九是两件事没做:NVIC 里那条中断通道压根没使能,或者函数名和启动文件里定义的名字对不上——名字对不上,你写的就是一个普通函数,自然不会挂到向量表上。

2. 关中断为什么存在:一个自增操作就能说明白

讲完了中断是什么,接下来要回答一个听起来很怪的问题:好端端的中断,为什么要关掉?很多人第一次听到"关中断"会本能地觉得这是"反操作"——中断不是好东西吗,关它干嘛。答案藏在并发里。只要有两个执行流(一个是主程序,一个是中断服务函数)会碰同一份数据,就存在被打断后数据处于"半成品状态"的窗口,而关中断就是把这个窗口暂时封死的手段。下面用最经典的一个例子把它讲透。

2.1 一句 counter++ 被编译器拆成了三条指令

在 C 里写counter++,看着像一个原子动作,可编译器把它翻译成汇编后通常是三条指令:先把counter从内存读进寄存器(Load),在寄存器里加一(Add),再写回内存(Store)。假设counter初值是 5:主程序刚执行完 Load,寄存器里是 5,还没来得及写回,这时一个中断来了。中断服务函数里也对同一个counter做了自增,它完整地跑完了 Load 5、Add 6、Store 6,内存里现在是 6。中断返回,主程序拿着寄存器里那个旧值 5 继续 Add 得到 6,Store 回内存——内存里还是 6。两次自增,结果只加了一次。

这就是所谓竞态(race condition),它的可怕之处在于偶发:绝大多数时候你跑一天都不会出错,一旦时序凑巧撞上就丢一次数据,而且完全不报错。调试时你加打印、加断点,时序一改变,问题就"消失"了,于是陷入"改一改好像好了、过几天又冒出来"的循环。对付它的标准手段,就是在 Load 和 Store 之间不让中断插进来——把这三条指令包进一个临界区(critical section),进入前关中断,出来后开中断。这样中断服务函数要么在这段代码之前跑完,要么等它跑完再跑,绝不会卡在中间。

2.2 关中断守住的到底是什么边界

理解临界区,关键在于想清楚"我要保护的是哪一段不变式"。上面那个例子里,不变式是"counter的值必须能反映每一次自增",所以保护范围就是读-改-写这三条指令,一个字节都不能多。实际工程里常见的不变式还有几类:一个链表的head指针和tail指针必须同时更新,不能让中断看到只有head变了的中间状态;一个状态机的主状态和子状态必须成套修改;一个环形缓冲区的读写指针必须保持一致。判断临界区边界的经验法则是:找出所有会被中断和主程序同时访问的共享变量,围着它们的每一次"非原子修改"划范围。

这里有个坑我踩过不止一次:有人图省事,把整个处理流程都用关中断包起来——从读取外设寄存器、解析协议、计算校验,一直到存进缓冲区。结果临界区长达几百微秒,中断延迟被拉得老高,串口开始丢字节,看门狗都差点被喂不上。正确的做法恰恰相反:临界区要尽可能短,只包住那几条真正需要原子的指令,耗时的解析、计算全部挪到关中断之外做。记住一句话——关中断是为了保护数据,不是为了保护逻辑。

2.3 关中断的代价要用延迟来支付

关中断不是免费的,它的代价是**中断延迟(interrupt latency)**的增加。在关中断期间,所有被屏蔽的中断都不会被响应,它们只能排队等着,直到你开中断。如果这段时间里外设需要及时响应,而它又没有硬件缓冲,数据就会直接丢。最典型的就是 UART:接收寄存器通常只有一个字节的缓冲,当前字节还没被读走、下一个字节就到了,硬件就会置溢出标志并把新字节丢掉。这也是为什么高速串口在关中断时间偏长的系统里特别容易掉数据。

代价还不止丢数据。中断延迟变大,会直接影响实时性:电机控制的电流环要求在一个 PWM 周期内完成采样和计算,你中途关中断拖了几十微秒,控制就可能失稳;音频采集要求每个采样周期按时取数,延迟一抖就听到爆音。所以关中断这件事要当成"借钱"来看待——能借,但要清楚利息(延迟)有多高、什么时候必须还。有经验的做法是给自己定个硬指标,比如"任何临界区关中断时间不超过 10 微秒",然后用示波器去实测验证,而不是凭感觉认为"我这段很快"。

3. 开中断不是"再把开关拨回去"这么简单

很多人把关中断和开中断想成一对对称的操作:关下去、用完再打开,一开一关,回到原点。真实情况比这复杂得多。开中断时要考虑"开回到什么程度"——是恢复到关之前的原样,还是无条件全开;要考虑当时是不是身处另一个更外层的临界区;还要考虑在多核系统里,你关的这一下到底关住了谁。这一节把这几个层次拆开说。

3.1 允许嵌套和不允许嵌套是两种中断模型

先区分两个概念:中断嵌套和临界区嵌套。中断嵌套说的是"高优先级中断能不能打断正在执行的低优先级中断服务函数"。有些系统是不允许嵌套的,一个中断进来后,CPU 自动屏蔽同级及更低优先级的中断,等这个服务函数整段跑完才放开;另一些系统允许嵌套,高优先级可以随时抢占低优先级,响应更快,但代码复杂度也上去了——同一个共享变量的访问顺序变得更难预测,栈的深度也不再是常数。

Cortex-M 的做法比较巧妙:它的优先级是硬件仲裁的,只要两个中断优先级不同,高优先级天然就能抢占正在跑的低优先级服务函数,不需要你在服务函数里手动开中断;优先级相同的则不会互相打断。老式的 8259 中断控制器时代就不是这样,中断服务函数入口会自动清掉中断允许位,你如果想让更高优先级的中断进来,必须在服务函数里手动执行一次sti,而这又带来了重入风险。理解这两种模型的差别,能帮你解释很多"为什么我这段代码在 A 板子上没事、换到 B 板子就崩"的怪现象。

3.2 局部关中断与全局关中断:多核环境下的分水岭

在单核时代,"关中断"就是字面意思——CPU 只有一颗,关了它谁也进不来。到了多核,事情变了:cli这样的指令只能关掉当前这颗核的中断,别的核照样该收中断收中断,照样能跑、能改内存。也就是说,如果你在核 A 上关中断来保护一个共享变量,核 B 上正在跑的代码完全不理会你这套,照样可以在你读-改-写的中间把变量改掉。这时候光关中断已经保护不住了,必须上**自旋锁(spinlock)**之类的多核同步原语。

正确的组合通常是"锁 + 关本地中断":先拿锁(保证同一时刻只有一颗核在临界区里),再关本地中断(保证这颗核上不会被中断服务函数打扰),出来时反过来做。为什么拿了锁还要关中断?因为即使你抢到了锁,这颗核上万一来了个中断服务函数也要访问同一份数据,它要么自旋等锁(如果它自己也去拿锁,就可能死锁),要么直接无视你把数据改坏。所以在"这个数据结构会被中断服务函数访问"的前提下,锁和关中断缺一不可。这是从单核思路迁移到多核思路时最容易漏的一环。

3.3 屏蔽寄存器与优先级:比全开全关更细的粒度

"关中断/开中断"是粗粒度的开关,硬件其实还提供了更细的粒度——中断屏蔽寄存器和优先级屏蔽。前者可以针对某一号中断单独开关,比如你只想屏蔽串口中断但保留定时器中断,改对应的屏蔽位就行;后者更灵活,Cortex-M 的 BASEPRI 寄存器可以设定一个"门槛",凡是优先级数值高于(即优先级低)这个门槛的中断都被屏蔽,而更紧急的中断仍能穿透。举个例子,你在处理一段不能被普通中断打扰的代码,但又希望最高优先级的故障中断随时能进来,就可以把 BASEPRI 设成某个中间值,实现"只放行一部分"。

用细细粒度的屏蔽,好处是延迟可控。总比"一刀切全关"要温和得多:全关中断意味着所有中断的延迟都被你的临界区拖长,而精确屏蔽只影响那些你确实不想被打扰的中断,其余照常响应。我在做中断优化时的一条经验是:能精确屏蔽就别全关,能用硬件仲裁就别手写开关。硬件本来就为不同紧急程度设计了优先级,你不用它反而用最粗暴的方式全关,等于白白牺牲了系统的实时性。

4. 三套平台的开关中断实操写法

概念讲完了,落到代码上。不同平台开关中断的写法差异很大,这里挑三个最常见的——裸机 x86、Cortex-M 单片机、Linux 内核——分别看一眼,重点是理解每种写法的"保护范围"和"恢复语义",而不是死记指令。下面这张表先把它们对照起来。

平台关中断开中断保存/恢复关键注意点
x86 裸机clistipushf/popfsti会在下一条指令后才生效
Cortex-M__disable_irq()__enable_irq()读 PRIMASK直接调用会破坏外层状态
Linux 内核local_irq_disable()local_irq_enable()local_irq_save(flags)优先用 save/restore 版本

4.1 x86 的 cli / sti 与 EFLAGS.IF

x86 上关中断靠的是cli(Clear Interrupt flag)指令,它把 EFLAGS 寄存器里的 IF 位清 0;开中断靠sti(Set Interrupt flag),把 IF 置 1。这里有几个细节值得记住。第一,sti不是马上生效的,它要等下一条指令执行完才真正打开中断,这个设计是为了保证sti; ret这种"开完中断就返回"的序列不会在返回之前被中断插一脚。第二,EFLAGS 里除了 IF 还有一堆标志位,恢复时不能自己造一个值塞回去,要么用pushf/popf成对保存恢复,要么就在关中断前把整个标志寄存器存下来。第三,现代 x86 上sti和cli属于特权指令,用户态跑不了,所以你在应用程序里是没法直接关中断的,这件事只能由内核来做。

如果用内联汇编写,常见的样子是这样:

// 保存标志寄存器并关中断 unsigned long flags; asm volatile("pushf; popq %0; cli" : "=r"(flags) :: "memory"); // ... 临界区 ... // 恢复标志寄存器(中断状态随之恢复) asm volatile("pushq %0; popf" :: "r"(flags) : "memory", "cc");

注意这里用"恢复"而不是"直接sti",原因在下一小节的 Cortex-M 里会更明显——无条件开中断会把外层的保护也给解掉。

4.2 Cortex-M 的 PRIMASK、BASEPRI 与 CMSIS 封装

Cortex-M 把中断开关做成了内核寄存器。最常用的是PRIMASK,它只有一位,置 1 就屏蔽所有可配置优先级的中断(NMI 和 HardFault 除外),清零就放开。CMSIS 提供了两个函数:__disable_irq()写 PRIMASK 为 1,__enable_irq()写 PRIMASK 为 0。看着简单,坑也正在这里:__enable_irq()是无条件打开的。如果你在临界区 A 里关了中断,A 里面又调用了某个函数 B,B 也关中断、处理完调用__enable_irq()打开——B 一返回,A 的保护就没了,A 剩下的代码暴露在中断之下。

正确做法是保存和恢复 PRIMASK,而不是无脑开关:

uint32_t primask = __get_PRIMASK(); // 记住关之前的原始状态 __disable_irq(); // 关 // ... 临界区 ... __set_PRIMASK(primask); // 恢复到进入前的样子,而不是强行打开

这套"保存原始状态、恢复原始状态"的模式,是嵌套临界区能正确工作的关键。除了 PRIMASK,还有FAULTMASK(连 HardFault 都屏蔽,只在极特殊场景用,用错会让系统无法调试)和前面提到的BASEPRI(按优先级门槛屏蔽,适合需要"放行高优先级中断"的场合)。stm32 上大部分场景用 PRIMASK 的保存恢复就够了,只有在实时性要求高、又确实需要放行某几类中断时,才值得动用 BASEPRI。

4.3 Linux 内核的 local_irq_save 与自旋锁的搭配

Linux 内核把上面这套"保存再恢复"的思路封装成了现成的接口,这也是它的代码里几乎看不到裸的"关中断/开中断"的原因。常用的四件套是:local_irq_disable()/local_irq_enable()做简单的本地关闭与打开;local_irq_save(flags)/local_irq_restore(flags)做带保存的关闭与恢复。内核规范里明确推荐后者,因为你能保证退出时状态和进入时一致,不会把外层已经关掉的中断给打开。

真正在驱动里保护共享数据,用的通常不是这四个函数,而是带中断保护的自旋锁变体,比如spin_lock_irqsave(&lock, flags)和spin_unlock_irqrestore(&lock, flags)。它的语义是"拿锁的同时关本地中断,并把原来状态存进 flags",出来时"放锁并恢复中断状态"。这里面的逻辑值得琢磨:为什么要先拿锁再关中断,而不是先关中断再拿锁?因为如果先关中断再等锁,而锁正被另一颗核持有、那颗核又在等一个需要你这颗核处理的中断,就可能卡死。内核里凡是在中断上下文和进程上下文都会访问的数据结构,几乎都用这套接口,你写驱动时照着抄基本不会错。

5. 把"关了多久"量化出来:中断延迟的组成与测量

前面反复说"临界区别太长",可"长"到底是多少、怎么量,才是工程上真正管用的东西。这一节讲清楚一次中断从发生到真正进服务函数要经过哪些环节,以及怎么用低成本的手段把它测出来。很多人调试中断问题全靠猜,其实只要把延迟量出来,问题往往一眼就露出来了。

5.1 一次中断从发生到进入服务函数的完整时间轴

一次硬件中断从"事件发生"到"服务函数第一条指令执行",中间要经过好几段。第一段是外设到中断控制器的传播:外设置起中断标志,信号要经过同步逻辑传到中断控制器,这部分通常是几个时钟周期,几乎可以忽略。第二段是中断控制器的仲裁:控制器要判断这个中断的优先级、当前有没有更高优先级的中断在服务、能不能抢占,多个中断同时来的时候还要排队,这段在高负载系统里会明显变长。第三段是CPU 的响应与压栈:CPU 完成当前指令、识别中断、把现场(程序计数器、状态寄存器、部分通用寄存器)压栈,Cortex-M 上这一步是硬件自动完成的,大约十几个周期,但如果是带 FPU 的芯片并且中断里用了浮点,压栈会多出几十个周期。第四段才是真正的服务函数入口,到这里为止的时间就是硬件中断延迟。

如果中间还有一段关中断,那么这段延迟还要再加上"距离关中断结束还剩多少时间"。所以系统的最坏情况中断延迟 ≈ 硬件响应时间 + 最长临界区的关中断时间。想优化延迟,要么减小硬件开销(比如别在中断里用浮点、别开太深的栈),要么压缩临界区长度,后者通常是更可控的一头。

5.2 用 GPIO 翻转加逻辑分析仪测关键路径

测中断延迟不一定需要专业设备,一根 GPIO 加一台逻辑分析仪(甚至便宜的示波器)就够。思路是:在临界区进入时把一个空闲引脚拉高,退出时拉低,用逻辑分析仪测这段高电平的宽度,就是你的关中断时长。测中断延迟则反过来:让外设产生一个中断,同时在中断服务函数第一条语句处翻转另一个引脚,用两个通道看它们之间的时间差。前提是外设产生中断的瞬间你能用第三个通道或者已知的波形参照出来,比如用一个定时器输出一个已知相位的方波,中断打在这条方波的边沿上。

在 Cortex-M 上还有个更精细的办法——用 DWT 里的周期计数器(CYCCNT),在服务函数入口读一次、出口读一次,就能得到服务函数执行了多少个时钟周期,再除以主频换算成时间。这个办法对定位"到底哪个中断服务函数跑太久"极其有效。我排查过一个案子,主程序一切正常,设备运行几分钟后偶尔卡死,最后用这个方法量出来是某个串口中断服务函数里的printf占了几毫秒——串口在中断里打印,本身就是在中断里做了极慢的阻塞操作,一个字符还没发完下一个中断就来了,层层叠加,最终把系统拖垮。把打印挪到主循环里之后,问题当场消失。

5.3 DMA 加空闲中断:把中断次数压到最低的思路

说到中断优化,有一个思路特别值得单独拎出来讲,就是"减少中断次数"本身。中断不是越多越好,每一次中断都有压栈、仲裁、跳转、返回的开销,数据量大、速率高的时候,逐字节中断会把 CPU 的时间全吃在进出中断上。串口接收就是典型场景:波特率拉高之后,每个字节都触发一次接收中断,CPU 光是应付中断就快忙不过来,解析逻辑根本没时间跑。

一个成熟的做法是DMA 加空闲中断:用 DMA 把串口收到的数据自动搬进内存缓冲区,CPU 完全不参与逐字节搬运;然后再开一个"空闲中断"(总线检测到超过一帧时间没有新数据就触发),只有当一整包数据收完、线路安静下来时才打断 CPU 一次,让它一次性处理整帧。这样一来,一包数据从 N 次中断降到了 1 次中断,CPU 的负担瞬间下来,丢字节的概率也大幅降低。这个模式在 stm32 上很常见,也是"中断优化"里最有效的一招之一。它背后的思想可以推广到很多场景——能用 DMA 搬的活就别让 CPU 靠中断一个个来,让硬件去干重复劳动,CPU 只在"整件事完成"的关键节点被打扰一次。

6. 关中断与开中断的常见错法:六个真实踩过的坑

前面讲原理和写法,这一节专门讲错法。下面这些都是我和身边人在真实项目里遇到过的,每一条都配了排查思路,你可以对照自己的代码自查。

6.1 忘记恢复,以及恢复得太早

第一类错误是忘记恢复:进了临界区关了中断,中途因为某个return、某个出错分支、甚至某个异常提前出去了,开中断那句没被执行到,中断就一直关着。单核系统里中断全关着意味着定时器不走了、串口不收了、看门狗喂不上了,表现就是"程序跑着跑着卡死"。排查这类问题有个笨办法很好用:在关中断和开中断两个点各翻转一个专用 GPIO,用逻辑分析仪看这个引脚的高电平是不是出现了"一直不落"的情况,落不下去的地方就是漏了恢复。写代码时则尽量用"成对、单出口"的写法,别让临界区里有多个return。

第二类相反,是恢复得太早。你以为共享数据已经处理完了,提前把中断打开,结果后面还有几行也动到了这份数据,保护就形同虚设。这种 bug 更隐蔽,因为它不会死机,只会偶尔丢数据,正是前面说的那种偶发竞态。

6.2 在临界区里干了不该干的活

临界区里最不该干的事,按危险程度排大概是这样:调用可能会睡眠的操作(Linux 里关中断上下文中睡眠会直接崩)、做浮点运算(在带 FPU 的 Cortex-M 上会触发额外的压栈开销)、打印日志(极慢,占用中断禁用的时间以毫秒计)、等硬件标志位(可能永远等不到,尤其在中断被关掉的情况下)。这几样里,打印和等标志位是最常见的。等标志位尤其阴险:你在关中断的状态下等一个"需要中断才能被置位"的标志,那它永远不会置位,直接死循环。

判断一段代码该不该放进临界区,我习惯问自己一句:这段代码里有没有任何一步会"等"别人?只要有等待,就不该出现在关中断的保护范围里,因为它把关中断时间从一个确定的短值变成了不确定的长值。

6.3 多核下"我明明关了中断"的幻觉

这个坑在第 3.2 节提过,这里再强调一次,因为它太常见了。移植代码从单核到多核、或者从 MCU 到带多核的应用处理器时,原来的"关中断保护共享变量"会突然失效。现象是偶发的数据错乱,加日志更难复现,因为多核的时序比单核复杂得多。排查时先确认一件事:这份数据是不是不只被本地中断访问、还被别的核访问?如果是,光关中断远远不够,必须上跨核的锁。记住单核的"原子"到了多核经常就不再原子了。

6.4 中断服务函数里再开中断引起的重入

最后一个坑和"开中断"直接相关。有的服务函数执行时间偏长,开发者为了不让其它中断等太久,在服务函数中途手动开了中断,让高优先级中断能进来。想法没错,但如果你没考虑重入,就会出问题:同一个服务函数可能被自己或同类中断再次进入,上一轮的局部变量、被改了一半的全局状态还没收拾好,新的执行流就冲进来了。表现为数据错乱甚至栈溢出。正确的做法是:如果平台支持硬件优先级抢占(比如 Cortex-M),就让硬件去处理嵌套,别手动开关;如果确实要手动开,务必保证服务函数是可重入的——不依赖会被打断的全局中间状态,或者用重入计数保护起来。写中断服务函数有个总原则我一直坚持:尽量短、尽量纯粹、只做最紧急的那一件事,剩下的留给主循环。能遵守这条,上面一大半的坑会自动绕开。

我个人实际的体会是,中断这块最难的不是写出能跑的代码,而是写出"在最坏时序下也不会错"的代码。临界区长度、开关中断的配对、多核与中断的组合,这些平时看不出问题,一到高负载或者现场工况恶劣的时候才暴露。所以每写完一段带临界区的代码,我都会刻意问自己三件事:这段关中断最长会关多久、它有没有可能漏掉恢复、这份数据会不会被别的核碰到。把这三个问题答清楚,代码才敢放心交出去。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询