7.2 KiB
7.2 KiB
µC/OS 与 SYS/BIOS:中断进入/退出 与分层调度
这两个内核都把"中断参与调度"做成了**显式协议**,于是:ISR 尾部突然发生上下文切换
→ 合理行为,不是异常
task 已 ready,却要等中断退出后才运行
→ 不是调度延迟
判断调度时机时,必须把"中断进入/退出"这一段算进控制流。 </CORE RULE>
一、Micrium µC/OS:ISR 必须通知内核
标准 ISR 形态:
保存寄存器
→ OSIntEnter() (或直接 OSIntNestingCtr++)
→ …处理…
→ OSIntExit()
恢复寄存器 → 从中断返回
OSIntEnter()/OSIntExit()必须成对,且OSIntEnter()不能被任务级代码调用OSIntEnter()递增全局中断嵌套计数,从而保证 ISR 执行期间不做任务调度- 实现细节:若内核未在运行则直接返回;嵌套计数达到上限(250)也返回
- 直接递增计数可省去函数调用开销,但要求"递增时中断已关闭"——并非所有架构都成立,需要自己确认
OSIntExit() 的决策序列(这是恢复控制流的关键):
内核未运行 → 返回
关中断 → 若嵌套计数为 0 → 开中断返回(防计数回绕)
递减嵌套计数 → 若仍 > 0(还有嵌套 ISR)→ 开中断返回
若调度器仍被锁 → 开中断返回
找出最高优先级就绪任务 → 与当前任务相同 → 开中断返回
否则更新切换计数并执行"中断级上下文切换"
- 即:只有最后一个嵌套 ISR 退出时才判断"是否需要切换到被 ISR 变为就绪的更高优先级任务"
- 在 Cortex-M 上,该切换通常只是触发 PendSV(最低优先级异常),真正的切换推迟到所有 ISR 完成之后
所以:ready 但没运行 常可解释为"还在 ISR 里 / 调度器被锁 / PendSV 尚未执行"。
二、TI SYS/BIOS:四级线程 + Swi 禁用会连带禁用 Task 调度
线程层次(优先级由高到低):
Hwi(硬件中断) > Swi(软件中断) > Task > Idle
- Swi 不能阻塞、总是运行到完成;Task 可以阻塞,且会被任何更高优先级的线程抢占
- Swi 与 Task 都有优先级级别(数值越大优先级越高,0 最低)
关键的连带效应:
Swi_disable()会同时禁用 Task 调度器——等价于同时调用"禁 Task"与"禁 Swi"- 因此:
Swi_post()在临界区中不会立即运行;即使Semaphore_post()让更高优先级任务就绪,当前任务也不会在Swi_restore()之前被抢占 Swi_restore()在此前 Task 调度器未被禁用的前提下,会重新启用并进入 Task 调度器- 硬性约束:Swi 调度器被禁用时绝不能调用阻塞 API(如信号量等待/门互斥进入),否则会"不可恢复地损坏"Task 调度器内部状态,行为"不可预测且通常是灾难性的"
- 禁用某一层不影响更高层抢占(
Swi_disable()不阻止 Hwi 抢占)
栈模型:
- Hwi 与 Swi 共用同一个系统中断栈(ISR 栈),一个程序一个;每个 Task 有私有栈
- 若系统里没有 Task,所有线程共用一个系统栈;默认系统栈大小 4096 字节
- 每多一个嵌套的 Hwi/Swi 优先级层次,栈需求就上升;Swi 栈需求与所用 Swi 优先级数量成正比
所以:
高优先级 Task 已就绪却不切换
→ 先查 Swi 调度器是否被禁用(它会连带禁 Task 调度)
→ 不要先找死锁
三、µC/OS 的其余高频误判
调度器锁与挂起是可嵌套的:
OSSchedLock()是可嵌套的:即使退出了 ISR,只要调度器仍被锁住就不会重调度;必须成对解除OSTaskSuspend()同样可嵌套——长期不就绪的高优先级任务先查这两者的嵌套层级,再谈优先级反转
信号量 vs 互斥量:
- 多个任务可同时共享的资源 → 信号量;独占资源 → 互斥量(互斥量实现会处理优先级反转)
- 因此出现"低优先级任务影响高优先级任务"时,先确认作者是不是把信号量当互斥量用了
软件定时器回调运行在定时器任务上下文(这是很值钱的一条):
- 回调在 OS 内部的定时器任务上下文中执行——既不是中断,也不是创建/启动该定时器的任务
- 用的是定时器任务的栈,所以定时器任务的栈要按回调需求配足
- 回调里绝不能做阻塞/等待(延时、任何
OS???Pend())——因为所有回调在定时器任务里串行执行,阻塞一个回调会把整个定时器子系统卡住 - 所以:
TimerCallback() → 内部有 blocking wait
→ 高价值的 bug 信号(不是"偶发卡顿")
PendAbort:被唤醒 ≠ 拿到资源:
- 中止等待(针对任务内建消息队列或普通消息队列)的作用是中止等待并让任务就绪,而不是正常投递
- 只能由任务调用(不能从 ISR 调);指针为 NULL 或指向调用者自己是无效用法
- 被唤醒的任务会带着"等待被中止"的错误码返回
所以:任务被唤醒、但队列/信号量里并没有新消息——先看是不是中止等待路径,"被唤醒"与"成功获取资源"是两件事。
决策树
拿到 µC/OS 或 SYS/BIOS 目标
├─ 调度时机对不上
│ ├─ µC/OS:查 OSIntNestingCtr(是否还在 ISR 里)、调度器锁、PendSV 是否已执行
│ ├─ SYS/BIOS:查 Swi 调度器是否被禁用(连带禁用 Task 调度)
│ └─ 别先判"调度延迟/死锁"
├─ ISR 内调用内核 API
│ ├─ µC/OS:是否成对调用 OSIntEnter/OSIntExit
│ ├─ SYS/BIOS:是否误用阻塞 API(Swi 禁用期间调用会损坏调度器状态)
│ └─ 注意 SYS/BIOS 的 Swi 不能阻塞
├─ 栈相关异常
│ ├─ SYS/BIOS:Hwi 与 Swi 共用 ISR 栈;嵌套层次与 Swi 优先级数影响栈需求
│ └─ 默认系统栈 4096 字节,注意配置项
└─ 看到"直接递增嵌套计数"的写法
└─ 只在"递增时中断已关闭"的架构上安全,需逐架构确认
工具与验证
- 中断协议:ISR 的进入/退出调用是否成对;嵌套计数的读写点
- 调度禁用:调度器锁、Swi/Task 禁用标志与恢复点
- 栈:系统(ISR)栈与任务栈的划分、嵌套层次与栈配置
- 验证:能同时说清「当前是否在 ISR 上下文 + 哪一层的调度被禁用 + 切换在何时真正发生」
该平台的坑(汇总)
- 把 ISR 尾部的切换当异常:那是中断退出时的正常调度决策
- 忽略嵌套计数:只有最后一个嵌套 ISR 退出才判断切换
- 以为 PendSV 触发就等于已切换:真正的切换在所有 ISR 完成后
- 在 µC/OS 里让任务级代码调用
OSIntEnter():协议禁止 - 照搬"直接递增计数"的优化:只在特定架构安全
- 忘记
Swi_disable()会连带禁用 Task 调度:这是 SYS/BIOS 最反直觉的一条 - 在 Swi 调度器禁用期间调用阻塞 API:会不可恢复地损坏调度器状态
- 按"每个线程独立栈"估算 SYS/BIOS 栈需求:Hwi 与 Swi 共用 ISR 栈