4. Tick中断服务函数设计:Tick ISR的职责,处理任务延时与时间片轮转,避免ISR过长
Tick中断,说白了就是系统的心跳。没有它,任务调度就没了节奏。很多初学者觉得Tick ISR就是简单地把一个计数器加一,然后退出。嗯,如果你也这么想,那今天咱们得好好聊聊。
我个人习惯把Tick ISR看作整个RTOS的“脉搏”。它不能太复杂,否则系统会“心律不齐”;也不能太简单,否则任务延时和时间片轮转就全乱了。我在项目中遇到过好几次,因为Tick ISR里塞了太多活儿,导致系统响应变慢,甚至丢中断。那会儿排查起来真是头疼。
4.1 Tick ISR的核心职责
Tick ISR到底该干什么?我总结下来,就三件事:
- 更新系统节拍计数器 —— 这是最基础的,记录系统跑了多少个Tick。
- 处理任务延时 —— 检查哪些任务延时到期了,把它们从阻塞态唤醒。
- 触发时间片轮转 —— 如果当前任务的时间片用完了,就标记一下,让调度器在退出ISR后切换任务。
你想想看,这三件事其实都不复杂,但很多人容易犯一个错:在Tick ISR里做太多计算,或者调用那些会阻塞的函数。我曾经见过一个同事,在Tick ISR里打印调试信息,结果系统直接卡死。为什么?因为打印函数本身可能用了互斥锁,而中断里拿锁,很容易死锁。
核心原则:Tick ISR 应该只做“标记”和“计数”,不做“处理”。真正的任务切换和延时处理,交给调度器在退出ISR后完成。
4.2 任务延时处理机制
任务延时,说白了就是让任务“睡一会儿”。每个任务都有一个延时计数器,Tick ISR每来一次,就把所有正在延时的任务的计数器减一。当计数器减到0,就把这个任务从延时队列移到就绪队列。
这里有个细节:延时队列怎么组织?
| 组织方式 | 优点 | 缺点 |
|---|---|---|
| 链表(按延时时间排序) | 每次Tick只需检查链表头部,效率高 | 插入新任务时需要排序,稍复杂 |
| 数组(固定大小) | 实现简单,适合任务数固定的场景 | 每次Tick需要遍历所有任务,浪费CPU |
| 时间轮(Time Wheel) | 插入和检查都是O(1),适合大量任务 | 实现稍复杂,占用内存稍多 |
我个人习惯用链表,按延时时间从小到大排序。这样每次Tick中断,只需要检查链表头部,如果头部没到期,后面的肯定都没到期。效率很高。
小技巧:如果任务延时时间很长(比如几秒),可以考虑用“分频”的方式。比如每10个Tick才检查一次长延时任务,减少ISR负担。
4.3 时间片轮转的实现
时间片轮转,就是让同优先级的任务轮流执行。每个任务有一个时间片计数器,Tick ISR每来一次,就把当前任务的时间片计数器减一。减到0时,就标记一下,让调度器在退出ISR后切换到下一个同优先级任务。
这里要注意:时间片轮转只对同优先级的任务有效。不同优先级的任务,还是按优先级抢占。
// 伪代码:Tick ISR中的时间片处理
void Tick_ISR(void) {
// 1. 更新系统节拍
system_tick++;
// 2. 处理任务延时
ProcessTaskDelay();
// 3. 处理时间片轮转
if (current_task->time_slice > 0) {
current_task->time_slice--;
if (current_task->time_slice == 0) {
// 标记需要切换任务
need_switch = 1;
// 重置时间片
current_task->time_slice = DEFAULT_TIME_SLICE;
}
}
// 4. 触发调度(在ISR退出时)
if (need_switch) {
TriggerContextSwitch();
}
}
你看,代码其实很简单。但有个坑:时间片的值不能太小,否则任务频繁切换,上下文切换的开销会吃掉CPU。我建议时间片至少是系统Tick周期的5-10倍。比如Tick周期是1ms,时间片可以设成5ms或10ms。
避坑指南:我曾经把时间片设成1个Tick,结果系统大部分时间都在做上下文切换,任务反而跑不动。后来改成10个Tick,性能明显提升。记住:时间片不是越小越好。
4.4 避免ISR过长的实战技巧
ISR过长,是嵌入式开发中最常见的问题之一。Tick ISR尤其容易“膨胀”,因为开发者总想在里面塞点“小功能”。
我总结了几条实战经验:
- 不要在ISR里调用printf或任何I/O操作。 这些函数通常很慢,而且可能涉及锁或中断嵌套,容易出问题。
- 不要在ISR里做浮点运算。 浮点运算在中断里会占用大量CPU时间,而且可能破坏浮点寄存器上下文。
- 不要在ISR里申请动态内存。 malloc和free不是可重入的,在中断里调用可能导致内存管理崩溃。
- 把耗时操作放到任务里做。 如果Tick ISR里需要处理复杂逻辑,可以用“软中断”或“信号量”通知一个高优先级任务去处理。
举个例子,我之前做一个项目,需要在每个Tick里更新一个复杂的滤波算法。如果直接在ISR里算,CPU占用率直接飙到80%。后来我改成在ISR里只设置一个标志,然后在任务里处理滤波,CPU占用率降到了10%。
黄金法则:Tick ISR的执行时间,不要超过系统Tick周期的10%。如果Tick周期是1ms,ISR执行时间不要超过100us。超过这个值,系统响应就会变差。
4.5 Tick ISR的典型流程图
下面这张图,是我画的一个典型的Tick ISR处理流程。你可以看到,ISR只做标记和计数,真正的任务切换在ISR退出后由调度器完成。
4.6 实测:Tick ISR的优化效果
最后,我分享一个实际项目的优化数据。之前一个项目,Tick ISR里做了很多事,包括任务延时处理、时间片轮转、还有几个软件定时器的更新。ISR执行时间大约150us,Tick周期是1ms,CPU占用率15%。
优化后,我把软件定时器的更新移到了任务里,ISR只做任务延时和时间片轮转。ISR执行时间降到了30us,CPU占用率降到3%。系统响应明显变快,而且再也没有丢过中断。
| 指标 | 优化前 | 优化后 |
|---|---|---|
| ISR执行时间 | 150 us | 30 us |
| CPU占用率 | 15% | 3% |
| 系统响应延迟 | 有时超过2ms | 稳定在1ms以内 |
| 中断丢失次数 | 偶尔发生 | 从未发生 |
嗯,这就是Tick ISR设计的核心思路。记住:ISR要短,要快,只做标记,不做处理。 这样你的系统才能跑得稳、跑得快。