4、定时器启动与停止:API接口设计
定时器的启动和停止,说白了就是两个核心动作:把定时器挂到时间轮上,以及从时间轮上摘下来。听起来简单,但这里面的门道可不少。我刚开始写RTOS内核时,就因为在启动流程里少考虑了一个边界条件,结果系统跑着跑着就崩了。
4.1 API接口设计:timer_start / timer_stop
先看接口定义。我个人习惯把接口设计得尽量简洁,调用者不需要知道时间轮内部怎么运作。
// 启动一个软定时器
// @param timer 定时器控制块指针
// @param ticks 定时时长(以系统tick为单位)
// @param mode 定时模式:TIMER_ONCE 或 TIMER_PERIODIC
// @return 0 成功,-1 失败
int timer_start(timer_t *timer, uint32_t ticks, uint32_t mode);
// 停止一个软定时器
// @param timer 定时器控制块指针
// @return 0 成功,-1 失败
int timer_stop(timer_t *timer);
你可能会问:为什么返回值用int而不是void?嗯,这里要注意。启动和停止都有可能失败。比如定时器已经在运行中,你再启动它,就应该返回错误。我在项目中遇到过有人反复调用timer_start,结果时间轮上挂了同一个定时器多次,超时回调被触发了N遍。
避坑指南: 我曾经在调试一个工业控制设备时,发现定时器回调被重复触发。查了两天才定位到问题——调用者没有检查返回值,以为每次启动都是新的。所以,接口设计一定要留错误返回通道。
4.2 启动流程:插入时间轮
启动一个定时器,核心工作就是把它插入到时间轮的某个槽位里。流程大致如下:
- 检查状态:定时器是否已经在运行?如果是,直接返回错误。
- 计算超时tick:当前系统tick + 定时时长 = 绝对超时时间点。
- 计算槽位:根据超时tick,找到时间轮上对应的槽位。
- 插入链表:把定时器节点挂到该槽位的链表尾部。
- 更新状态:标记定时器为“运行中”。
这里有个关键点:槽位计算。时间轮通常是一个环形数组,每个槽位对应一个tick范围。比如一个256槽的时间轮,每个tick走一格,那么超时tick对256取模,就得到槽位索引。
static int timer_insert(timer_t *timer, uint32_t expire_tick) {
uint32_t slot = expire_tick & (TIMER_WHEEL_SIZE - 1); // 取模运算,用位运算加速
// 关中断,保护链表操作
uint32_t level = cpu_int_disable();
// 插入到槽位链表尾部
list_add_tail(&timer->node, &timer_wheel[slot]);
timer->expire_tick = expire_tick;
timer->state = TIMER_STATE_RUNNING;
// 恢复中断
cpu_int_restore(level);
return 0;
}
个人经验: 我习惯用位运算代替取模,因为时间轮大小通常是2的幂次。这样既快又简洁。但如果你用的不是2的幂次,那就老老实实取模吧,别为了炫技搞出bug。
4.3 停止流程:从时间轮移除
停止定时器,就是把它从时间轮的链表里摘下来。流程相对简单:
- 检查状态:定时器是否在运行?如果已经停止,直接返回。
- 从链表移除:把定时器节点从槽位链表中摘除。
- 更新状态:标记为“已停止”。
这里有个容易踩的坑:中断上下文。如果定时器回调函数正在执行,而此时另一个任务调用了timer_stop,怎么办?
我曾经踩过的坑: 有一次在通信协议栈里,一个定时器超时回调正在发送数据,另一个线程却把这个定时器给停了。结果链表节点被删了一半,另一半还在回调里用着,直接导致内存访问异常。从那以后,我强制要求在停止定时器时,必须等待回调执行完毕(如果正在执行的话)。
int timer_stop(timer_t *timer) {
if (timer->state != TIMER_STATE_RUNNING) {
return -1; // 已经停了,别重复操作
}
uint32_t level = cpu_int_disable();
// 从链表移除
list_del(&timer->node);
timer->state = TIMER_STATE_STOPPED;
cpu_int_restore(level);
return 0;
}
4.4 启动与停止的对称性
你想想看,启动和停止其实是一对对称操作。启动时加锁、插入、更新状态;停止时加锁、移除、更新状态。这种对称性在设计时一定要保持住。
| 操作 | 前置条件 | 核心动作 | 后置状态 |
|---|---|---|---|
| timer_start | 定时器未运行 | 插入时间轮槽位链表 | TIMER_STATE_RUNNING |
| timer_stop | 定时器正在运行 | 从时间轮槽位链表移除 | TIMER_STATE_STOPPED |
我个人建议,在实现时把链表操作和状态更新放在同一个临界区里。不要先更新状态再操作链表,也不要反过来。原子性很重要,否则会出现状态和链表不一致的情况。
小技巧: 如果你在调试时发现定时器莫名其妙消失了或者重复触发了,先检查临界区保护是否到位。八成是中断没关好,或者链表操作和状态更新没放在一起。
嗯,启动和停止的接口设计就讲到这里。说白了,核心就是两件事:插入和移除。但细节决定成败,边界条件、中断保护、状态一致性,这些才是真正考验功力的地方。下一节我们会讲定时器的超时处理,也就是时间轮怎么“转”起来。