Lwip循环超时事件
1,背景:
在学习野火《LwIP 应用开发实战指南》中的超时处理时,对于循环超时事件的注册逻辑看得晕晕乎乎的,在对比了源码和野火文档的讲解后,觉得有必要记录一下自己的理解,也方便其他初学者在理解这部分内容时能少绕一些弯路。 注意:本文一些地方会直接使用野火文档的描述,感兴趣的读者请直接阅读野火文档。
2,超时队列
LwIP 通过一个 sys_timeo 类型的数据结构管理与超时链表相关的所有超时事件。 LwIP
使用这个结构体记录下内核中所有被注册的超时事件, 这些结构体会以链表的形式一个个 连接在超时链表中,
而内核中只有一条超时链表,那么怎么对超时链表进行管理呢? LwIP 定义了一个 sys_timeo 类型的指针
next_timeout,并且将 next_timeout 指向当前内核中链表
头部,所有被注册的超时事件都会按照被处理的先后顺序排列在超时链表上。
简单来说,就是lwip内核会维护一条超时链表,每次需要进行超时检测的时候,就调用接口往超时链表中添加节点就好了。
typedef void (* sys_timeout_handler)(void *arg);
struct sys_timeo
{
struct sys_timeo *next; (1)
u32_t time; (2)
sys_timeout_handler h; (3)
void *arg; (4)
};
/** The one and only timeout list */
static struct sys_timeo *next_timeout; (5)
以下是注册函数
void
sys_timeout(u32_t msecs, sys_timeout_handler handler, void *arg)
{
u32_t next_timeout_time;
LWIP_ASSERT_CORE_LOCKED();
/* overflow handled by TIME_LESS_THAN macro */
next_timeout_time = (u32_t)(sys_now() + msecs); (1)
sys_timeout_abs(next_timeout_time, handler, arg);
}
static void
sys_timeout_abs(u32_t abs_time, sys_timeout_handler handler, void *arg)
{
struct sys_timeo *timeout, *t;
timeout = (struct sys_timeo *)memp_malloc(MEMP_SYS_TIMEOUT); (2)
if (timeout == NULL)
{
return;
}
timeout->next = NULL;
timeout->h = handler;
timeout->arg = arg;
timeout->time = abs_time; (3)
if (next_timeout == NULL)
{
next_timeout = timeout; (4)
return;
}
if (TIME_LESS_THAN(timeout->time, next_timeout->time))
{
timeout->next = next_timeout;
next_timeout = timeout; (5)
}
else
{
for (t = next_timeout; t != NULL; t = t->next)
{
if ((t->next == NULL) ||
TIME_LESS_THAN(timeout->time, t->next->time))
{
timeout->next = t->next;
t->next = timeout; (6)
break;
}
}
}
}
3,超时检查
void sys_check_timeouts(void): 这是用于裸机的函数,用户需要在裸机应用程序中周期 性调用该函数,每次调用的时候
LwIP 都会检查超时链表上第一个 sys_timeo 结构体是否到 期,如果没有到期,直接退出该函数,否则,执行 sys_timeo
结构体中对应的超时回调函数, 并从链表上删除它,然后继续检查下一个 sys_timeo 结构体,直到 sys_timeo 结构体没有超
时才退出。 tcpip_timeouts_mbox_fetch(sys_mbox_t *mbox, void **msg):
这个函数在操作系统的线 程中循环调用,主要是等待 tcpip_mbox 消息,是可阻塞的,如果在等待 tcpip_mbox
的过程中发生超时事件,则会同时执行超时事件处理,即调用超时回调函数。 LwIP 是这样子处理 的,如果已经发生超时, LwIP 就会内部调用
sys_check_timeouts()函数去检查超时的 sys_timeo 结构体并调用其对应的回调函数,
如果没有发生超时,那就一直等待消息,其等 待的时间为下一个超时时间的时间, 一举两得。 LwIP 中 tcpip 线程就是靠这种方法,即处
理了上层及底层的 tcpip_mbox 消息,同时处理了所有需要超时处理的事件
4,循环超时事件
所谓的循环超时事件,其实就是执行超时回调函数后会重新注册进入超时队列的事件。lwip定义了数组来管理所有循环超时事件。
这里比较难以理解的是循环超时事件是如何注册的以及执行完如何重新注册,先看一下源码
#define TCP_TMR_INTERVAL 250
#define IP_TMR_INTERVAL 1000
#define ARP_TMR_INTERVAL 1000
struct lwip_cyclic_timer
{
u32_t interval_ms;
lwip_cyclic_timer_handler handler;
};
const struct lwip_cyclic_timer lwip_cyclic_timers[] =
{
{TCP_TMR_INTERVAL, HANDLER(tcp_tmr)},
{IP_TMR_INTERVAL, HANDLER(ip_reass_tmr)},
{ARP_TMR_INTERVAL, HANDLER(etharp_tmr)},
}
lwip_cyclic_timer(void *arg)
{
u32_t now;
u32_t next_timeout_time;
const struct lwip_cyclic_timer *cyclic = (const struct lwip_cyclic_timer *)arg;
cyclic->handler();
now = sys_now();
next_timeout_time = (u32_t)(current_timeout_due_time + cyclic->interval_ms);
if (TIME_LESS_THAN(next_timeout_time, now))
{
sys_timeout_abs((u32_t)(now + cyclic->interval_ms), lwip_cyclic_timer, arg);
}
else
{
sys_timeout_abs(next_timeout_time, lwip_cyclic_timer, arg);
}
}
void sys_timeouts_init(void)
{
size_t i;
for(i =(LWIP_TCP ? 1 : 0);i < LWIP_ARRAYSIZE(lwip_cyclic_timers); i++)
{
sys_timeout(lwip_cyclic_timers[i].interval_ms,lwip_cyclic_timer,
LWIP_CONST_CAST(void *, &lwip_cyclic_timers[i]));
}
}
首先循环超时事件存在于lwip_cyclic_times[]数组中,使用lwip_cyclic_times()将每个超时事件注册到超时链表中,注意此时注册进入超时队列的回调函数并不是最终超时任务执行的回调函数,而是一个统一的回调函数(类似于一个选择器,lwip_cyclic_timer(),而选择器传入的参数才是真正执行的回调函数)