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退出后由调度器完成。

Tick ISR处理流程图 Tick ISR 处理流程图 Tick 中断触发 1. 更新系统节拍计数器 2. 处理任务延时队列 3. 时间片计数减一 时间片 用完? 是 标记需要切换 否 继续执行当前任务 退出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要短,要快,只做标记,不做处理。 这样你的系统才能跑得稳、跑得快。