2、核心数据结构设计:定时器控制块(TCB)设计、定时器状态机(空闲、运行、停止)、定时器链表组织方式
好,咱们接着聊软定时器的核心设计。
上一章我讲了定时器的整体架构,说白了就是「谁在管、怎么管、管什么」。这一章咱们深入到底层,看看定时器控制块(TCB)到底长什么样,状态机怎么转,链表怎么挂。
这部分我当年第一次写RTOS内核时踩过不少坑,尤其是状态机的边界条件。嗯,咱们一个一个来拆。
2.1 定时器控制块(TCB)设计
定时器控制块,就是每个定时器的「身份证」。内核要管理一个定时器,必须知道它什么时候到期、到期后干什么、当前处于什么状态。
我个人习惯把TCB设计成结构体,里面包含以下几类信息:
- 标识信息:定时器ID、名称(调试用)
- 时间信息:初始超时值、剩余时间、是否周期模式
- 回调信息:到期回调函数指针、回调参数
- 状态信息:当前状态(空闲/运行/停止)
- 链表信息:前驱和后继指针(用于挂入链表)
下面是我常用的TCB定义,你可以参考一下:
typedef struct rt_timer {
char name[RT_NAME_MAX]; /* 定时器名称 */
rt_uint8_t id; /* 定时器ID */
rt_uint8_t state; /* 当前状态 */
rt_uint32_t init_tick; /* 初始超时tick数 */
rt_uint32_t remain_tick; /* 剩余tick数 */
rt_uint8_t is_periodic; /* 是否周期定时器 */
void (*callback)(void *arg); /* 回调函数 */
void *arg; /* 回调参数 */
struct rt_timer *prev; /* 链表前驱 */
struct rt_timer *next; /* 链表后继 */
} rt_timer_t;
这里有个细节我想强调一下:remain_tick 这个字段。我在项目中遇到过一种情况,定时器被暂停后又恢复,如果只保存初始值,恢复时就会丢失已经流逝的时间。所以 remain_tick 必须动态更新,每次tick中断减1,暂停时冻结,恢复时继续。
核心要点:TCB是定时器的「灵魂」,设计时要把「时间管理」和「状态管理」分开。时间字段负责倒计时,状态字段负责生命周期。
2.2 定时器状态机:空闲、运行、停止
定时器的状态机其实不复杂,就三个状态:空闲、运行、停止。但边界条件处理不好,很容易出bug。
我画个状态转换图给你看:
创建
┌───────────┐
│ ▼
│ ┌──────┐ 启动 ┌──────┐
│ │ 空闲 │─────────▶│ 运行 │
│ └──────┘ └──────┘
│ ▲ │ │
│ │ │ │
│ │ 超时/停止 │ │
│ │◄───────────────┘ │
│ │ │
│ │ 暂停 │
│ │◄──────────────────┘
└───────────┘
三个状态的含义:
| 状态 | 含义 | 典型操作 |
|---|---|---|
| 空闲 | 定时器已创建,但未启动 | 可启动、可删除 |
| 运行 | 定时器正在倒计时 | 可暂停、可停止、可修改超时值 |
| 停止 | 定时器被暂停,剩余时间保留 | 可恢复、可停止(重置) |
你可能会问:「为什么要有停止状态?直接回到空闲不行吗?」
嗯,这里有个实际场景。我在做物联网设备时,有个定时器控制传感器采样周期。用户可能临时暂停采样,但希望恢复后继续从暂停点开始计时,而不是从头开始。这时候停止状态就派上用场了——它保留了 remain_tick。
避坑指南:我曾经在状态转换时忘记处理「运行→停止→运行」的恢复逻辑,结果恢复后定时器直接超时了。后来加了个标志位,判断是「首次启动」还是「恢复启动」,才彻底解决。
2.3 定时器链表组织方式
定时器怎么组织?最常用的就是双向链表。为什么不用数组?因为定时器数量动态变化,数组不好扩展。为什么不用单向链表?因为删除操作需要遍历,效率低。
我一般用两种链表组织方式:
- 全局链表:所有定时器挂在一个链表上,简单粗暴。
- 分级链表:按超时时间分组,比如1-10 tick一组,11-100 tick一组,提高查找效率。
对于初学者,我建议先用全局链表。等你对内核调度熟悉了,再考虑优化。
全局链表的组织方式如下:
/* 全局定时器链表头 */
static rt_timer_t *timer_list_head = RT_NULL;
/* 插入定时器到链表尾部 */
void rt_timer_insert(rt_timer_t *timer) {
rt_timer_t *node = timer_list_head;
if (node == RT_NULL) {
timer_list_head = timer;
timer->prev = RT_NULL;
timer->next = RT_NULL;
return;
}
while (node->next != RT_NULL) {
node = node->next;
}
node->next = timer;
timer->prev = node;
timer->next = RT_NULL;
}
/* 从链表中删除定时器 */
void rt_timer_remove(rt_timer_t *timer) {
if (timer->prev != RT_NULL) {
timer->prev->next = timer->next;
} else {
timer_list_head = timer->next;
}
if (timer->next != RT_NULL) {
timer->next->prev = timer->prev;
}
timer->prev = RT_NULL;
timer->next = RT_NULL;
}
这里有个小技巧:插入时按超时时间排序,把剩余时间短的放在前面。这样每次tick中断只需要检查链表头部的几个定时器,不用遍历整个链表。我管它叫「懒人优化法」——效果立竿见影。
注意:链表操作一定要关中断!我在项目中遇到过,tick中断里修改链表,同时任务也在操作同一个定时器,结果链表指针乱飞,系统直接挂掉。后来统一用临界区保护,再也没出过问题。
2.4 小结
这一章咱们把定时器的「骨架」搭好了。TCB是数据载体,状态机是行为逻辑,链表是组织方式。三者缺一不可。
下一章我会讲定时器的核心机制——tick驱动和超时检测。到时候你会看到,这些数据结构是怎么在中断里「活」起来的。
嗯,先消化这些吧。有问题随时问我。