在Zephyr Tick Clock简介一文中分析tick clock的工作原理提到每个tick中断的时候将会调用z_clock_announce,通知现在已经走了一个tick了,同时也提到了tick clock是sheep time和wait timeout的基础设施,本文将分析Zephyr的Timeout模块,说明Zephyr如何管理timeout对象,以及如果驱动timeout。
Timeout本身会去驱动Zephyr内核的时间片,本文不对该部分进行分析。同时我们继续以tickless kernel既一个tick一次中断来分析。
Timeout分析
Timeout节点
Zephyr的Timeout模块管理的是一组struct _timeout节点,节点内描述该节点要在多少个tick后超时和超时后要调用的callback
1 2 3 4 5 6 7 | typedef void (*_timeout_func_t)(struct _timeout *t); struct _timeout { sys_dnode_t node; s32_t dticks; //超时tick _timeout_func_t fn; //超时后调用fn }; |
Timeout实现
Timeout模块的代码在kernel/timeout.c中,Timeout模块的管理实现如下图:
Timeout本身维护一个双向链表,链表的timeout_list, 链表内等待timeout的节点已等待的tick数从小到大排序,某一个节点要等待的dick数是从链表的头一直累加到该节点:例如
T1等待的时间是T1->dticks,
T2等待的时间是T2->dticks+T1->dticks
T3等待的时间是T3->dticks+T2->dticks+T1->dticks
这样做的好处是,每次tick中断到后只用更新第一个节点的dticks,就等于对所有节点需要等待的ticks的更新,而不用遍历整个链表,这样可以有效缩短tick中断的时间。
分析前提:
我们仍然以非tickless kernel(1个tick一次中断)为前提进行分析,所以不在tick中断内时announce_remaining永远为0,z_clock_elapsed()也会返回为0,进一步的elapsed()返回值也会退化为0
1 2 3 4 | static s32_t elapsed(void) { return announce_remaining == 0 ? z_clock_elapsed() : 0; } |
当有使用者需要进行timeout时通过z_add_timeout将timeout的节点加入到链表。
z_add_timeout
从下面的流程分析可以看到,插入一个timeout节点时,会从头开始遍历链表,刨除比自己小的节点等待的tick数,然后让自己的后续节点刨除该节点要等待的tick数(图中红色部分)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | void z_add_timeout(struct _timeout *to, _timeout_func_t fn, s32_t ticks) { //注册回调 to->fn = fn; //必须等待至少一个tick ticks = MAX(1, ticks); LOCKED(&timeout_lock) { struct _timeout *t; to->dticks = ticks + elapsed(); //elapsed()为0,to->dticks为ticks //开始遍历链表,将to按照ticks从小到大的顺序排列 for (t = first(); t != NULL; t = next(t)) { __ASSERT(t->dticks >= 0, ""); if (t->dticks > to->dticks) { //更新插入节点后一个节点的等待ticks数, t->dticks -= to->dticks; sys_dlist_insert(&t->node, &to->node); break; } //减去前面节点的tick数 to->dticks -= t->dticks; } //如果链表为NULL,直接将节点放入链表的head if (t == NULL) { sys_dlist_append(&timeout_list, &to->node); } //该流程对非tickless kernel无效 if (to == first()) { z_clock_set_timeout(next_timeout(), false); } } } |
添加了timeout对象也可以通过z_abort_timeout移除对应的timeout对象。
z_abort_timeout
由于移除节点会将tick数一并移除,所以要将移除的tick数还到后续节点内(图中绿色部分)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | int z_abort_timeout(struct _timeout *to) { int ret = -EINVAL; LOCKED(&timeout_lock) { //在链表中找到要移除的节点 if (sys_dnode_is_linked(&to->node)) { //移除该节点 remove_timeout(to); ret = 0; } } return ret; } static void remove_timeout(struct _timeout *t) { //如果要移除的节点有后续节点,需要将要移除的节点tick数补回到后续节点去 if (next(t) != NULL) { next(t)->dticks += t->dticks; } //从链表中移除节点 sys_dlist_remove(&t->node); } |
Timeout更新
每次tick中断发生时,就会驱动引擎z_clock_announce,在z_clock_announce内会对链表的tick数进行更新并检查,如果发现节点超时将移除节点并进行callback
对于非tickless kernel每一个tick中断都会调用z_clock_announce,传入的参数为ticks = 1,代码分析如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | void z_clock_announce(s32_t ticks) { k_spinlock_key_t key = k_spin_lock(&timeout_lock); announce_remaining = ticks; //第一个节点超时,移除第一个节点循环检查后面的节点是否超时 //可能存在多个节点同一时刻超时的情况 while (first() != NULL && first()->dticks <= 1) { struct _timeout *t = first(); int dt = t->dticks; //系统tick累加 curr_tick += dt; //announce_remaining被变为0 announce_remaining -= dt; //移除超时节点 t->dticks = 0; remove_timeout(t); //超时节点回调z_add_timeout的fn k_spin_unlock(&timeout_lock, key); t->fn(t); key = k_spin_lock(&timeout_lock); } //如果之前有节点超时announce_remaining会被清0,这里操作无意义 //如果之前没有节点超时,这里对链表的超时tick进行更新 if (first() != NULL) { first()->dticks -= announce_remaining; } //tick累加 curr_tick += announce_remaining; announce_remaining = 0; //对于非tickless kernel, z_clock_set_timeout不会生效 z_clock_set_timeout(next_timeout(), false); k_spin_unlock(&timeout_lock, key); |
其它API
下面介绍一些在非tickless kernel下使用的其它API,原理比较简单不再展开分析,有兴趣可以翻阅代码
s32_t z_timeout_remaining(struct _timeout *timeout) 获取指定timeout还剩多少tick超时
s64_t z_tick_get(void) 返回系统运行了多少个tick,也就是curr_tick
u32_t z_tick_get_32(void) 返回系统运行了多少个tick,取curr_tick低32位
Tickless Kernel
在Tickless kernel下,会在每次Tick中断时通过timeout的next_timeout计算下一次超时要多少个tick,并以该时间设置systick,让其在超时的时候才产生中断,这也将大大减少tick中断的上下文切换,之后会有文章专门分析该部分流程。
关于k_time
在zephyr内核中提供了一个k_timer, kernel/timer.c, 其实就是包装了timeout模块,因此使用k_timer要特别注意过期回调函数,因为k_timer注册的过期回调函数最后是通过timeout模块在tick isr中执行,如果执行时间过长会影响zephyr的tick调度。