LwIP 之四 超时处理/定时器(timeouts.c/h)
??目前,網絡上多數文章所使用的LwIP版本為1.4.1。最新版本為2.0.3。從1.4.1到2.0.3(貌似從2.0.0開始),LwIP的源碼有了一定的變化,甚至于源碼的文件結構也不一樣,內部的一些實現源文件也被更新和替換了。
簡介
??在LwIP中很多時候都要用到超時處理,超時處理的實現是 TCP/IP 協議棧中一個重要部分。LwIP為每個與外界網絡連接的任務都有設定了 timeout 屬性,即等待超時時間。超時處理的相關代碼實現在timeouts.c/h中。
??在舊版本得LwIP中,超時處理被稱之為定時器,但是,在最新版的LwIP中,原來的timer已經被刪除,轉而使用了timeouts來代替。實際的實現上也有一定的區別。
??在該文件中,針對有誤操作系統(宏NO_SYS),對外提供的函數是不同的,下面看看這個文件的總結構。
內部定時器
??在timeouts.c/h中,第一部分便是一個被稱為lwip_cyclic_timer的結構。LwIP使用該結構存放了其內部使用的循環定時器。這些定時器在LwIP初始化時通過函數void sys_timeouts_init(void)調用定時器注冊函數void sys_timeout(u32_t msecs, sys_timeout_handler handler, void *arg)注冊進入超時管理鏈表中。在2.0.0之前的版本中,是沒有該部分的。該部分被分開在了LwIP內部,現在進行了統一處理。lwip_cyclic_timer的結構如下:
/** Function prototype for a stack-internal timer function that has to be* called at a defined interval */ typedef void (* lwip_cyclic_timer_handler)(void);/** This struct contains information about a stack-internal timer functionthat has to be called at a defined interval */ struct lwip_cyclic_timer {u32_t interval_ms;lwip_cyclic_timer_handler handler; #if LWIP_DEBUG_TIMERNAMESconst char* handler_name; #endif /* LWIP_DEBUG_TIMERNAMES */ };??在timeouts.c中,有如下全局變量,存放了LwIP內部使用的各定時器
/** This array contains all stack-internal cyclic timers. To get the number of* timers, use LWIP_ARRAYSIZE() */ const struct lwip_cyclic_timer lwip_cyclic_timers[] = { #if LWIP_TCP/* The TCP timer is a special case: it does not have to run always andis triggered to start from TCP using tcp_timer_needed() */{TCP_TMR_INTERVAL, HANDLER(tcp_tmr)}, #endif /* LWIP_TCP */ #if LWIP_IPV4 #if IP_REASSEMBLY{IP_TMR_INTERVAL, HANDLER(ip_reass_tmr)}, #endif /* IP_REASSEMBLY */ #if LWIP_ARP{ARP_TMR_INTERVAL, HANDLER(etharp_tmr)}, #endif /* LWIP_ARP */ #if LWIP_DHCP{DHCP_COARSE_TIMER_MSECS, HANDLER(dhcp_coarse_tmr)},{DHCP_FINE_TIMER_MSECS, HANDLER(dhcp_fine_tmr)}, #endif /* LWIP_DHCP */ #if LWIP_AUTOIP{AUTOIP_TMR_INTERVAL, HANDLER(autoip_tmr)}, #endif /* LWIP_AUTOIP */ #if LWIP_IGMP{IGMP_TMR_INTERVAL, HANDLER(igmp_tmr)}, #endif /* LWIP_IGMP */ #endif /* LWIP_IPV4 */ #if LWIP_DNS{DNS_TMR_INTERVAL, HANDLER(dns_tmr)}, #endif /* LWIP_DNS */ #if LWIP_IPV6{ND6_TMR_INTERVAL, HANDLER(nd6_tmr)}, #if LWIP_IPV6_REASS{IP6_REASS_TMR_INTERVAL, HANDLER(ip6_reass_tmr)}, #endif /* LWIP_IPV6_REASS */ #if LWIP_IPV6_MLD{MLD6_TMR_INTERVAL, HANDLER(mld6_tmr)}, #endif /* LWIP_IPV6_MLD */ #endif /* LWIP_IPV6 */ };該部分通關函數void sys_timeouts_init(void);將內部使用的延時定時器注冊進入鏈表中,如下圖:
超時處理鏈表
??超時定時器是按鏈表的形式進行組織的,且按時間長短進行排序,時間最短的永遠在最前面。使用全局變量static struct sys_timeo *next_timeout;指示超時鏈表,該指針即為超時鏈表的頭。超時鏈表需要通過函數void sys_timeout(u32_t msecs, sys_timeout_handler handler, void *arg)進行注冊。鏈表的節點使用如下結構體表示:
/** Function prototype for a timeout callback function. Register such a function* using sys_timeout().** @param arg Additional argument to pass to the function - set up by sys_timeout()*/ typedef void (* sys_timeout_handler)(void *arg);struct sys_timeo {struct sys_timeo *next;u32_t time;sys_timeout_handler h;void *arg; #if LWIP_DEBUG_TIMERNAMESconst char* handler_name; #endif /* LWIP_DEBUG_TIMERNAMES */ };超時定時器注冊
??下面詳細分析一下void sys_timeout(u32_t msecs, sys_timeout_handler handler, void *arg)是如何對超時函數進行注冊的。源碼很簡單。
u32_t now, diff;/* 1. 申請節點內存 */timeout = (struct sys_timeo *)memp_malloc(MEMP_SYS_TIMEOUT);if (timeout == NULL) {LWIP_ASSERT("sys_timeout: timeout != NULL, pool MEMP_SYS_TIMEOUT is empty", timeout != NULL);return;}/* 2.計算差值,至于為什么要額外搞個差值,暫時還沒搞明白!!! */now = sys_now();if (next_timeout == NULL) {diff = 0;timeouts_last_time = now;} else {diff = now - timeouts_last_time;}/* 3. 節點各變量賦值 */timeout->next = NULL;timeout->h = handler;timeout->arg = arg;timeout->time = msecs + diff; #if LWIP_DEBUG_TIMERNAMEStimeout->handler_name = handler_name;LWIP_DEBUGF(TIMERS_DEBUG, ("sys_timeout: %p msecs=%"U32_F" handler=%s arg=%p\n",(void *)timeout, msecs, handler_name, (void *)arg)); #endif /* LWIP_DEBUG_TIMERNAMES */ /* 4. 如果創建的是第一個定時器,則不用特殊處理,next_timeout是一個全局指針,指向定時器鏈表中第一個定時器 */if (next_timeout == NULL) {next_timeout = timeout;return;}/* 4. 從第二個定時器開始就要添加到鏈表中,添加原則是定時最短的定時器始終在前面。如果新添加的定時器時長小于當前鏈首定時器,則新添加的定時器成為鏈首,舊的鏈首定時器的定時值要減去新鏈首定時器定時值, 如果新添加的定時器大于等于當前鏈首定時器的時長,則要在整個鏈表里逐個比較, 最終插入到合適位置,當然其后定時器的定時值也要進行調整 */if (next_timeout->time > msecs) {next_timeout->time -= msecs;timeout->next = next_timeout;next_timeout = timeout;} else {for (t = next_timeout; t != NULL; t = t->next) {timeout->time -= t->time;if (t->next == NULL || t->next->time > timeout->time) {if (t->next != NULL) {t->next->time -= timeout->time;} else if (timeout->time > msecs) {/* If this is the case, 'timeouts_last_time' and 'now' differs too much.This can be due to sys_check_timeouts() not being called at the righttimes, but also when stopping in a breakpoint. Anyway, let's assumethis is not wanted, so add the first timer's time instead of 'diff' */timeout->time = msecs + next_timeout->time;}timeout->next = t->next;t->next = timeout;break;}}}下面舉例說明(由于在處理鏈表時,僅time 有用,因此下面的的其他參數用字母代替):
??從上面的舉例可以看出,如果新添加的定時器比頭結點(next_timeout指向的第一個節點)的時間短,則直接往鏈表頭插入,同時對頭結點(next_timeout指向的第一個節點)的時間進行調整,后續節點的時間不動。
??如果新添加的定時器比頭結點(next_timeout指向的第一個節點)的時間長,則需要遍歷鏈表,查找合適位置(比其短的定時器之后,比其長的定時器之前)插入,其后定時器的定時值也要進行調整,其前的定時器無需調整。
超時定時器刪除
??從超時鏈表中刪除指定的定時器時通過函數void sys_untimeout(sys_timeout_handler handler, void *arg)來完成的。源碼很簡單。
void sys_untimeout(sys_timeout_handler handler, void *arg) {struct sys_timeo *prev_t, *t;/* 超時鏈表為空的判斷 */if (next_timeout == NULL) {return;}/* 從鏈表頭開始遍歷這個鏈表 */for (t = next_timeout, prev_t = NULL; t != NULL; prev_t = t, t = t->next) {if ((t->h == handler) && (t->arg == arg)) { /* 條件匹配 *//* We have a match *//* Unlink from previous in list */if (prev_t == NULL) {next_timeout = t->next;} else {prev_t->next = t->next;}/* If not the last one, add time of this one back to next */if (t->next != NULL) {t->next->time += t->time;}memp_free(MEMP_SYS_TIMEOUT, t); /* 釋放節點資源 */return;}}return; }超時定時器檢查
??不管是否有os支持,超時定時器都可以使用。lwip中如下兩個函數可以實現對超時的處理:
- void sys_check_timeouts(void):裸機應用程序在外部周期性調用該函數,每次進來檢查定時器鏈表上定時最短的定時器是否到期,如果沒有到期,直接退出該函數,否則,執行該定時器回調函數,并從鏈表上刪除該定時器,然后繼續檢查下一個定時器,直到沒有一個定時器到期退出。
- void sys_timeouts_mbox_fetch(sys_mbox_t *mbox, void **msg):這個函數可在os線程中循環調用,主要是等待mbox消息,并可阻塞,如果等待mbox時超時,則會同時執行超時事件處理,即調用定時器回調函數,如果一直沒有mbox消息,則會永久性地循環將所有超時定時器檢查一遍(內部調用了void sys_check_timeouts(void)),一舉兩得。
??LwIP中tcpip線程就是靠這種方法,即處理了上層及底層的mbox消息,同時處理了所有需要定時處理的事件。
在檢查超時定時器鏈表時,對于已經超時的則進行刪除,下面以添加時的例子說明一下檢查函數:
- 第一次檢查:
- 第二次檢查:
- 第三次檢查:
總結
還有部分沒弄明白,后面更新!
總結
以上是生活随笔為你收集整理的LwIP 之四 超时处理/定时器(timeouts.c/h)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: FreeRTOS 之 在Cortex-M
- 下一篇: LwIP 之五 详解动态内存管理 内存堆