4.5.2 哈希桶定时器
4.5.2 哈希桶定时器
📚 本节导读
学习时长: 约 35 分钟
难度级别: ⭐⭐⭐☆☆
前置知识: 哈希表基础、系统时钟概念、双向链表、位运算(&、~、<<)
哈希桶定时器(Hash Bucket Timer)是 OneOS 内核提供的软件定时器实现方案。它以"空间换时间"为设计思想,把超时时间按哈希值分散到多个"桶"中,从而把定时器插入操作从 O(n) 降到 O(1)。同时,它借助位掩码(bitmask)标记"哪个桶里有到期定时器",让每个 tick 中断只需扫描少数桶,并交由定时器任务统一执行回调,非常适合定时器数量多、频繁创建销毁的场景。
说明:本节的哈希桶版定时器 API 实现源码见下方"五、哈希桶定时器 API 实现";统一的 API 签名、参数、返回值与使用示例见
4.5.4-定时器API.md。本节主体聚焦哈希桶定时器的数据结构、位掩码机制、tick 处理流程与对外 API 实现。
🎯 学习目标
- 理解哈希桶定时器的设计思想和 O(1) 插入原理
- 掌握哈希桶定时器的内部数据结构(桶数组、位掩码、句柄标志)
- 理解定时器任务如何按位掩码处理到期定时器
- 理解每个 tick 中断中
k_timer_need_handle()与k_move_timer_list_one_step()的分工 - 掌握哈希桶版
os_timer_*对外 API 的实现流程 - 明确哈希桶定时器的适用场景与选型边界
一、哈希桶定时器概念
1.1 设计思想:空间换时间
哈希桶定时器将系统所有软件定时器,按"超时时间离当前时刻还有多少个 tick"这个哈希值,分散到一组环形排列的桶中。每个桶是一个双向链表,存放超时时刻落在该桶的定时器。
这样,插入一个定时器时只需:
- 计算它应该落在哪个桶(一次哈希运算);
- 直接挂到该桶链表的尾部。
无需遍历其他定时器,因此插入时间复杂度为 O(1),与系统中已有的定时器数量无关。
1.2 为什么需要哈希桶(与单链表对比)
先看传统单链表定时器的困境:所有定时器按到期时刻排成一条有序链表,插入时要从头遍历,找到"到期时刻比自己晚"的节点插入。当定时器有几百个时,平均每次插入要比较几百次,开销显著,且时间不可预测。
类比:单链表定时器像"一列按身高排队的队伍",新来者要从头一个个比身高找位置(O(n));哈希桶定时器则像"按学号尾号分到 8 个抽屉",直接算尾号放进对应抽屉(O(1))。
| 对比维度 | 单链表定时器 | 哈希桶定时器 |
|---|---|---|
| 插入时间复杂度 | O(n),需遍历链表 | O(1),哈希定位桶 |
| 插入时间确定性 | 差,取决于定时器数量 | 好,恒定 |
| 定时器数量多时性能 | 明显下降 | 几乎不受影响 |
| 是否支持硬件(ISR)回调 | 支持 | 不支持,只有软件定时器 |
| 实现复杂度 | 简单 | 较复杂 |
1.3 哈希桶定时器使用场景
一句话核心
哈希桶定时器适用于系统中存在大量软件定时器、且要求插入时间确定(O(1))的场景。
机制对比表
| 维度 | 哈希桶定时器 | 信号量 | 事件 | 消息队列 | 单链表定时器 |
|---|---|---|---|---|---|
| 传递内容 | 到点执行回调 | 计数同步 | 事件标志位 | 变长数据 | 到点执行回调 |
| 是否携带数据 | 否(回调参数) | 否 | 否 | 是 | 否(回调参数) |
| 定时/延时能力 | 强(O(1) 插入) | 有(超时等待) | 有(超时等待) | 有(超时等待) | 有(O(n) 插入) |
| 回调执行上下文 | 定时器任务 | 唤醒任务 | 唤醒任务 | 唤醒任务 | 定时器任务/ISR |
典型场景
场景一:大量周期性的外设采样上报
- 谁在做什么:系统里可能有几十个周期任务(温度采样每 200ms、湿度每 500ms、电量每 1s……),每个都对应一个定时器。
- 为什么用哈希桶:几十上百个定时器频繁创建/重新激活,若用单链表每次插入要遍历,开销大。
- 实际工作情况:每个任务
os_timer_start()后,哈希桶 O(1) 把定时器放入对应桶;到点后定时器任务回调,周期定时器再重新激活。
场景二:协议栈中的超时重传管理
- 谁在做什么:TCP/HTTP 等协议栈会为每个连接维护"超时重传定时器",连接频繁建立/断开。
- 为什么用哈希桶:定时器频繁创建和销毁,插入性能要求高,O(1) 保证确定性。
- 实际工作情况:连接建立时创建并启动定时器,数据确认后销毁或重置,连接多了也能保持稳定。
选型边界表(何时不该用它)
| 场景 | 该用什么 | 为什么 |
|---|---|---|
| 需要在 ISR 中执行回调 | 单链表硬定时器 | 哈希桶只有软件定时器,回调在定时器任务中执行 |
| 定时器数量很少(≤10 个) | 单链表定时器 | 实现更简单、内存开销更小,O(n) 代价可忽略 |
| 只需要一个简单延时 | os_task_delay() / 任务超时 | 无需专门定时器 |
结合源码的使用注意点
- 哈希桶定时器只有软件定时器,回调在定时器任务(优先级
OS_TIMER_TASK_PRIO = 0,最高优先级)中执行,不能在中断上下文运行,也不应执行耗时过长、会阻塞的操作(会卡住整个定时器任务)。 - 若使用低功耗 tickless(
OS_USING_TICKLESS_LPMGR),必须同时定义OS_HASH_BUCKET_TIMER_SORT(见os_clock.c中#error "In low power mode..."的强制约束)。 - 桶数量由
OS_HASH_BUCKET_TIMER_POWER决定(默认 3 → 8 桶),OS_TIMER_LIST_ENTRIES > 32会编译报错。
二、内部数据结构
2.1 定时器控制块(dummy_timer)字段详解
哈希桶定时器的对象控制块定义在 kernel/include/os_dummy.h,对用户隐藏具体实现,通过 os_timer_dummy_t 别名暴露。
来源: kernel/include/os_dummy.h
struct dummy_timer
{
void (*timeout_func_dummy)(void *timeout_param); /* Timeout function. */
void *parameter_dummy; /* Timeout function's parameter. */
os_tick_t init_ticks_dummy; /* Timer timeout tick. */
os_tick_t round_ticks_dummy; /* Timeout tick remaining. */
#ifdef OS_USING_HASH_BUCKET_TIMER
uint32_t index_dummy;
#endif
os_list_node_t list_dummy;
dummy_timer_active_node_t active_node_dummy;
char name_dummy[OS_NAME_MAX + 1];
};字段含义表:
| 字段 | 类型 | 含义 |
|---|---|---|
timeout_func_dummy | 函数指针 | 超时回调函数,到点时由定时器任务调用 |
parameter_dummy | void * | 回调函数的参数 |
init_ticks_dummy | os_tick_t | 初始超时 tick 数(用户设定的超时周期) |
round_ticks_dummy | os_tick_t | 轮转 tick 数(当前剩余量,实现时按桶内相对值计算) |
index_dummy | uint32_t | 定时器当前所在的桶索引(OS_TIMER_INDEX_INVALID = 0xFFFFFFFF 表示未激活) |
list_dummy | os_list_node_t | 全局定时器链表节点(挂到 gs_os_timer_list,用于管理所有定时器对象) |
active_node_dummy | dummy_timer_active_node_t | 活跃节点,用于挂到哈希桶的活跃链表 |
name_dummy | char[] | 定时器名称 |
提醒:用户不应直接访问这些内部字段,而应通过
os_timer_*系列 API 操作(见4.5.4-定时器API.md)。
2.2 活跃节点(dummy_timer_active_node)字段详解
来源: kernel/include/os_dummy.h
struct dummy_timer_active_node
{
#ifdef OS_USING_HASH_BUCKET_TIMER
os_list_node_t active_list_dummy;
uint8_t info_dummy;
#endif
/* used for compensate periodic timer, if the timeout cb do than init_tick, can't compensate it */
#if OS_TIMER_PERIODIC_COMPENSATE
os_tick_t timeout_tick_dummy;
#endif
uint8_t flag_dummy;
};字段含义表:
| 字段 | 类型 | 含义 |
|---|---|---|
active_list_dummy | os_list_node_t | 活跃链表节点,把定时器挂到某个哈希桶的活跃链表 |
info_dummy | uint8_t | 句柄标志位:bit0 记录"处理标志索引",bit1 表示"链表头节点"(SORT 模式用) |
flag_dummy | uint8_t | 定时器标志(OS_TIMER_FLAG_ACTIVATED、OS_TIMER_FLAG_INITED、OS_TIMER_FLAG_PERIODIC 等) |
timeout_tick_dummy | os_tick_t | 仅 OS_TIMER_PERIODIC_COMPENSATE 时使用(默认 0,不启用) |
2.3 全局变量:桶数组与位掩码
哈希桶定时器模块的全局变量定义在 kernel/source/os_timer_hash.c:
来源: kernel/source/os_timer_hash.c
struct os_timer_active_list_info
{
os_timer_active_node_t active_node;
};
typedef struct os_timer_active_list_info os_timer_active_list_info_t;
OS_ALIGN(OS_ALIGN_SIZE)
static uint8_t gs_timer_task_stack[OS_TIMER_TASK_STACK_SIZE];
static os_task_t gs_os_timer_task;
static os_bool_t gs_os_timer_need_handle = OS_FALSE;
static os_list_node_t gs_os_timer_list = OS_LIST_INIT(gs_os_timer_list);
static OS_DEFINE_SPINLOCK(gs_os_timer_list_lock);
static os_timer_active_list_info_t *gs_os_timer_list_start;
static os_timer_active_list_info_t *gs_os_timer_list_end;
static os_timer_active_list_info_t *gs_os_timer_list_current;
static os_timer_active_list_info_t gs_os_timer_active_list_info[OS_TIMER_LIST_ENTRIES];
static uint32_t gs_os_timer_active_bit_mask;
static uint32_t gs_os_timer_active_bit_number[OS_TIMER_LIST_ENTRIES];
/* Indicates whether the timer is being handled by the timer task. */
static uint8_t gs_os_timer_handle_flag[OS_TIMER_LIST_ENTRIES][2];关键全局变量说明:
| 变量 | 属于谁 | 数量 | 用途 |
|---|---|---|---|
gs_os_timer_active_list_info[OS_TIMER_LIST_ENTRIES] | 模块全局变量 | 系统 1 组(8 个) | 环形哈希桶数组,每个元素是一个桶的活跃链表头 |
gs_os_timer_list_start / end / current | 模块全局变量 | 各 1 个 | 指向桶数组首、尾、当前待处理桶的指针 |
gs_os_timer_active_bit_mask | 模块全局变量 | 1 个 | 位掩码,bit i = 1 表示"第 i 个桶有到期定时器" |
gs_os_timer_active_bit_number[i] | 模块全局变量 | 8 个 | 记录第 i 个桶被标记"需要处理"的次数(用于补偿计算) |
gs_os_timer_handle_flag[i][2] | 模块全局变量 | 8×2 | 表示第 i 个桶是否正被定时器任务处理(双缓冲) |
gs_os_timer_task / gs_timer_task_stack | 模块全局变量 | 各 1 个 | 定时器任务的控制块与栈 |
2.4 关键宏定义与基准数据
来源: kernel/source/os_timer_hash.c(宏定义;各宏的默认值见 5.1 配置选项)
#define OS_TIMER_TO_ID(timer) ((os_timer_id)(timer))
#define OS_TIMER_FROM_ID(tid) ((os_timer_t *)(tid))
#define OS_TIMER_LOW_MASK ((1U << OS_HASH_BUCKET_TIMER_POWER) - 1U)
#define OS_TIMER_MOD(tick) ((tick)&OS_TIMER_LOW_MASK)
#define OS_TIMER_ROUND(tick) ((tick) & (~OS_TIMER_LOW_MASK))
#define OS_TIMER_LIST_ENTRIES (1U << OS_HASH_BUCKET_TIMER_POWER)
#define OS_TIMER_IS_HEAD(node) ((node)->info & 2U)
#define OS_TIMER_GET_HANDLE_FLAG_INDEX(node) ((node)->info & 1U)核心宏含义:
| 宏 | 作用 |
|---|---|
OS_TIMER_MOD(tick) | 取 tick 的低 POWER 位,作为桶索引(相当于 tick % OS_TIMER_LIST_ENTRIES,位运算更快) |
OS_TIMER_ROUND(tick) | 取 tick 的高位,得到"整轮(每轮 8 个 tick)"部分,即 round_ticks 的基值 |
OS_TIMER_LIST_ENTRIES | 桶数量 = 1 << POWER |
OS_TIMER_IS_HEAD(node) | 判断节点是否为链表头(读 info 的 bit1) |
OS_TIMER_GET_HANDLE_FLAG_INDEX(node) | 读节点句柄标志索引(info 的 bit0) |
全局统一基准数据(本节所有演算沿用此套数据)
本节假设配置如下(与 OneOS 默认配置一致):
| 配置项 | 值 | 说明 |
|---|---|---|
OS_HASH_BUCKET_TIMER_POWER | 3 | 桶数组大小的对数 |
OS_TIMER_LIST_ENTRIES | 1 << 3 = 8 | 共 8 个桶,索引 0~7 |
OS_TIMER_LOW_MASK | (1<<3)-1 = 0x7 | 掩码,低 3 位为 1 |
OS_TIMER_MOD(tick) | tick & 0x7 | 桶索引 ∈ [0,7] |
OS_TIMER_ROUND(tick) | tick & ~0x7 | 8 的倍数 |
本节的桶索引、
round_ticks演算全部基于上述数据,后续分配/处理例子复用同一套数字,前后一致。
位运算数值演算表
以 tick = 21 为例:
| 计算项 | 代入计算 | 结果 |
|---|---|---|
| 二进制表示 | 21 = 0b00010101 | 0x15 |
OS_TIMER_MOD(21) | 0x15 & 0x7 = 0b00010101 & 0b00000111 | 0b00000101 = 5 |
OS_TIMER_ROUND(21) | 0x15 & ~0x7 = 0b00010101 & 0b11111000 | 0b00010000 = 16 |
含义:定时器超时 21 个 tick,落在桶 5;round_ticks 基值为 16(即要走完整 2 轮 8 个 tick,剩 5 个 tick 落在桶 5)。
三、哈希桶工作原理
3.1 哈希桶整体结构
定时器桶数组是一个环形缓冲区,共 OS_TIMER_LIST_ENTRIES(此处 8)个桶。gs_os_timer_list_current 每过一个 tick 前移一步(循环),指向"当前待检查"的桶。
gs_os_timer_list_start gs_os_timer_list_end
│ │
▼ ▼
┌────────┬────────┬────────┬────────┬────────┬────────┬────────┬────────┐
│ 桶0 │ 桶1 │ 桶2 │ 桶3 │ 桶4 │ 桶5 │ 桶6 │ 桶7 │
│ ┌─┐┌─┐ │ ┌─┐ │ ┌─┐┌─┐│ ┌─┐ │ │ ┌─┐ │ │ ┌─┐┌─┐ │
│ │T││T│ │ │T│ │ │T││T││ │T│ │ │ │T│ │ │ │T││T│ │
│ └─┘└─┘ │ └─┘ │ └─┘└─┘│ └─┘ │ │ └─┘ │ │ └─┘└─┘ │
└────────┴────────┴────────┴────────┴────────┴────────┴────────┴────────┘
▲ ▲
└────────────────── 环形:桶7 的下一个回到 桶0 ──────────────────────────┘
↑
gs_os_timer_list_current(当前 tick 要检查的桶)核心理解:
- 每个桶是一个双向链表,存放"剩余轮次 + 桶索引"落在该桶的定时器。
gs_os_timer_list_current与gs_os_timer_list_start的偏移量,就是"当前已经走过的 tick 数对 8 取模"。- 位掩码
gs_os_timer_active_bit_mask的 bit i = 1,表示"第 i 个桶里存在到期定时器",定时器任务据此只处理有到期的桶,无需遍历全部 8 个桶。
3.2 插入操作(O(1))
插入(激活)一个定时器时,核心逻辑在 _k_timer_activate():
来源: kernel/source/os_timer_hash.c
static void _k_timer_activate(os_timer_t *timer)
{
os_timer_active_list_info_t *current;
os_timer_active_list_info_t *start;
uint32_t dec_ticks;
uint32_t index;
uint8_t handle_flag_index;
OS_ASSERT(timer->init_ticks > 0);
if (os_list_empty(&timer->active_node.active_list))
{
/* Timer is inactive,activate it. */
_k_timer_get_active_list(¤t, &start, OS_NULL);
dec_ticks = timer->init_ticks - 1;
/*
* Calculate index of acitve list array to insert.
* Do not use "index = OS_TIMER_MOD((current - start) + dec_ticks);",
* because it maybe overflow.
*/
index = OS_TIMER_MOD(dec_ticks);
index = OS_TIMER_MOD((current - start) + index);
timer->round_ticks = OS_TIMER_ROUND(dec_ticks);
#ifdef OS_HASH_BUCKET_TIMER_SORT
_k_timer_add_to_sort_list(&start[index].active_node, timer);
#else
os_list_add_tail(&start[index].active_node.active_list, &timer->active_node.active_list);
#endif
timer->active_node.flag |= OS_TIMER_FLAG_ACTIVATED;
timer->index = index;
handle_flag_index = OS_TIMER_GET_HANDLE_FLAG_INDEX(&start[index].active_node);
_k_timer_set_handle_flag_index(&timer->active_node, handle_flag_index);
}
}分步详解
第一步:参数校验(条件 timer->init_ticks == 0 时触发断言)
OS_ASSERT(timer->init_ticks > 0);- 作用:保证超时 tick 数至少为 1。若为 0,定时器无法进入任何桶(
dec_ticks = -1,位运算结果异常),故直接断言。 - 常见错误:用户调用
os_timer_create(..., timeout=0, ...)且未检查返回值。
第二步:判断是否已激活(条件 os_list_empty(&timer->active_node.active_list) 为空)
if (os_list_empty(&timer->active_node.active_list))
{
/* Timer is inactive,activate it. */
_k_timer_get_active_list(¤t, &start, OS_NULL);
dec_ticks = timer->init_ticks - 1;- 进入条件:定时器的活跃链表节点未挂到任何桶(处于未激活态)。若已激活,则直接跳过整个激活流程(不做任何事)。
_k_timer_get_active_list()取出当前桶指针current和首桶指针start。dec_ticks = init_ticks - 1:减 1 是关键。因为定时器启动的当个 tick 也算一个 tick,实际剩余轮数计算要减 1(见下方演算)。
第三步:计算桶索引并设置轮转值(无条件执行)
index = OS_TIMER_MOD(dec_ticks);
index = OS_TIMER_MOD((current - start) + index);
timer->round_ticks = OS_TIMER_ROUND(dec_ticks);- 分两步计算
index,不用OS_TIMER_MOD((current - start) + dec_ticks),是因为(current - start)与dec_ticks之和可能溢出uint32_t,拆成两次位与更安全。 timer->round_ticks记录"整轮数 × 8",即要跨过的完整 8-tick 轮次。
数值演算表
设当前 gs_os_timer_list_current 指向桶 2(current - start = 2),新定时器 init_ticks = 21:
| 计算项 | 代入计算 | 结果 |
|---|---|---|
dec_ticks | 21 - 1 | 20 |
OS_TIMER_MOD(20) | 20 & 0x7 = 0b10100 & 0b00111 | 0b100 = 4 |
(current - start) + 4 | 2 + 4 | 6 |
OS_TIMER_MOD(6) | 6 & 0x7 | 6(目标桶) |
OS_TIMER_ROUND(20) | 20 & ~0x7 = 0b10100 & 0b11000 | 16(round_ticks) |
下面把这条"含义"拆成四层详细解释。
(a)先建立直觉:桶 = "剩余 tick 个位",round_ticks = "已走过的整轮"
把 8 个桶想象成时钟表盘上的 8 个刻度(0~7)。一个定时器要等 init_ticks 个 tick 才触发,它可以拆成两个部分:
- 能整除 8 的部分(整轮):每过 8 个 tick,
gs_os_timer_list_current就会在表盘上完整绕一圈回到起点。这些整轮不必在桶里"排队",因为反正要等满整轮。 - 余数部分(个位,0~7):整轮走完后,还差几个 tick 才到超时。这几个 tick 恰好对应表盘上的前进格数,也就是桶的位置。
所以一个定时器在桶数组里的位置 = (当前桶索引 + 余数) % 8,而 round_ticks 记录的是"要跨过多少个完整整轮 × 8"。
(b)两层定位:为什么 init_ticks = 21 会拆成"桶 6 + round_ticks 16"
以本例数据代入:当前 gs_os_timer_list_current 在桶 2,新定时器 init_ticks = 21。
关键在源码先把 init_ticks 减 1(dec_ticks = 20)。为什么减 1:因为"启动那一刻"就占用了当前这 1 个 tick(当前 tick 即将过去的瞬间启动定时器,等下一个 tick 中断到来时,时间已推进了 1)。若按 21 来算,会多等 1 个 tick。因此实际要"从当前桶往后数"的 tick 数是 20。
把 20 拆成"整轮 + 余数":
| 成分 | 值 | 含义 |
|---|---|---|
| 整轮(round_ticks) | 16 = 2 × 8 | 要跨过完整 2 轮 |
| 余数(桶内位移) | 4 = 20 % 8 | 整轮之外,还要在表盘上多走 4 格 |
于是桶索引 = (当前桶 2 + 余数 4) % 8 = 6。这就是定时器落在桶 6 的原因。
(c)逐 tick 时间线:从启动到超时,指针如何走完 20 个 tick
为直观验证,把定时器启动后 gs_os_timer_list_current 每一步走到哪个桶列出来(当前从桶 2 开始,每个 tick 中断指针前移一步):
| 已过 tick | 指针指向桶 | 备注 |
|---|---|---|
| 启动瞬间 | 2 | 定时器放入桶 6 |
| 1 | 3 | 跨出 1 格 |
| 2 | 4 | |
| 3 | 5 | |
| 4 | 6 | 到达桶 6,但 round_ticks=16 还未走完 |
| 5 | 7 | |
| 6 | 0 | 第 1 轮回绕 |
| 7 | 1 | |
| 8 | 2 | |
| 9 | 3 | |
| 10 | 4 | |
| 11 | 5 | |
| 12 | 6 | 再次经过桶 6,round_ticks 剩 8 |
| 13 | 7 | 第 2 轮回绕 |
| 14 | 0 | |
| 15 | 1 | |
| 16 | 2 | |
| 17 | 3 | |
| 18 | 4 | |
| 19 | 5 | |
| 20 | 6 | 到达桶 6,round_ticks 减到 0,定时器到期 |
可见定时器真正触发,是第 20 个 tick 中断时指针第二次扫到桶 6 的那一次。前一次(tick 4)指针经过桶 6 时,
round_ticks还有 16(要走 2 整轮),于是k_timer_need_handle()用round_ticks -= 8把它"压过一轮"、不唤醒;直到 round_ticks 耗尽,才真正触发回调。这就是round_ticks存在的意义——避免定时器在被放入桶后,仅仅因为指针路过就被误判为到期。
(d)为什么"经过桶 6"两次,第一次不触发?
这正是哈希桶定时器"一层哈希、一层轮次"的精髓:
- 哈希(桶索引)只负责粗定位:告诉系统"它大致在表盘哪个位置附近"。
- 轮次(round_ticks)负责精确定时:告诉系统"还要绕几整圈才真正到期"。
每次指针扫过一个桶,k_timer_need_handle() 只把该桶里定时器的 round_ticks 减掉 8(整轮),而不立刻触发。只有当 round_ticks 减到 0 的那个 tick,才判定到期并唤醒定时器任务。这样:
- 插入仍 O(1)(只算哈希、挂链表);
- 长定时器(超过 8 tick)也不会被提前误触;
- 每个 tick 中断最多做一次
round_ticks -= 8和一次判断,开销恒定。
小结:本例中,定时器 init_ticks=21 被表示为"桶 6 + round_ticks 16",含义是"当前指针在桶 2,需要先跨 2 整轮(16 tick)让 round_ticks 耗尽,再在表盘上前进 4 格到达桶 6 触发"。
第四步:挂入桶链表并标记激活(无条件执行)
#ifdef OS_HASH_BUCKET_TIMER_SORT
_k_timer_add_to_sort_list(&start[index].active_node, timer);
#else
os_list_add_tail(&start[index].active_node.active_list, &timer->active_node.active_list);
#endif
timer->active_node.flag |= OS_TIMER_FLAG_ACTIVATED;
timer->index = index;
handle_flag_index = OS_TIMER_GET_HANDLE_FLAG_INDEX(&start[index].active_node);
_k_timer_set_handle_flag_index(&timer->active_node, handle_flag_index);- 非 SORT 模式:直接
os_list_add_tail挂到目标桶尾部,O(1)。 - SORT 模式:调用
_k_timer_add_to_sort_list按round_ticks排序插入(用于 tickless)。 - 置
OS_TIMER_FLAG_ACTIVATED标志,记录timer->index = index(后续停表/查询剩余时间要用)。 - 把桶头节点的"句柄标志索引"复制给定时器(配合
gs_os_timer_handle_flag双缓冲使用,见 3.3)。
插入操作 O(1) 的本质
整个插入过程只做了:1 次减一、2 次位与运算、1 次链表尾部插入。不涉及任何遍历,与系统中定时器总数无关,因此是 O(1)。
正因如此,哈希桶定时器才被设计成"软件定时器"——每次 tick 只把"当前桶"里到期的定时器交给定时器任务处理,而插入永远 O(1)。
四、tick 处理流程
4.1 每个 tick 中断的分工
每次系统时钟 tick 中断时,os_tick_increase() 会调用哈希桶定时器的两个入口函数:
来源: kernel/source/os_clock.c
#ifdef OS_USING_HASH_BUCKET_TIMER
/* Is need timer handle.*/
if (k_timer_need_handle())
{
is_sched = OS_TRUE;
}
/* Move gs_os_timer_list_current forward one step. */
k_move_timer_list_one_step();
#endif分工总结:
| 函数 | 时机 | 作用 |
|---|---|---|
k_timer_need_handle() | 检查当前桶是否有到期定时器 | 有则置位掩码、唤醒定时器任务,返回 OS_TRUE |
k_move_timer_list_one_step() | 检查完当前桶后 | 把 gs_os_timer_list_current 前移一步(到下一个桶,越界回绕到桶 0) |
"前移一步(循环)"是什么意思?
哈希桶定时器把时间进程映射到了"指针在环形桶数组上的走动"上:每过一个 tick,指针就前进一个桶。k_move_timer_list_one_step() 就是驱动指针前进的动作。
- 普通情况:指针从当前桶移到下一个桶,如
gs_os_timer_list_current从桶 5 指向桶 6。 - 越界回绕:当指针在最后一个桶(桶 7)时,再"前移一步"会超出数组末尾,于是回绕到第一个桶(桶 0),形成环形——因为 8 个桶要无限循环地表示不断前进的时间。
- 为什么必须和
k_timer_need_handle()一前一后调用:先检查"当前这个桶"里有没有到期的定时器(k_timer_need_handle),检查完再把指针移到下一个桶(k_move_timer_list_one_step),为下一个 tick 做准备。顺序颠倒会导致漏检或重复检查。
具体实现(含"越界判断 + 回绕"两行代码与环形演算表)见下方 4.3
k_move_timer_list_one_step()分步详解。
4.2 k_timer_need_handle() —— 检查当前桶是否需处理
来源: kernel/source/os_timer_hash.c
os_bool_t k_timer_need_handle(void)
{
os_bool_t need_handle;
int32_t current_list_idx;
/*
* If the list pointed by gs_os_timer_list_current is not expty,set flag gs_os_timer_need_handle
* and resume timer task to handle the timer list pointed by gs_os_timer_list_current.
*/
if (!os_list_empty(&gs_os_timer_list_current->active_node.active_list))
{
#ifdef OS_HASH_BUCKET_TIMER_SORT
os_timer_t *timer;
timer =
os_list_entry(gs_os_timer_list_current->active_node.active_list.next, os_timer_t, active_node.active_list);
if (timer->round_ticks >= OS_TIMER_LIST_ENTRIES)
{
/* There is no timer timeout, do not need wake up timer task. */
timer->round_ticks -= OS_TIMER_LIST_ENTRIES;
}
else
#endif
{
/* List is not empty,set flag and set the list bit mask.*/
gs_os_timer_need_handle = OS_TRUE;
current_list_idx = gs_os_timer_list_current - gs_os_timer_list_start;
OS_BIT_SET(gs_os_timer_active_bit_mask, current_list_idx);
gs_os_timer_active_bit_number[current_list_idx]++;
}
}
need_handle = OS_FALSE;
/* List pointed by gs_os_timer_list_current is not empty,resume timer task to handle the list. */
if (gs_os_timer_need_handle && (OS_TASK_STATE_SUSPEND == gs_os_timer_task.state))
{
gs_os_timer_task.state &= ~OS_TASK_STATE_SUSPEND;
gs_os_timer_task.state |= OS_TASK_STATE_READY;
k_readyq_put(&gs_os_timer_task);
need_handle = OS_TRUE;
}
return need_handle;
}分步详解
第一步:检查当前桶是否为空(条件 os_list_empty(...) 非空)
if (!os_list_empty(&gs_os_timer_list_current->active_node.active_list))
{
#ifdef OS_HASH_BUCKET_TIMER_SORT
os_timer_t *timer;
timer =
os_list_entry(gs_os_timer_list_current->active_node.active_list.next, os_timer_t, active_node.active_list);
if (timer->round_ticks >= OS_TIMER_LIST_ENTRIES)
{
/* There is no timer timeout, do not need wake up timer task. */
timer->round_ticks -= OS_TIMER_LIST_ENTRIES;
}
else
#endif
{
/* List is not empty,set flag and set the list bit mask.*/
gs_os_timer_need_handle = OS_TRUE;
current_list_idx = gs_os_timer_list_current - gs_os_timer_list_start;
OS_BIT_SET(gs_os_timer_active_bit_mask, current_list_idx);
gs_os_timer_active_bit_number[current_list_idx]++;
}
}- 进入条件:当前桶非空。
- SORT 模式子分支:取该桶链表头第一个定时器,若其
round_ticks >= 8,说明它还要跨完整一轮才到期,此时不用唤醒定时器任务,只需把round_ticks减 8(表示又走完一轮)。 - 非 SORT / 未跨整轮子分支:说明当前桶有到期定时器。该分支做三件事:置
gs_os_timer_need_handle = OS_TRUE、OS_BIT_SET(gs_os_timer_active_bit_mask, current_list_idx)、gs_os_timer_active_bit_number[current_list_idx]++。下面逐项拆解。
位掩码:OS_BIT_SET(gs_os_timer_active_bit_mask, current_list_idx) 在做什么
OS_BIT_SET(value, bit) 宏定义(kernel/include/os_stddef.h):
#define OS_BIT_SET(value, bit) ((value) |= (1 << (bit)))它的作用是把 value 的第 bit 位(从 0 开始)置为 1,其余位不变。gs_os_timer_active_bit_mask 是一个 uint32_t,它的第 i 位 = 1 表示"第 i 个桶有到期定时器需要定时器任务处理"。
以当前指针在桶 3(current_list_idx = 3)为例,逐位演算 OS_BIT_SET:
| 计算项 | 代入计算 | 结果 |
|---|---|---|
current_list_idx | 3 | 要置位的位号 |
1 << 3 | 0b00000001 << 3 | 0b00001000 = 0x08 |
原 bit_mask(假设桶 1 已被标记) | 0b00000010 | 当前状态 |
bit_mask |= 0x08 | 0b00000010 | 0b00001000 | 0b00001010 |
| 结果 | bit1 和 bit3 都为 1,其余位不变 |
上式中
0b00001010=0x0A,表示桶 1 和桶 3 都有到期定时器。|=(按位或)的精妙在于:它只"置 1"而不会清掉其他已置位的桶——如果之前桶 1 已被标记,这次再标记桶 3,两个标志都能保留。
为什么位掩码能加速处理:定时器任务醒来后,只需用 os_ffs(bit_mask) 一次算出"最小的待处理桶",处理完 OS_BIT_CLR 清掉该位再取下一位,无需遍历全部 8 个桶(见 4.4 节)。
补偿计数:gs_os_timer_active_bit_number[current_list_idx]++ 在做什么
gs_os_timer_active_bit_number[current_list_idx]++;gs_os_timer_active_bit_number[i]是一个计数器,记录"第 i 个桶在"这次被标记为到期、需要处理"的累计次数"。- 为什么需要它:定时器任务每次被唤醒后,会一次性把
bit_mask里所有置位的桶都处理掉(见 4.4 的while循环)。但在定时器任务尚未跑起来、或正在处理别的桶的这段时间里,gs_os_timer_list_current可能已经又走了好几轮,再次扫到同一个桶,于是bit_number[i]就会累加。
补偿计数的数值含义
每个桶每被"扫到并标记"一次,就代表时间又前进了一整轮(8 个 tick)。bit_number[i] 记录了定时器任务"欠"第 i 个桶多少个整轮。以桶 3 为例:
| 情形 | bit_number[3] | 含义 |
|---|---|---|
| 第 1 次扫到桶 3 | 0 + 1 = 1 | 标记了 1 次,欠 0 个整轮 |
| 定时器任务还没处理,指针又扫到桶 3 | 1 + 1 = 2 | 欠 1 个整轮(8 tick) |
| 再次漏过 | 2 + 1 = 3 | 欠 2 个整轮(16 tick) |
补偿值如何被使用(与 4.4 的 compensate_tick 呼应)
定时器任务真正处理桶 3 时,会计算:
compensate_tick = (gs_os_timer_active_bit_number[current_list_idx] - 1) * (OS_TIMER_LIST_ENTRIES);bit_number[3] = 3时,compensate_tick = (3-1) × 8 = 16,表示"这段时间欠了 2 整轮(16 tick)"。- 对周期性定时器,这 16 tick 会被减掉(
round_ticks -= compensate_tick),从而修正因定时器任务调度延迟造成的周期偏差(见 4.4 第三步)。
一句话总结该子分支:有到期定时器 → 置
need_handle标志 → 用位掩码"登记"是哪个桶 → 用bit_number"记账"欠了几轮,三者配合让定时器任务能精准、高效地补处理。
第二步:唤醒定时器任务(条件 gs_os_timer_need_handle 且定时器任务挂起)
need_handle = OS_FALSE;
/* List pointed by gs_os_timer_list_current is not empty,resume timer task to handle the list. */
if (gs_os_timer_need_handle && (OS_TASK_STATE_SUSPEND == gs_os_timer_task.state))
{
gs_os_timer_task.state &= ~OS_TASK_STATE_SUSPEND;
gs_os_timer_task.state |= OS_TASK_STATE_READY;
k_readyq_put(&gs_os_timer_task);
need_handle = OS_TRUE;
}
return need_handle;- 进入条件:
gs_os_timer_need_handle == OS_TRUE(有到期定时器)且定时器任务当前处于挂起态。 - 把定时器任务从 SUSPEND 置为 READY,放回就绪队列。
- 返回
OS_TRUE通知os_tick_increase()需要调度(is_sched = OS_TRUE)。
运行实例
下面用连续几个 tick 的序列,演示 k_timer_need_handle() 的三种走向。沿用本节基准数据(8 个桶,非 SORT 模式)。
场景设定:当前指针 gs_os_timer_list_current 在桶 3。桶 3 的活跃链表里有定时器 T1,其 round_ticks = 0(已耗尽整轮,本次扫到即到期);桶 4、桶 5 为空;定时器任务当前处于 SUSPEND。
场景一:当前桶为空(桶 4)→ 返回 OS_FALSE
当指针走到桶 4 时,os_list_empty(&gs_os_timer_list_current->active_node.active_list) 为真(桶 4 是空链表),整个 if 块被跳过:
| 检查项 | 值 | 结论 |
|---|---|---|
| 桶 4 是否为空 | 是 | 不置位掩码 |
gs_os_timer_need_handle | 仍为 OS_FALSE | 不唤醒 |
| 返回值 | OS_FALSE |
实际效果:is_sched 不变,定时器任务继续挂起,指针随后前移到桶 5。这个 tick 没有任何定时器到期。
场景二:当前桶非空且定时器到期(桶 3,round_ticks = 0)→ 返回 OS_TRUE
指针走到桶 3 时,桶 3 非空,走 else 分支(非 SORT 或 round_ticks < 8):
| 检查项 | 值 | 结论 |
|---|---|---|
| 桶 3 是否为空 | 否 | 进入分支 |
current_list_idx | 3 - 0 = 3(current - start) | 目标桶索引 |
| 置位掩码 | OS_BIT_SET(bit_mask, 3),bit_mask = 0b00001000 | 标记桶 3 |
bit_number[3] | 0 + 1 = 1 | 记录被标记次数 |
gs_os_timer_need_handle | OS_TRUE | 有待处理 |
| 定时器任务状态 | SUSPEND | 唤醒 |
| 返回值 | OS_TRUE |
实际效果:os_tick_increase() 收到 OS_TRUE,置 is_sched = OS_TRUE,稍后调度到定时器任务。定时器任务从位掩码 0b00001000 用 os_ffs 定位到桶 3,执行 T1 的回调。这正是"到期定时器"被发现的时刻。
场景三:当前桶非空但定时器未走完整轮(SORT 模式,round_ticks >= 8)→ 仅减 8、返回 OS_FALSE
若开启了 OS_HASH_BUCKET_TIMER_SORT(桶内定时器排序),桶 3 里的首定时器 round_ticks = 16(还要跨 2 整轮)。此时走 SORT 子分支:
| 检查项 | 值 | 结论 |
|---|---|---|
| 桶 3 是否为空 | 否 | 进入分支 |
首定时器 round_ticks | 16,16 >= 8 | 命中 SORT 分支 |
round_ticks 处理 | 16 - 8 = 8 | 又走完一轮,但不触发 |
| 置位掩码 | 否 | 不标记 |
gs_os_timer_need_handle | 仍 OS_FALSE | 不唤醒 |
| 返回值 | OS_FALSE |
实际效果:定时器还要等,round_ticks 从 16 减到 8。指针继续前移。等到下次 round_ticks 减到 0 的那个 tick,才会像场景二一样触发。这就是"未走完整轮不误触发"的体现。
场景速览对比
| 场景 | 当前桶 | 首定时器 round_ticks | 是否置位掩码 | 是否唤醒 | 返回值 |
|---|---|---|---|---|---|
| 一(空桶) | 桶 4 | 无定时器 | 否 | 否 | OS_FALSE |
| 二(到期) | 桶 3 | 0 | 是 | 是 | OS_TRUE |
| 三(SORT 未到期) | 桶 3 | 16 ≥ 8 | 否 | 否 | OS_FALSE |
条件走向总结
k_timer_need_handle() 条件走向总结:
├── 当前桶为空(os_list_empty == 非空不成立)
│ └── 不置位掩码、不唤醒 → 返回 OS_FALSE
└── 当前桶非空
├── SORT 模式且首定时器 round_ticks >= 8
│ └── 仅 round_ticks -= 8,不置位掩码 → 返回 OS_FALSE(仍不唤醒)
└── 非 SORT 或 round_ticks < 8
└── 置位掩码、bit_number++、gs_os_timer_need_handle = OS_TRUE
├── 若定时器任务处于 SUSPEND
│ └── 唤醒定时器任务、放入就绪队列 → 返回 OS_TRUE
└── 若定时器任务未挂起(已在处理中)
└── 不重复唤醒 → 返回 OS_FALSE4.3 k_move_timer_list_one_step() —— 前移当前桶指针
来源: kernel/source/os_timer_hash.c
void k_move_timer_list_one_step(void)
{
if (++gs_os_timer_list_current > gs_os_timer_list_end)
{
gs_os_timer_list_current = gs_os_timer_list_start;
}
}分步详解
第一步:指针自增并判断越界(无条件执行)
if (++gs_os_timer_list_current > gs_os_timer_list_end)
{
gs_os_timer_list_current = gs_os_timer_list_start;
}- 先把
gs_os_timer_list_current自增 1。 - 进入条件:自增后超过桶数组末尾
gs_os_timer_list_end(即从桶 7 走到桶 8,越界)。 - 若越界则回绕到首桶
gs_os_timer_list_start(桶 0),形成环形。
为什么每个 tick 都要调用它:因为哈希桶定时器的每个桶对应"一个 tick"。指针每走一步,就代表时间前进 1 个 tick。当前桶里那些"刚好走到"的定时器就到期了。
数值演算表(环形前移)
设当前 gs_os_timer_list_current 依次指向各桶:
| 当前指向 | 执行后指向 | 是否回绕 |
|---|---|---|
| 桶 0 | 桶 1 | 否 |
| 桶 5 | 桶 6 | 否 |
| 桶 7 | 桶 0(回绕) | 是 |
4.4 定时器任务:如何按位掩码处理到期定时器
k_timer_module_init() 会创建并启动优先级为 OS_TIMER_TASK_PRIO = 0(最高)的定时器任务,入口为 _k_timer_task_entry()。当 k_timer_need_handle() 唤醒它后,它进入处理循环。
来源: kernel/source/os_timer_hash.c
while (0 != gs_os_timer_active_bit_mask)
{
/* Get the timer's current list idx,which is needed to handle. */
current_list_idx = os_ffs(gs_os_timer_active_bit_mask) - 1;
iter_current = &gs_os_timer_active_list_info[current_list_idx];
/* Move the current active list pointed to expire list. */
os_list_add(&iter_current->active_node.active_list, &expired_timer_head.active_list);
os_list_del(&iter_current->active_node.active_list);
_k_timer_update_handle_flag((uint32_t)current_list_idx);
/* Clear timer's list bit mask. */
OS_BIT_CLR(gs_os_timer_active_bit_mask, current_list_idx);
/* Handle timers of expire list one by one. */
current_pos = os_list_first(&expired_timer_head.active_list);
while (OS_NULL != current_pos)
{
current_timer = os_list_entry(current_pos, os_timer_t, active_node.active_list);
/* Delete current timer from expire list. */
#ifdef OS_HASH_BUCKET_TIMER_SORT
_k_timer_del_from_sort_list(current_timer);
#else
os_list_del(current_pos);
#endif
os_list_init(&restart_timer_head.active_list);
OS_ASSERT(0 == OS_TIMER_MOD(current_timer->round_ticks));
/*
* Get timeout funciton and parameter. If the timer does not expire or timer is periodic,
* add the timer to restart list in order to activate it again after call timeout funcion.
*/
/* Tick needed to compensate */
compensate_tick = (gs_os_timer_active_bit_number[current_list_idx] - 1) * (OS_TIMER_LIST_ENTRIES);
if (current_timer->round_ticks <= compensate_tick)
{
/* Timer expire. */
current_timer->round_ticks = 0;
if ((current_timer->active_node.flag & OS_TIMER_FLAG_PERIODIC) != 0)
{
/* Add current timer to restart timer list in order to activate it again afterwards. */
os_list_add(&restart_timer_head.active_list, current_pos);
}
/* Get timeout function and parameter. The timeout funciton will be called afterwards. */
timeout_func = current_timer->timeout_func;
timeout_param = current_timer->parameter;
}
else
{
/* Timer do not expire */
current_timer->round_ticks -= compensate_tick;
/* Add current timer to restart timer list. */
os_list_add(&restart_timer_head.active_list, current_pos);
timeout_func = OS_NULL;
}
/* The timer is oneshot, deactivate it. */
if (os_list_empty(&restart_timer_head.active_list))
{
_k_timer_deactivate(current_timer);
}
OS_KERNEL_EXIT();
/* Call timeout function if it is expired. */
if (OS_NULL != timeout_func)
{
timeout_func(timeout_param);
}
OS_KERNEL_ENTER();
/* If the timer does not expire or timer is periodic, add timer to active list. */
restart_pos = os_list_first(&restart_timer_head.active_list);
if (OS_NULL != restart_pos)
{
/* Remove timer from restart list. */
os_list_del(restart_pos);
restart_timer = os_list_entry(restart_pos, os_timer_t, active_node.active_list);
if (0 == restart_timer->round_ticks)
{
/* Timer expire, reactivate timer. */
_k_timer_activate(restart_timer);
}
else
{
/* Timer does not expire, round_ticks subtract active list entries. */
restart_timer->round_ticks -= OS_TIMER_LIST_ENTRIES;
/*
* There is no need to recalculate active list's index where the
* unexpired timer adds. Just add timer to its original active list.
*/
#ifdef OS_HASH_BUCKET_TIMER_SORT
_k_timer_add_to_sort_list(&gs_os_timer_list_start[restart_timer->index].active_node,
restart_timer);
#else
os_list_add_tail(&(gs_os_timer_list_start[restart_timer->index].active_node.active_list),
restart_pos);
#endif
handle_flag_index = OS_TIMER_GET_HANDLE_FLAG_INDEX(
&gs_os_timer_list_start[restart_timer->index].active_node);
_k_timer_set_handle_flag_index(&restart_timer->active_node, handle_flag_index);
}
}
current_pos = os_list_first(&expired_timer_head.active_list);
}
gs_os_timer_active_bit_number[current_list_idx] = 0;
}说明:上述
while (0 != gs_os_timer_active_bit_mask)循环体完整取自源码、无省略,覆盖"到期处理 + restart 重激活"两部分;其中 restart 重激活细节属定时器生命周期管理,也在4.5.4-定时器API.md中讲解。
分步详解
第一步:从位掩码取出要处理的桶(条件 gs_os_timer_active_bit_mask != 0)
while (0 != gs_os_timer_active_bit_mask)
{
/* Get the timer's current list idx,which is needed to handle. */
current_list_idx = os_ffs(gs_os_timer_active_bit_mask) - 1;- 进入条件:位掩码非 0,即存在"有到期定时器的桶"。
os_ffs()返回第一个置 1 位的位置(从 1 开始计数),减 1 得 0 基索引。这就是"最小的待处理桶索引"。
数值演算表
设 gs_os_timer_active_bit_mask = 0b01010010(桶 1、4、6 有到期定时器):
| 计算项 | 代入计算 | 结果 |
|---|---|---|
| 掩码二进制 | 0b01010010 | bit1、bit4、bit6 为 1 |
os_ffs(0x52) | 第一个 1 在 bit1,位置从 1 起算为 2 | 2 |
current_list_idx | 2 - 1 | 1(桶 1) |
即先处理桶 1,清掉该位后继续循环,再处理桶 4、桶 6。
第二步:把该桶移到"到期链表"并更新句柄标志(无条件执行)
iter_current = &gs_os_timer_active_list_info[current_list_idx];
/* Move the current active list pointed to expire list. */
os_list_add(&iter_current->active_node.active_list, &expired_timer_head.active_list);
os_list_del(&iter_current->active_node.active_list);
_k_timer_update_handle_flag((uint32_t)current_list_idx);
/* Clear timer's list bit mask. */
OS_BIT_CLR(gs_os_timer_active_bit_mask, current_list_idx);- 把整个桶的链表整体摘下挂到
expired_timer_head(到期链表),避免逐个移动节点。 _k_timer_update_handle_flag()翻转该桶的句柄标志(双缓冲),标记"此桶正被定时器任务处理"。- 清除掩码对应位,让循环能处理下一个有到期的桶。
第三步:逐个处理到期定时器(条件 current_pos != OS_NULL)
current_pos = os_list_first(&expired_timer_head.active_list);
while (OS_NULL != current_pos)
{
current_timer = os_list_entry(current_pos, os_timer_t, active_node.active_list);
/* Delete current timer from expire list. */
#ifdef OS_HASH_BUCKET_TIMER_SORT
_k_timer_del_from_sort_list(current_timer);
#else
os_list_del(current_pos);
#endif
os_list_init(&restart_timer_head.active_list);
OS_ASSERT(0 == OS_TIMER_MOD(current_timer->round_ticks));
/*
* Get timeout funciton and parameter. If the timer does not expire or timer is periodic,
* add the timer to restart list in order to activate it again after call timeout funcion.
*/
/* Tick needed to compensate */
compensate_tick = (gs_os_timer_active_bit_number[current_list_idx] - 1) * (OS_TIMER_LIST_ENTRIES);
if (current_timer->round_ticks <= compensate_tick)
{
/* Timer expire. */
current_timer->round_ticks = 0;
if ((current_timer->active_node.flag & OS_TIMER_FLAG_PERIODIC) != 0)
{
/* Add current timer to restart timer list in order to activate it again afterwards. */
os_list_add(&restart_timer_head.active_list, current_pos);
}
/* Get timeout function and parameter. The timeout funciton will be called afterwards. */
timeout_func = current_timer->timeout_func;
timeout_param = current_timer->parameter;
}
else
{
/* Timer do not expire */
current_timer->round_ticks -= compensate_tick;
/* Add current timer to restart timer list. */
os_list_add(&restart_timer_head.active_list, current_pos);
timeout_func = OS_NULL;
}- 从到期链表依次取出定时器。
OS_ASSERT(0 == OS_TIMER_MOD(current_timer->round_ticks)):round_ticks必为 8 的倍数(因为到达桶时只可能跨完整轮),若否则说明状态损坏。- 补偿计算:
compensate_tick = (bit_number[idx]-1) * 8。bit_number[idx]表示该桶在这轮被标记处理的次数。若定时器已到(round_ticks <= compensate_tick)则回调;否则说明它还要等下一轮,减掉补偿值后放回 restart 列表。
数值演算:用一个完整场景看"补偿"到底怎么算
要理解 compensate_tick,必须回到"为什么 bit_number 会累加到 3"这个过程。下面用一个连续的 tick 时间线来演算(仍沿用 8 桶基准数据)。
场景设定:桶 1 里有两个周期定时器 T 和 S,定时器任务此刻正忙(在处理桶 5 里一大批回调,短时间内轮不到桶 1)。同时指针 gs_os_timer_list_current 会周期性扫过桶 1。
- T:
init_ticks = 28。放入桶 1 时按 3.2 节公式算得round_ticks = OS_TIMER_ROUND(28-1) = 24(3 整轮),桶内位置 =(1 + OS_TIMER_MOD(27)) % 8。 - S:
init_ticks = 20。放入桶 1 时round_ticks = OS_TIMER_ROUND(20-1) = 16(2 整轮)。
时间线(每经过 8 个 tick,指针绕回一次桶 1):
| 轮次 | 事件 | bit_number[1] 变化 | T 的 round_ticks | S 的 round_ticks |
|---|---|---|---|---|
| 初始 | 定时器任务忙,还没轮到桶 1 | 0 | 24 | 16 |
| 第 1 轮扫到桶 1 | k_timer_need_handle() 标记到期 | 0 → 1 | 24 | 16 |
| 第 2 轮扫到桶 1 | 定时器任务仍忙,又标记一次 | 1 → 2 | 24 | 16 |
| 第 3 轮扫到桶 1 | 又标记一次 | 2 → 3 | 24 | 16 |
| 定时器任务终于处理桶 1 | 计算补偿值 | 3 | 24 | 16 |
现在定时器任务真正处理桶 1,执行到 compensate_tick 计算:
compensate_tick = (gs_os_timer_active_bit_number[current_list_idx] - 1) * (OS_TIMER_LIST_ENTRIES);bit_number[1] = 3,代入得 compensate_tick = (3 - 1) × 8 = 16。这 16 表示:定时器任务欠桶 1 的处理时间 = 2 个整轮(16 tick)——因为在定时器任务忙的这段时间里,指针已经绕了 2 圈(标记了 3 次,其中"欠"的轮次是 3-1=2 次,第 1 次就是"当前这次",无需补偿)。
逐行判断(T 和 S 分别代入):
| 定时器 | 当前 round_ticks | 判断 round_ticks <= 16? | 结果 |
|---|---|---|---|
| S | 16 | 16 <= 16 成立 | 到期 → 置 round_ticks = 0,触发回调 |
| T | 24 | 24 <= 16 不成立 | 未到期 → round_ticks = 24 - 16 = 8,放回 restart 等下一轮 |
为什么 S 到期、T 未到期:
- S 只需等 2 整轮(16 tick)就到期,而这 16 tick 恰好被"补偿值"全覆盖,所以此刻就是它到期的时候,立即触发。
- T 要等 3 整轮(24 tick),补偿值只覆盖了 2 整轮(16 tick),还剩 1 整轮(8 tick)没走完,所以
round_ticks从 24 减到 8,放回原桶等下一轮指针再扫到,下一次扫到(round_ticks=8)时compensate_tick又会覆盖它,从而触发。
为什么需要补偿:定时器任务执行回调可能耗时(本场景中被桶 5 拖住),导致指针绕了好几圈,同一个桶被 k_timer_need_handle() 标记了多次。如果没有补偿,定时器任务醒来后无法知道"时间已经悄悄过去了 16 个 tick",会把还没真正到期的 T 也当成到期而提前回调,破坏周期准确性。bit_number 记录了这个"漏过的轮次",compensate_tick 据此把欠的时间扣掉,保证周期性定时器的触发时刻尽量接近真实周期。
第四步:调用超时回调(条件 timeout_func != OS_NULL)
OS_KERNEL_EXIT();
/* Call timeout function if it is expired. */
if (OS_NULL != timeout_func)
{
timeout_func(timeout_param);
}
OS_KERNEL_ENTER();- 关键点:调用回调前先
OS_KERNEL_EXIT()开调度,允许更高优先级任务抢占;回调在定时器任务上下文执行,故绝不能是阻塞/长耗时操作。
条件走向总结(单桶处理)
处理第 current_list_idx 个桶(bit_mask 非 0):
├── os_ffs(bit_mask)-1 取最小待处理桶 → 移到到期链表、清掩码位
│ └── while 遍历该桶每个定时器
│ ├── current_timer->round_ticks <= compensate_tick(已到期)
│ │ ├── 若是周期定时器 → 加入 restart 列表待重激活
│ │ └── 取 timeout_func/param,OS_KERNEL_EXIT 后调用回调
│ └── current_timer->round_ticks > compensate_tick(未到期)
│ └── round_ticks -= compensate_tick,加入 restart 列表,不调回调
│ └── bit_number[current_list_idx] = 0(清计数)
└── bit_mask 清空后退出 while4.5 位掩码机制的"为什么"
为什么用位掩码而非直接遍历全部桶?
- 定时器任务每次被唤醒,只处理确实有到期定时器的桶,通过
os_ffs快速定位,避免在每次 tick 都遍历 8 个桶。 - 位掩码是"哪位有到期"的高度压缩表示,配合
os_ffs(硬件指令 / O(1) 位运算)可做到常数时间定位,这就是哈希桶定时器处理高效的又一原因。
五、哈希桶定时器 API 实现
本节给出哈希桶定时器下对外 API 的完整实现源码(
kernel/source/os_timer_hash.c)与内部辅助函数详解。它们与单链表定时器版本的差异点已在本节各小节用表格标注;统一的 API 签名、参数、返回值与使用示例见4.5.4-定时器API.md,本节不再重复。
哈希桶实现的对外 API 全集(均在 OS_USING_HASH_BUCKET_TIMER 下编译):
| API | 功能 | 本节小节 |
|---|---|---|
os_timer_create | 创建定时器(静态/动态) | 5.1 |
os_timer_destroy | 销毁定时器 | 5.2 |
os_timer_start | 启动定时器 | 5.3 |
os_timer_stop | 停止定时器 | 5.4 |
os_timer_set_timeout_ticks | 设置超时 tick 数 | 5.5 |
os_timer_get_timeout_ticks | 获取超时 tick 数 | 5.6 |
os_timer_get_remain_ticks | 获取剩余 tick 数 | 5.7 |
单链表定时器也有同名
os_timer_create等 API,实现见4.5.3的"单链表定时器 API 实现"章节。同名 API 在两个实现下各有一份源码,这是内核"统一接口、双实现"的设计。
5.1 os_timer_create —— 创建定时器(哈希桶版)
来源: kernel/source/os_timer_hash.c
os_timer_id os_timer_create(os_timer_dummy_t *timer_cb,
const char *name,
void (*function)(void *parameter),
void *parameter,
os_tick_t timeout,
uint8_t flag)
{
os_timer_t *timer;
os_timer_t *iter_timer;
os_bool_t status;
#ifndef OS_USING_HEAP
OS_ASSERT(OS_NULL != timer_cb);
#endif
OS_ASSERT((OS_FALSE == os_is_irq_active()));
if (OS_NULL != timer_cb)
{
timer = OS_TYPE_CONVERT(os_timer_t *, timer_cb);
status = OS_TRUE;
os_spin_lock(&gs_os_timer_list_lock);
os_list_for_each_entry(iter_timer, &gs_os_timer_list, os_timer_t, list)
{
if (iter_timer == timer)
{
timer = OS_NULL;
OS_KERN_LOG(KERN_ERROR, TIMER_TAG, "%s", (OS_KERNEL_ERR_INFO));
status = OS_FALSE;
break;
}
}
if (OS_TRUE == status)
{
os_spin_unlock(&gs_os_timer_list_lock);
_k_timer_do_init(timer, name, function, parameter, timeout, flag & (~OS_TIMER_FLAG_DYNAMIC));
os_spin_lock(&gs_os_timer_list_lock);
_k_timer_add_to_list(timer);
}
os_spin_unlock(&gs_os_timer_list_lock);
}
#ifdef OS_USING_HEAP
else
{
timer = (os_timer_t *)OS_KERNEL_MALLOC(sizeof(os_timer_t));
if (OS_NULL == timer)
{
OS_KERN_LOG(KERN_ERROR, TIMER_TAG, "malloc timer fail");
}
else
{
_k_timer_do_init(timer, name, function, parameter, timeout, flag | OS_TIMER_FLAG_DYNAMIC);
os_spin_lock(&gs_os_timer_list_lock);
_k_timer_add_to_list(timer);
os_spin_unlock(&gs_os_timer_list_lock);
}
}
#endif
return OS_TYPE_CONVERT(os_timer_id, timer);
}分步详解
第一步:参数校验(条件 !OS_USING_HEAP 或中断上下文)
#ifndef OS_USING_HEAP
OS_ASSERT(OS_NULL != timer_cb);
#endif
OS_ASSERT((OS_FALSE == os_is_irq_active()));- 进入条件(无堆):若未启用
OS_USING_HEAP,则timer_cb不能为OS_NULL(没有堆可用于动态分配),否则断言。 - 进入条件(中断):
os_is_irq_active()为真时断言。创建定时器不能在中断上下文调用,因为要加自旋锁、遍历链表,中断里做会破坏实时性。
第二步:静态创建分支(条件 timer_cb != OS_NULL)
if (OS_NULL != timer_cb)
{
timer = OS_TYPE_CONVERT(os_timer_t *, timer_cb);
status = OS_TRUE;
os_spin_lock(&gs_os_timer_list_lock);
os_list_for_each_entry(iter_timer, &gs_os_timer_list, os_timer_t, list)
{
if (iter_timer == timer)
{
timer = OS_NULL;
OS_KERN_LOG(KERN_ERROR, TIMER_TAG, "%s", (OS_KERNEL_ERR_INFO));
status = OS_FALSE;
break;
}
}
if (OS_TRUE == status)
{
os_spin_unlock(&gs_os_timer_list_lock);
_k_timer_do_init(timer, name, function, parameter, timeout, flag & (~OS_TIMER_FLAG_DYNAMIC));
os_spin_lock(&gs_os_timer_list_lock);
_k_timer_add_to_list(timer);
}
os_spin_unlock(&gs_os_timer_list_lock);
}- 进入条件:用户传入静态控制块地址。
- 加自旋锁后遍历全局定时器链表
gs_os_timer_list查重:若该控制块已被创建过,则timer = OS_NULL并记录错误日志,本次创建失败(防止重复创建同一静态控制块)。 - 若未重复,调用
_k_timer_do_init(...)(见下方 5.9 辅助函数)初始化字段,其中flag & (~OS_TIMER_FLAG_DYNAMIC)强制清除动态标志(静态创建不属于动态分配)。 - 初始化后加锁挂入全局链表
gs_os_timer_list,返回定时器 ID。
第三步:动态创建分支(条件 timer_cb == OS_NULL 且启用堆)
#ifdef OS_USING_HEAP
else
{
timer = (os_timer_t *)OS_KERNEL_MALLOC(sizeof(os_timer_t));
if (OS_NULL == timer)
{
OS_KERN_LOG(KERN_ERROR, TIMER_TAG, "malloc timer fail");
}
else
{
_k_timer_do_init(timer, name, function, parameter, timeout, flag | OS_TIMER_FLAG_DYNAMIC);
os_spin_lock(&gs_os_timer_list_lock);
_k_timer_add_to_list(timer);
os_spin_unlock(&gs_os_timer_list_lock);
}
}
#endif- 进入条件:
timer_cb == OS_NULL且启用了堆。 - 从堆分配一个
os_timer_t大小的控制块;分配失败记录日志并返回OS_NULL。 - 分配成功则
_k_timer_do_init(...),其中flag | OS_TIMER_FLAG_DYNAMIC置上动态标志(后续os_timer_destroy据此释放堆内存)。
第四步:返回定时器 ID(无条件执行)
return OS_TYPE_CONVERT(os_timer_id, timer);- 把
os_timer_t *强转为os_timer_id返回。创建失败时timer == OS_NULL,返回OS_NULL。
条件走向总结
os_timer_create()(哈希桶版)条件走向总结:
├── 无 OS_USING_HEAP 且 timer_cb == OS_NULL → 断言
├── 处于中断上下文 → 断言
├── timer_cb != OS_NULL(静态创建)
│ ├── 该控制块已在全局链表中(重复创建)→ timer = OS_NULL → 返回 OS_NULL
│ └── 未重复 → _k_timer_do_init(清 DYNAMIC)→ 挂全局链表 → 返回 timer
└── timer_cb == OS_NULL(动态创建,需堆)
├── OS_KERNEL_MALLOC 失败 → 返回 OS_NULL
└── 分配成功 → _k_timer_do_init(置 DYNAMIC)→ 挂全局链表 → 返回 timer5.2 os_timer_destroy —— 销毁定时器(哈希桶版)
来源: kernel/source/os_timer_hash.c
os_err_t os_timer_destroy(os_timer_id timer_id)
{
os_timer_t *timer;
OS_KERNEL_INIT();
timer = OS_TYPE_CONVERT(os_timer_t *, timer_id);
OS_ASSERT(OS_NULL != timer);
OS_ASSERT(0 != (timer->active_node.flag & OS_TIMER_FLAG_INITED));
OS_ASSERT(OS_FALSE == os_is_irq_active());
OS_ASSERT((OS_FALSE == os_is_irq_disabled()));
OS_ASSERT((OS_FALSE == os_is_schedule_locked()));
#ifndef OS_USING_HEAP
OS_ASSERT(OS_TIMER_FLAG_STATIC == (timer->active_node.flag & OS_TIMER_FLAG_DYNAMIC));
#endif
OS_KERNEL_ENTER();
_k_timer_remove(timer);
OS_KERNEL_EXIT();
#ifdef OS_USING_HEAP
if ((timer->active_node.flag & OS_TIMER_FLAG_DYNAMIC) != 0)
{
OS_KERNEL_FREE(timer);
}
#endif
return OS_SUCCESS;
}分步详解
第一步:准备与参数校验(条件:入口无条件执行)
OS_KERNEL_INIT();
timer = OS_TYPE_CONVERT(os_timer_t *, timer_id);
OS_ASSERT(OS_NULL != timer);
OS_ASSERT(0 != (timer->active_node.flag & OS_TIMER_FLAG_INITED));
OS_ASSERT(OS_FALSE == os_is_irq_active());
OS_ASSERT((OS_FALSE == os_is_irq_disabled()));
OS_ASSERT((OS_FALSE == os_is_schedule_locked()));
#ifndef OS_USING_HEAP
OS_ASSERT(OS_TIMER_FLAG_STATIC == (timer->active_node.flag & OS_TIMER_FLAG_DYNAMIC));
#endif- 进入条件:调用
os_timer_destroy()时无条件执行。 OS_ASSERT(OS_NULL != timer):定时器 ID 不能为空。OS_ASSERT(0 != (flag & INITED)):定时器必须已初始化,防止销毁未创建的对象。OS_ASSERT(OS_FALSE == os_is_irq_active())/os_is_irq_disabled()/os_is_schedule_locked():不能在中断、关中断、锁调度时销毁。OS_ASSERT(OS_TIMER_FLAG_STATIC == (flag & DYNAMIC))(无堆时):未启用堆时定时器必为静态。
第二步:从管理链表与活跃链表移除(条件:已初始化)
OS_KERNEL_ENTER();
_k_timer_remove(timer);
OS_KERNEL_EXIT();- 进入条件:参数校验通过后无条件执行。
_k_timer_remove做三件事(见下方 5.10):从全局管理链表摘除list→_k_timer_deactivate停活跃计时并摘活跃桶 → 清除OS_TIMER_FLAG_INITED。
第三步:释放动态内存(条件 OS_USING_HEAP 且为动态分配)
#ifdef OS_USING_HEAP
if ((timer->active_node.flag & OS_TIMER_FLAG_DYNAMIC) != 0)
{
OS_KERNEL_FREE(timer);
}
#endif
return OS_SUCCESS;- 进入条件:启用了堆,且该定时器由
os_timer_create动态创建(置了OS_TIMER_FLAG_DYNAMIC)。 - 满足则
OS_KERNEL_FREE(timer)释放堆内存;静态创建的不释放。最后返回OS_SUCCESS。
条件走向总结
os_timer_destroy()(哈希桶版)条件走向总结:
├── 参数校验失败(timer 为空 / 未初始化 / 在中断 / 关中断 / 锁调度 / 无堆却动态)→ 断言
├── 进入临界区 _k_timer_remove:摘管理链表 → 停活跃计时 → 清 INITED
├── 若启用堆 且 flag&DYNAMIC != 0(动态创建)→ OS_KERNEL_FREE(timer)
└── 返回 OS_SUCCESS5.3 os_timer_start —— 启动定时器(哈希桶版)
来源: kernel/source/os_timer_hash.c
os_err_t os_timer_start(os_timer_id timer_id)
{
os_timer_t *timer;
OS_KERNEL_INIT();
timer = OS_TYPE_CONVERT(os_timer_t *, timer_id);
OS_ASSERT(OS_NULL != timer);
OS_ASSERT(0 != (timer->active_node.flag & OS_TIMER_FLAG_INITED));
OS_KERNEL_ENTER();
/* First deactivate timer, then start it. */
_k_timer_deactivate(timer);
_k_timer_activate(timer);
OS_KERNEL_EXIT();
return OS_SUCCESS;
}分步详解
第一步:准备与参数校验(条件:入口无条件执行)
OS_KERNEL_INIT();
timer = OS_TYPE_CONVERT(os_timer_t *, timer_id);
OS_ASSERT(OS_NULL != timer);
OS_ASSERT(0 != (timer->active_node.flag & OS_TIMER_FLAG_INITED));- 进入条件:调用
os_timer_start()时无条件执行。 OS_ASSERT(OS_NULL != timer)/OS_ASSERT(0 != (flag & INITED)):定时器非空且已初始化(否则启动无意义)。
第二步:先停后启(条件:已初始化)
OS_KERNEL_ENTER();
/* First deactivate timer, then start it. */
_k_timer_deactivate(timer);
_k_timer_activate(timer);
OS_KERNEL_EXIT();
return OS_SUCCESS;- 进入条件:参数校验通过后无条件执行。
- 先停:
_k_timer_deactivate(timer)—— 若定时器已在运行先停止并摘旧节点,保证重复os_timer_start从初始超时重新计时。 - 后启:
_k_timer_activate(timer)—— 计算桶索引、挂入活跃桶、置ACTIVATED。_k_timer_activate的完整源码与详解见上文 3.2 节。 - 出临界区后返回
OS_SUCCESS。
为什么"先停后启"? 若不先停掉旧的活跃状态,重复启动会导致同一定时器被重复挂入活跃桶,产生"一个定时器多个计时节点"的脏状态。先停后启保证了"每次启动都是干净的一次全新计时"。
条件走向总结
os_timer_start()(哈希桶版)条件走向总结:
├── timer 为空 / 未初始化 → 断言
├── 进入临界区
│ ├── _k_timer_deactivate:若曾激活则停止(否则空操作)
│ └── _k_timer_activate:计算桶索引、挂入活跃桶、置 ACTIVATED
└── 返回 OS_SUCCESS5.4 os_timer_stop —— 停止定时器(哈希桶版)
来源: kernel/source/os_timer_hash.c
os_err_t os_timer_stop(os_timer_id timer_id)
{
os_err_t ret;
os_timer_t *timer;
OS_KERNEL_INIT();
timer = OS_TYPE_CONVERT(os_timer_t *, timer_id);
OS_ASSERT(OS_NULL != timer);
OS_ASSERT(0 != (timer->active_node.flag & OS_TIMER_FLAG_INITED));
if (!(timer->active_node.flag & OS_TIMER_FLAG_ACTIVATED))
{
ret = OS_FAILURE;
}
else
{
OS_KERNEL_ENTER();
_k_timer_deactivate(timer);
OS_KERNEL_EXIT();
ret = OS_SUCCESS;
}
return ret;
}分步详解
第一步:准备与参数校验(条件:入口无条件执行)
OS_KERNEL_INIT();
timer = OS_TYPE_CONVERT(os_timer_t *, timer_id);
OS_ASSERT(OS_NULL != timer);
OS_ASSERT(0 != (timer->active_node.flag & OS_TIMER_FLAG_INITED));- 进入条件:调用
os_timer_stop()时无条件执行。 OS_ASSERT(OS_NULL != timer)/OS_ASSERT(0 != (flag & INITED)):定时器非空且已初始化。
第二步:判断是否在运行(条件 flag & ACTIVATED)
if (!(timer->active_node.flag & OS_TIMER_FLAG_ACTIVATED))
{
ret = OS_FAILURE;
}
else
{
OS_KERNEL_ENTER();
_k_timer_deactivate(timer);
OS_KERNEL_EXIT();
ret = OS_SUCCESS;
}
return ret;- 进入条件 A(未激活):
flag & ACTIVATED == 0,定时器本来就没在运行(从未 start 或已单次到期)。直接返回OS_FAILURE,不做任何操作。 - 进入条件 B(激活中):
flag & ACTIVATED != 0,定时器正在运行。临界区内_k_timer_deactivate(timer)停止它并摘除活跃节点,返回OS_SUCCESS。
为什么"未运行返回
OS_FAILURE"? 这样调用方能区分"我确实停掉了一个正在跑的定时器"(OS_SUCCESS)与"它本来就没在跑"(OS_FAILURE)。因此应检查返回值。
条件走向总结
os_timer_stop()(哈希桶版)条件走向总结:
├── timer 为空 / 未初始化 → 断言
├── flag & ACTIVATED == 0(未在运行)→ ret = OS_FAILURE → 返回 OS_FAILURE
└── flag & ACTIVATED != 0(在运行)→ _k_timer_deactivate → ret = OS_SUCCESS → 返回 OS_SUCCESS5.5 os_timer_set_timeout_ticks —— 设置超时 tick 数(哈希桶版)
来源: kernel/source/os_timer_hash.c
os_err_t os_timer_set_timeout_ticks(os_timer_id timer_id, os_tick_t timeout)
{
os_timer_t *timer;
OS_KERNEL_INIT();
timer = OS_TYPE_CONVERT(os_timer_t *, timer_id);
OS_ASSERT(OS_NULL != timer);
OS_ASSERT(0 != (timer->active_node.flag & OS_TIMER_FLAG_INITED));
OS_KERNEL_ENTER();
timer->init_ticks = timeout;
OS_KERNEL_EXIT();
return OS_SUCCESS;
}分步详解
第一步:准备与参数校验(条件:入口无条件执行)
OS_KERNEL_INIT();
timer = OS_TYPE_CONVERT(os_timer_t *, timer_id);
OS_ASSERT(OS_NULL != timer);
OS_ASSERT(0 != (timer->active_node.flag & OS_TIMER_FLAG_INITED));- 进入条件:调用时无条件执行。
OS_ASSERT(OS_NULL != timer)/OS_ASSERT(0 != (flag & INITED)):定时器非空且已初始化。
第二步:写入 init_ticks(条件:已初始化)
OS_KERNEL_ENTER();
timer->init_ticks = timeout;
OS_KERNEL_EXIT();
return OS_SUCCESS;- 进入条件:参数校验通过后无条件执行。
- 临界区内把
init_ticks直接改成timeout。不修改round_ticks,也不改变运行状态。 - 返回
OS_SUCCESS。
注意:此 API 只改"下次启动用的超时",对当前正在运行的定时器计时无影响。重新
os_timer_start(先停后启)后才按新值计时。
5.6 os_timer_get_timeout_ticks —— 获取超时 tick 数(哈希桶版)
来源: kernel/source/os_timer_hash.c
os_tick_t os_timer_get_timeout_ticks(os_timer_id timer_id)
{
os_tick_t init_ticks;
os_timer_t *timer;
OS_KERNEL_INIT();
timer = OS_TYPE_CONVERT(os_timer_t *, timer_id);
OS_ASSERT(OS_NULL != timer);
OS_ASSERT(0 != (timer->active_node.flag & OS_TIMER_FLAG_INITED));
OS_KERNEL_ENTER();
init_ticks = timer->init_ticks;
OS_KERNEL_EXIT();
return init_ticks;
}分步详解
第一步:准备与参数校验(条件:入口无条件执行)
OS_KERNEL_INIT();
timer = OS_TYPE_CONVERT(os_timer_t *, timer_id);
OS_ASSERT(OS_NULL != timer);
OS_ASSERT(0 != (timer->active_node.flag & OS_TIMER_FLAG_INITED));- 进入条件:调用时无条件执行。
OS_ASSERT(OS_NULL != timer)/OS_ASSERT(0 != (flag & INITED)):定时器非空且已初始化。
第二步:读取 init_ticks 并返回(条件:已初始化)
OS_KERNEL_ENTER();
init_ticks = timer->init_ticks;
OS_KERNEL_EXIT();
return init_ticks;- 进入条件:参数校验通过后无条件执行。
- 临界区内把
timer->init_ticks读入局部变量返回。 - 返回的是初始超时(
init_ticks),而非剩余时间。
易混淆:
get_timeout_ticks(初始值,init_ticks)≠get_remain_ticks(剩余值)。前者是"创建时设定的超时",后者是"当前还差多少 tick"。
5.7 os_timer_get_remain_ticks —— 获取剩余 tick 数(哈希桶版)
来源: kernel/source/os_timer_hash.c
os_tick_t os_timer_get_remain_ticks(os_timer_id timer_id)
{
os_tick_t remain_ticks;
os_timer_t *timer;
OS_KERNEL_INIT();
timer = OS_TYPE_CONVERT(os_timer_t *, timer_id);
OS_ASSERT(OS_NULL != timer);
OS_ASSERT(0 != (timer->active_node.flag & OS_TIMER_FLAG_INITED));
OS_KERNEL_ENTER();
remain_ticks = _k_timer_calc_remain_ticks(timer);
OS_KERNEL_EXIT();
return remain_ticks;
}分步详解
第一步:准备与参数校验(条件:入口无条件执行)
OS_KERNEL_INIT();
timer = OS_TYPE_CONVERT(os_timer_t *, timer_id);
OS_ASSERT(OS_NULL != timer);
OS_ASSERT(0 != (timer->active_node.flag & OS_TIMER_FLAG_INITED));- 进入条件:调用时无条件执行。
OS_ASSERT(OS_NULL != timer)/OS_ASSERT(0 != (flag & INITED)):定时器非空且已初始化。
第二步:调用计算函数并返回(条件:已初始化)
OS_KERNEL_ENTER();
remain_ticks = _k_timer_calc_remain_ticks(timer);
OS_KERNEL_EXIT();
return remain_ticks;- 进入条件:参数校验通过后无条件执行。
- 临界区内调用
_k_timer_calc_remain_ticks(timer)计算剩余 tick,返回。 - 若定时器未激活(未 start 或已到期),返回 0。
_k_timer_calc_remain_ticks的完整源码与演算见下方 5.11 辅助函数。
5.8 os_timer_check_exist —— 检查定时器是否存在(哈希桶版)
来源: kernel/source/os_timer_hash.c
os_bool_t os_timer_check_exist(os_timer_id timer_id)
{
os_timer_t *iter_timer;
os_bool_t valid_timer;
os_timer_t *timer;
timer = OS_TYPE_CONVERT(os_timer_t *, timer_id);
if (OS_NULL == timer)
{
os_kprintf("Invalid Parameter\r\n");
return OS_FALSE;
}
valid_timer = OS_FALSE;
os_spin_lock(&gs_os_timer_list_lock);
os_list_for_each_entry(iter_timer, &gs_os_timer_list, os_timer_t, list)
{
if (iter_timer == timer)
{
valid_timer = OS_TRUE;
break;
}
}
os_spin_unlock(&gs_os_timer_list_lock);
return valid_timer;
}分步详解
第一步:参数校验(条件:timer_id 是否为空)
timer = OS_TYPE_CONVERT(os_timer_t *, timer_id);
if (OS_NULL == timer)
{
os_kprintf("Invalid Parameter\r\n");
return OS_FALSE;
}- 进入条件:调用时无条件执行。
- 若
timer_id为OS_NULL,直接打印错误信息并返回OS_FALSE。此函数是唯一不做INITED断言、可安全用于"判断一个 ID 是否有效"的查询函数。
第二步:遍历全局链表查找(条件:timer 非空)
valid_timer = OS_FALSE;
os_spin_lock(&gs_os_timer_list_lock);
os_list_for_each_entry(iter_timer, &gs_os_timer_list, os_timer_t, list)
{
if (iter_timer == timer)
{
valid_timer = OS_TRUE;
break;
}
}
os_spin_unlock(&gs_os_timer_list_lock);
return valid_timer;- 进入条件:
timer非空。 - 加自旋锁后遍历全局管理链表
gs_os_timer_list:若找到iter_timer == timer,置valid_timer = OS_TRUE并退出。 - 返回
valid_timer:OS_TRUE表示该定时器已登记在册(存在);OS_FALSE表示未登记(可能被销毁或 ID 非法)。
条件走向总结
os_timer_check_exist()(哈希桶版)条件走向总结:
├── timer_id == OS_NULL → 打印错误 → 返回 OS_FALSE
└── timer_id != OS_NULL
├── 在全局链表中找到该定时器 → valid_timer = OS_TRUE → 返回 OS_TRUE
└── 未找到 → valid_timer = OS_FALSE → 返回 OS_FALSE单链表版差异:单链表版
os_timer_check_exist不做遍历,而是直接检查flag & INITED位来判断(见kernel/source/os_timer_slist.c),但语义一致——判断该 ID 是否是一个已初始化的有效定时器。
5.9 核心辅助函数 _k_timer_do_init —— 定时器字段初始化
一句话定位:_k_timer_do_init 在 os_timer_create 的静态/动态两分支中都被调用,负责把定时器控制块的所有字段初始化为初始状态,不持锁(由调用方保证临界区)。
来源: kernel/source/os_timer_hash.c
static void _k_timer_do_init(os_timer_t *timer,
const char *name,
void (*function)(void *parameter),
void *parameter,
os_tick_t timeout,
uint8_t flag)
{
timer->active_node.flag = (flag & (~OS_TIMER_FLAG_ACTIVATED)) | OS_TIMER_FLAG_INITED;
timer->active_node.info = 0;
timer->timeout_func = function;
timer->parameter = parameter;
timer->init_ticks = ((0 == timeout) ? 1U : timeout);
timer->round_ticks = timer->init_ticks;
timer->index = OS_TIMER_INDEX_INVALID;
if (OS_NULL != name)
{
(void)strncpy(&timer->name[0], name, OS_NAME_MAX);
timer->name[OS_NAME_MAX] = '\0';
}
else
{
timer->name[0] = '\0';
}
os_list_init(&timer->active_node.active_list);
#ifdef OS_HASH_BUCKET_TIMER_SORT
_k_timer_clear_head(&timer->active_node);
#endif
}逐行解释:
| 代码 | 作用 |
|---|---|
flag = (flag & ~ACTIVATED) | INITED | 清"激活"位(新建时未激活),置"已初始化"位 |
active_node.info = 0 | 清句柄标志 |
timeout_func = function / parameter = parameter | 保存回调与参数 |
init_ticks = (0 == timeout) ? 1U : timeout | 超时 tick,0 会被当作 1(防非法值) |
round_ticks = init_ticks | 初始剩余等于初始超时 |
index = OS_TIMER_INDEX_INVALID | 桶索引置为无效(未挂桶) |
strncpy / name[OS_NAME_MAX] = '\0' | 复制名称并确保以 \0 结尾 |
os_list_init(&timer->active_node.active_list) | 初始化活跃链表节点 |
_k_timer_clear_head(SORT 模式) | 清除"链表头"标志 |
标志位演算(假设用户传入 flag = OS_TIMER_FLAG_PERIODIC | OS_TIMER_FLAG_SOFT_TIMER,即二进制 0000 0101):
| 计算项 | 代入计算 | 结果 |
|---|---|---|
| 用户 flag | 0x05(0000 0101) | 0000 0101 |
flag & ~ACTIVATED(~0x02 = 1111 1101) | 0000 0101 & 1111 1101 | 0000 0101(bit1 清 0) |
| OS_TIMER_FLAG_INITED(0000 1000) | 0000 0101 | 0000 1000 | 0000 1101(bit3 置 1) |
最终 active_node.flag = 0x0D,即 bit0=1(周期)、bit2=1(软)、bit3=1(已初始化)。
5.10 核心辅助函数 _k_timer_remove / _k_timer_deactivate —— 摘链并停计时
一句话定位:_k_timer_remove 是 os_timer_destroy 的核心动作,负责把定时器从全局管理链表移除、停掉活跃计时并从活跃桶链表移除、最后清除"已初始化"标志。它调用了 _k_timer_deactivate,两者都不持锁,由调用方临界区保证。
来源: kernel/source/os_timer_hash.c
static void _k_timer_remove(os_timer_t *timer)
{
_k_timer_del_from_list(timer);
_k_timer_deactivate(timer);
timer->active_node.flag &= ~OS_TIMER_FLAG_INITED;
}逐行解释:
| 代码 | 作用 |
|---|---|
_k_timer_del_from_list(timer) | 从全局管理链表 gs_os_timer_list 摘除 timer->list 节点 |
_k_timer_deactivate(timer) | 若处于激活状态,清激活标志、桶索引置无效、从活跃桶链表摘除节点(见下方) |
flag &= ~INITED | 清除"已初始化"标志,标志此定时器对象已失效 |
若定时器本就未激活,
_k_timer_deactivate内部判断ACTIVATED位为 0 会直接跳过,不会重复摘链。销毁一个从未启动的定时器也是安全的。
来源: kernel/source/os_timer_hash.c
static void _k_timer_deactivate(os_timer_t *timer)
{
if ((timer->active_node.flag & OS_TIMER_FLAG_ACTIVATED) != 0)
{
_k_timer_active_bit_clear(timer);
timer->active_node.flag &= ~OS_TIMER_FLAG_ACTIVATED;
timer->index = OS_TIMER_INDEX_INVALID;
if (!os_list_empty(&timer->active_node.active_list))
{
#ifdef OS_HASH_BUCKET_TIMER_SORT
_k_timer_del_from_sort_list(timer);
#else
os_list_del(&timer->active_node.active_list);
#endif
}
}
}逐行解释:
| 代码 | 作用 |
|---|---|
if ((flag & ACTIVATED) != 0) | 仅在定时器正在激活时执行;否则直接跳过(停止未运行定时器是空操作) |
_k_timer_active_bit_clear(timer) | 若该桶只剩本定时器且其 round_ticks==0,清除活跃桶位图对应位 |
flag &= ~ACTIVATED | 清除激活标志 |
index = OS_TIMER_INDEX_INVALID | 桶索引置为无效 |
if (!os_list_empty(...active_list)) | 若活跃链表节点已挂入某桶,则摘除(SORT 模式走 _k_timer_del_from_sort_list,否则 os_list_del) |
5.11 核心辅助函数 _k_timer_calc_remain_ticks —— 计算剩余 tick
一句话定位:_k_timer_calc_remain_ticks 被 os_timer_get_remain_ticks 调用,根据定时器所在桶索引、当前活跃指针、以及"正在被定时器任务处理/已触发"的补偿量,估算出剩余 tick 数。
来源: kernel/source/os_timer_hash.c
static os_tick_t _k_timer_calc_remain_ticks(os_timer_t *timer)
{
os_timer_active_list_info_t *current;
os_timer_active_list_info_t *start;
os_timer_active_list_info_t *end;
os_tick_t remain_tick;
os_tick_t diff;
uint32_t current_index;
uint8_t handle_flag_index;
os_tick_t compensate_tick;
#ifdef OS_HASH_BUCKET_TIMER_SORT
os_timer_t *tmp_timer;
os_list_node_t *pos;
os_timer_active_node_t *active_node;
#endif
if ((timer->active_node.flag & OS_TIMER_FLAG_ACTIVATED) != 0)
{
OS_ASSERT((OS_TIMER_INDEX_INVALID != timer->index));
_k_timer_get_active_list(¤t, &start, &end);
/* Calculate the index of list pointed by gs_os_timer_list_current. */
current_index = current - start;
if (timer->index >= current_index)
{
/*
* The index of the active list where the timer is located is greater
* than index of list pointed by gs_os_timer_list_current.
*/
diff = timer->index - current_index + 1;
}
else
{
/*
* The index of the active list where the timer is located is less
* than index of list pointed by gs_os_timer_list_current. The pointer
* needs to wrap around to reach the list where the timer is located.
*/
diff = end - current + 1;
diff += timer->index + 1;
}
remain_tick = 0;
#ifdef OS_HASH_BUCKET_TIMER_SORT
/*
* If OS_HASH_BUCKET_TIMER_SORT enable,the actual round ticks of the timer is the
* sum of its round ticks plus all the round ticks of its previous timers
* which are located in the same active list.
*/
for (pos = &timer->active_node.active_list;; pos = pos->prev)
{
active_node = os_list_entry(pos, os_timer_active_node_t, active_list);
if (OS_TIMER_IS_HEAD(active_node))
{
break;
}
tmp_timer = os_list_entry(pos, os_timer_t, active_node.active_list);
remain_tick += tmp_timer->round_ticks;
}
remain_tick += diff;
#else
remain_tick = timer->round_ticks + diff;
#endif
handle_flag_index = OS_TIMER_GET_HANDLE_FLAG_INDEX(&timer->active_node);
if ((1 == gs_os_timer_handle_flag[timer->index][handle_flag_index]) ||
((OS_BIT_GET(gs_os_timer_active_bit_mask, timer->index)) != 0))
{
/* Tick needed to compensate */
compensate_tick = gs_os_timer_active_bit_number[timer->index] * (OS_TIMER_LIST_ENTRIES);
/* Timer is being handled by timer task. */
if (remain_tick <= compensate_tick)
{
remain_tick = 0;
}
else
{
remain_tick -= compensate_tick;
}
}
}
else
{
remain_tick = 0;
}
return remain_tick;
}数值演算:沿用 2.4 节基准数据(8 桶),设定时器挂桶 5、round_ticks = 16、当前活跃指针在桶 2(current_index = 2)、SORT 模式关闭:
| 计算项 | 代入计算 | 结果 |
|---|---|---|
timer->index >= current_index? | 5 >= 2 | 是 |
diff | 5 - 2 + 1 | 4 |
remain_tick(非 SORT) | round_ticks + diff = 16 + 4 | 20 |
| 补偿判断:是否被处理/触发? | 假设 handle_flag != 0,active_bit_number[index]=0 | compensate_tick = 0 |
最终 remain_tick | 20 - 0 | 20 |
即剩余约 20 tick。若该桶正被定时器任务处理且累计处理了若干完整桶周期,则按 active_bit_number * ENTRIES 补偿扣减,甚至归 0(说明马上到期)。
逐行解释:
| 代码 | 作用 |
|---|---|
if ((flag & ACTIVATED) != 0) | 仅激活中的定时器有剩余时间;否则直接返回 0 |
OS_ASSERT(OS_TIMER_INDEX_INVALID != timer->index) | 激活定时器必然已挂桶,索引必须有效 |
current_index = current - start | 求当前活跃指针相对桶数组的偏移 |
diff 计算 | 求"定时器所在桶"相对"当前桶"的桶跨度(含环绕处理) |
remain_tick = round_ticks + diff(非 SORT) | 剩余 = 跨桶差值 + 桶内跨度 |
| 补偿逻辑 | 若定时器所在桶正被定时器任务处理或已触发,按累计完整周期补偿扣减 |
else remain_tick = 0 | 未激活时剩余为 0 |
单链表版
k_single_list_timer_calc_remain_ticks逻辑不同:它按排序链表顺序累加round_ticks直到找到该定时器,见4.5.3的 6.9 节。
5.12 哈希桶版对外 API 特点小结
| API | 与单链表版的差异点 |
|---|---|
os_timer_create | 用自旋锁 gs_os_timer_list_lock 保护;静态创建会遍历全局链表查重;无硬/软标志校正(哈希桶只有软件定时器) |
os_timer_destroy | 用 _k_timer_remove;额外断言 os_is_irq_disabled/os_is_schedule_locked |
os_timer_start | 先 _k_timer_deactivate 再 _k_timer_activate |
os_timer_stop | 未激活返回 OS_FAILURE,激活则 _k_timer_deactivate |
set/get_timeout_ticks | 与单链表版逐字相同 |
get_remain_ticks | 调 _k_timer_calc_remain_ticks(哈希桶专属计算) |
os_timer_check_exist | 遍历全局链表 gs_os_timer_list 查找;单链表版直接查 INITED 位 |
os_timer_get_name/os_timer_is_active/os_timer_is_periodic/os_timer_set_oneshot/os_timer_set_periodic等函数在哈希桶与单链表两实现下逐字相同(仅在os_timer.h声明),其实现不再重复贴出,见4.5.4-定时器API.md的签名与使用说明。
六、配置与使用
6.1 配置选项
哈希桶定时器的启用与参数通过以下宏控制(定义在 oneos_config.h 中):
对应宏速查表:
| 宏 | 类型 | 默认值 | 作用 |
|---|---|---|---|
OS_USING_KERNEL_TIMER | bool | - | 必须启用,内核定时器总开关 |
OS_USING_HASH_BUCKET_TIMER | bool | 默认方案 | 选择哈希桶定时器实现 |
OS_TIMER_TASK_STACK_SIZE | int | 512 | 定时器任务栈大小(字节) |
OS_HASH_BUCKET_TIMER_POWER | int | 3 | 桶数量 = 1 << POWER(0~5) |
OS_HASH_BUCKET_TIMER_SORT | bool | n | 桶内定时器是否排序(低功耗 tickless 需要) |
桶数量对照表(OS_HASH_BUCKET_TIMER_POWER → OS_TIMER_LIST_ENTRIES):
| POWER | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| 桶数 | 1 | 2 | 4 | 8 | 16 | 32 |
若
OS_TIMER_LIST_ENTRIES > 32,源码中会触发#error "OS_TIMER_LIST_ENTRIES can not be greater than 32 !!!",因为位掩码gs_os_timer_active_bit_mask是 32 位uint32_t。
6.2 使用限制
- 不支持硬件定时器:哈希桶定时器只有软件定时器,回调在定时器任务上下文中执行,不能在 ISR 中运行。
OS_TIMER_FLAG_HARD_TIMER/OS_TIMER_FLAG_SOFT_TIMER标志仅单链表定时器有:哈希桶实现不使用这些标志。- 低功耗模式必须排序:若启用
OS_USING_TICKLESS_LPMGR,必须同时定义OS_HASH_BUCKET_TIMER_SORT,否则编译报错(os_clock.c中#error)。
📝 本节小结
核心要点回顾
| 要点 | 说明 |
|---|---|
| O(1) 插入 | 插入定时器 = 1 次哈希(位与)+ 1 次链表尾插,不遍历,与定时器总数无关 |
| 桶结构 | 环形桶数组 gs_os_timer_active_list_info[8],每桶一个双向链表,指针 current 每 tick 前移 |
| 位掩码 | gs_os_timer_active_bit_mask 标记"哪个桶有到期定时器",配合 os_ffs 常数时间定位 |
| tick 分工 | k_timer_need_handle() 检查并唤醒定时器任务;k_move_timer_list_one_step() 前移当前桶 |
| 定时器任务 | 最高优先级,处理所有有到期的桶,调用回调,周期定时器重激活 |
| 补偿机制 | bit_number 记录漏过的轮次,compensate_tick 补偿周期性定时器周期 |
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
| 回调里做阻塞/长耗时操作 | 回调在定时器任务中执行,必须短小、不阻塞,否则卡住所有定时器 |
| 在 ISR 中期望执行回调 | 哈希桶只有软件定时器,回调在定时器任务上下文;ISR 场景用单链表硬定时器 |
低功耗启用却不定义 OS_HASH_BUCKET_TIMER_SORT | 用 tickless 必须同时定义 SORT,否则编译 #error |
把 round_ticks 当作"绝对剩余 tick 数" | 它是"整轮数×8"的相对量,剩余绝对时间需经 OS_TIMER_MOD/补偿计算 |
用 index = (current + timeout) % N 直接算桶 | 源码为防溢出分两步 OS_TIMER_MOD,需减 1(init_ticks - 1) |
| 定时器数量很少却追求 O(1) | ≤10 个定时器用单链表更省内存、更简单 |
下一步
- 4.5.3 单链表定时器:了解单链表定时器如何通过有序链表实现硬/软定时器,与哈希桶方案对比。