第3章:关中断实现互斥——临界区保护与中断延迟影响分析
各位同学,咱们今天聊一个RTOS里最基础、也最“暴力”的互斥手段——关中断。
说实话,我刚入行那会儿,觉得这玩意儿太简单了。不就是把中断关了,干完活再打开嘛?后来在项目里被坑了几次,才明白这里面的门道有多深。嗯,今天我就把这些年踩过的坑、总结的经验,一次性讲清楚。
3.1 关中断为什么能实现互斥?
先想一个问题:临界区冲突的本质是什么?
说白了,就是两个执行流同时访问了同一个资源。在单核CPU上,这两个执行流要么是任务,要么是中断服务程序(ISR)。
那关中断为什么能解决问题?
因为中断是唯一能“抢走”CPU控制权的异步事件。你把中断一关,当前任务就独占CPU了。没有其他任务能打断你,ISR也进不来。这不就互斥了嘛?
核心原理:关中断 = 禁止所有可屏蔽中断 → 当前执行流独占CPU → 临界区安全
我在一个工业控制项目里遇到过这种情况:两个任务共享一个全局变量,用来记录传感器采样次数。不加保护的时候,偶尔会出现计数少一次的情况。查了半天,发现是任务A刚读完变量,还没来得及写回去,就被定时器中断打断了,中断里恰好也修改了这个变量。嗯,典型的“读-改-写”冲突。后来在操作前后加了关中断/开中断,问题就消失了。
3.2 关中断的典型用法
代码其实很简单,但细节决定成败。我给大家看一个标准模板:
// 保存当前中断状态,然后关中断
uint32_t saved_status = disable_interrupts();
// ---------- 临界区开始 ----------
// 这里操作共享资源
shared_counter++;
global_flags |= 0x01;
// ---------- 临界区结束 ----------
// 恢复之前的中断状态
restore_interrupts(saved_status);
注意看,我用了saved_status来保存状态。为什么?
因为调用这个函数的时候,中断可能本来就是关着的。如果你直接开中断,就会破坏调用者的状态。这在嵌套调用时特别容易出问题。我曾经见过一个同事,直接在临界区开头关中断,结尾开中断。结果在一个深层嵌套的调用链里,外层已经关了中断,内层一开,整个系统的时序全乱了。嗯,那天的调试过程,至今记忆犹新。
我的习惯:永远使用“保存-关-恢复”三步法。不要直接用“关-开”这种粗暴方式。这是RTOS编程的基本素养。
3.3 中断延迟——关中断的代价
关中断虽然简单粗暴,但代价也很明显:中断延迟。
什么叫中断延迟?就是硬件产生中断信号,到CPU真正开始执行ISR之间的时间差。你关中断的时间越长,这个延迟就越大。
咱们用表格来看一下影响因素:
| 因素 | 影响程度 | 说明 |
|---|---|---|
| 临界区长度 | 直接正比 | 关中断时间越长,延迟越大 |
| 中断优先级 | 间接影响 | 高优先级中断要求更短的关中断时间 |
| CPU架构 | 硬件相关 | 有些CPU关中断需要多条指令,本身就有延迟 |
| 嵌套深度 | 累积效应 | 多层嵌套关中断会叠加延迟 |
你想想看,如果一个系统里有关中断超过100微秒的临界区,而系统里有一个1kHz的定时器中断(周期1ms),那这个中断的抖动就会达到10%以上。对于电机控制、音频处理这类实时性要求高的场景,这简直是灾难。
避坑指南:我曾经在一个无人机飞控项目里,因为一个关中断的临界区写了200多行代码,导致GPS数据中断的延迟从5微秒飙升到80微秒。结果无人机在空中出现位置漂移,差点炸机。从那以后,我给自己定了个规矩:关中断的临界区,代码行数不超过20行,执行时间不超过10微秒。
3.4 关中断的适用场景
不是所有地方都适合关中断。我总结了一下,适合用关中断的场景有这几个:
- 操作极其简单的共享变量:比如一个标志位、一个计数器,几条指令就能搞定
- 中断ISR内部的临界区:中断里不能使用信号量(会阻塞),关中断是唯一选择
- CPU架构不支持其他同步机制:比如一些低端MCU,没有原子指令
- 对延迟不敏感的系统:比如温度采集,慢个几毫秒无所谓
不适合的场景:
- 长时间操作:比如大数据拷贝、复杂计算
- 调用可能阻塞的函数:关中断期间不能调用delay()、等待信号量等
- 实时性要求极高的系统:比如电机FOC控制、音频流处理
3.5 如何测量关中断的影响?
我建议每个做RTOS开发的工程师,都养成一个习惯:量化你的关中断时间。
方法很简单:
- 在关中断前,把一个GPIO拉高
- 在开中断后,把GPIO拉低
- 用示波器或者逻辑分析仪抓这个GPIO的波形
- 高电平的宽度就是关中断时间
我在项目里经常用这个方法做“地毯式排查”。把系统中所有关中断的地方都测一遍,然后列个表:
| 临界区位置 | 关中断时间(us) | 触发频率 | 最大累积延迟 | 是否可优化 |
|---|---|---|---|---|
| 任务A:更新计数器 | 0.5 | 100Hz | 0.5us | 否 |
| 任务B:拷贝传感器数据 | 15.2 | 10Hz | 15.2us | 是,改用DMA |
| ISR:处理串口数据 | 3.8 | 1kHz | 3.8us | 否 |
| 任务C:日志输出 | 120.0 | 1Hz | 120us | 是,必须优化 |
看到任务C那个120微秒了吗?这就是典型的“定时炸弹”。虽然它只触发1Hz,但一旦触发,所有中断都要等120微秒。如果这时候有个紧急的中断进来,系统响应就慢了。
3.6 关中断 vs 其他互斥方式
咱们简单对比一下关中断和其他互斥手段:
| 互斥方式 | 延迟影响 | 适用场景 | 复杂度 |
|---|---|---|---|
| 关中断 | 全局延迟 | 极短临界区、ISR内部 | 低 |
| 信号量 | 无(但可能阻塞) | 任务间同步、较长操作 | 中 |
| 互斥锁 | 无(但可能优先级反转) | 任务间互斥、资源保护 | 高 |
| 原子操作 | 极低(硬件级) | 简单变量操作 | 低 |
我个人习惯是:能用原子操作就不用关中断,能用信号量就不用关中断,关中断只留给最紧急、最简短的场景。
3.7 实战建议
最后,给大家几条我在项目里总结出来的实战建议:
- 关中断时间要量化:不要凭感觉,用示波器测一下
- 临界区要短小精悍:只保护真正需要保护的代码,别把无关操作包进去
- 注意嵌套:用保存/恢复的方式,不要直接开/关
- ISR里只能用关中断:别在ISR里用信号量,会死锁
- 定期审查:每个版本迭代后,重新检查所有关中断的地方
嗯,关中断这个话题,看似简单,但用好了是利器,用不好就是定时炸弹。希望大家在实际项目中,能把握好这个度。
下一章,咱们聊聊信号量的实现原理,那又是另一番天地了。