3、中断上下文:任务上下文与中断上下文、中断上下文切换开销、中断上下文限制
3.1 任务上下文与中断上下文
咱们先聊聊什么是上下文。说白了,上下文就是CPU干活时的“工作状态”。
任务上下文,就是普通任务(比如你写的应用线程)在运行时,CPU里那一堆寄存器的快照。包括程序计数器、堆栈指针、通用寄存器等等。我习惯把它理解成“任务的灵魂”。任务被切换出去时,灵魂得存好;切回来时,再还给它。
中断上下文呢?嗯,这个有点不一样。当硬件中断来了,CPU会暂停当前任务,跳去执行中断服务程序(ISR)。这时候,ISR运行的环境就是中断上下文。它不依赖任何任务,也不属于哪个进程。说白了,它是个“临时工”,干完活就走。
我在项目中遇到过一个问题:一个同事在ISR里调用了printf(),结果系统直接死机。为什么?因为printf()依赖任务上下文里的资源(比如锁、缓冲区),而中断上下文里根本没有这些。你想想看,一个临时工跑去用正式员工的工位,不出乱子才怪。
- 任务上下文:有独立的堆栈、调度器管理、可以睡眠
- 中断上下文:借用被打断任务的堆栈、不可睡眠、不可阻塞
3.2 中断上下文切换开销
每次中断发生,CPU都要做一次上下文切换。这个开销,比你想象的大。
我给你们拆解一下步骤:
- CPU自动压栈:PC、状态寄存器、某些通用寄存器
- 中断控制器裁决:哪个中断优先级高?
- 跳转到ISR入口:查中断向量表
- ISR手动压栈:保存更多寄存器(如果ISR用到了)
- 执行ISR逻辑
- 出栈恢复:逆序弹出所有寄存器
- 中断返回:恢复PC,继续执行原任务
你看,光是压栈出栈,就折腾了好几步。我记得有一次做ARM Cortex-M的优化,发现一个高频中断(1kHz)占了CPU 15%的时间。一查,全是上下文切换的锅。
具体开销有多大?我给你们一个参考数据:
| 操作 | 典型时钟周期数 |
|---|---|
| 中断响应(硬件压栈+跳转) | 12~20 cycles |
| ISR手动保存寄存器 | 10~30 cycles |
| ISR执行(假设很短) | 50~200 cycles |
| 恢复+返回 | 15~25 cycles |
| 总计 | 87~275 cycles |
注意,这还只是单次中断。如果你的系统里中断嵌套了,那开销会成倍增加。我曾经在一个项目中,三层中断嵌套,CPU光切换就花了40%的时间。嗯,后来我硬是把三层嵌套压成了一层,才把性能救回来。
3.3 中断上下文限制
中断上下文里能做什么,不能做什么,这是有规矩的。我总结了几条铁律:
- 不能睡眠:中断上下文没有调度器管理,你睡下去就没人叫得醒你。整个系统会挂掉。
- 不能获取互斥锁:万一锁被别的任务持有了,你只能等。但中断里不能等,一等等出死锁。
- 不能调用阻塞API:比如
read()、write()、malloc()。这些函数都可能触发调度,中断里一调度就乱套了。 - 不能访问用户空间:中断上下文属于内核空间,访问用户地址会触发缺页异常,而缺页异常又需要睡眠……死循环。
- 堆栈空间有限:中断用的是被打断任务的堆栈,或者专用的中断栈。通常只有几KB。你递归调用一下,栈就爆了。
vTaskDelay(),想着延迟1毫秒再处理。结果系统直接卡死。后来查手册才发现,FreeRTOS明确说了中断里不能调用vTaskDelay()。嗯,从那以后,我写ISR之前都会先翻一遍API文档的“中断安全”章节。
那中断上下文里到底能做什么?我列个清单:
- 读写硬件寄存器(比如清中断标志)
- 操作原子变量(比如
atomic_inc()) - 发送信号量或事件(用中断安全的API)
- 更新时间戳或计数器
- 把数据塞进无锁队列
说白了,中断上下文就是个“快递员”。它只管把包裹(数据)放到门口(队列),然后通知主人(任务)来取。千万别让快递员帮你拆包裹、做登记、写报告——那是主人该干的事。
3.4 知识体系结构图
下面这张图,是我梳理的中断上下文核心逻辑。你看一眼,就能把整个章节串起来。
这张图把三个核心知识点串起来了。你从左往右看:先搞清楚两种上下文的区别,再理解切换开销有多大,最后记住中断上下文里的禁忌。嗯,这三块搞明白了,中断处理这块你就入门了。
可可精品资料库