一. 前言
LWIP的定時器模塊,實現了通用的軟件定時器,用于內部的周期事件處理,比如arp,tcp的超時等,用戶也可以使用。這一篇來分析該模塊的實現。
二.代碼分析
2.1源碼
源碼位于
timeouts.c
timeouts.h
會按照如下條件編譯
#if LWIP_TIMERS && !LWIP_TIMERS_CUSTOM
即LWIP_TIMERS為1 ,LWIP_TIMERS_CUSTOM為0才會編譯,也是默認配置。
2.2數據結構
定時器的核心數據結構是一個單向鏈表,鏈表的節點如下
struct sys_timeo {
struct sys_timeo *next;
u32_t time;
sys_timeout_handler h;
void *arg;
#if LWIP_DEBUG_TIMERNAMES
const char* handler_name;
#endif /* LWIP_DEBUG_TIMERNAMES */
};
Next構成單向鏈表
time為絕對時間,即當前絕對時間滯后該值則表示定時器超時需要執行回調函數h。
arg可以傳入參數,
handler_name是debug打印信息用。
2.3超時比較算法
定時器使用的是絕對時間,即定時器的time和當前now時間比較,time<=now則表示定時器已經超時了需要處理,否則定時器還未到時間無需處理。
但是這里會有個問題,溢出的問題,time<=now就一定表示time的時刻提前于now嗎,不一定,也可能是到了定時器值的最大值繞回了,
比如如果定時器的值是32位的,
now為0xFFFFFFFF,time為0x00000001,
time
也可能time滯后于now為2的時間 即(0x100000000-0xFFFFFFFF)+0x00000001.
我們更傾向于是后者,因為后者的時間差更小,更符合實際情況,因為我們定時時間一般都很小。
這里的實現是
#define LWIP_MAX_TIMEOUT 0x7fffffff
#define TIME_LESS_THAN(t, compare_to) ( (((u32_t)((t)-(compare_to))) > LWIP_MAX_TIMEOUT) ? 1 : 0 )
實際上用到的就是我們上面提到的思想,我們更傾向于時間差更小的為實際情況,
實際該算法還有專門的文章進行討論,網上可以搜到。
即定義定時器最大范圍的一半,比如32位最大范圍是00xFFFFFFFF,共0x100000000的范圍,其一半的范圍是0x80000000,即00x7fffffff,作為基準,最大就只能定時器該時間,大于該時間認為不合理,實際是繞回反向的。
這里((u32_t)((t)-(compare_to)))按照無符號32位進行計算
如果t
如果t為1, compare_to為2則((u32_t)((t)-(compare_to)))結果為0xFFFFFFFF。
實際上就是0x100000000-0x02 + 0x01.
如果t>compare_to
t為2, compare_to為1
則((u32_t)((t)-(compare_to)))結果為0x1
該表達式即可以理解為t滯后compare_to的時間(未來時間),
更形象的理解是a-b即b需要追趕多少到a,即compare_to追趕到t需要多久,有可能繞回。
如果該滯后時間比較大,大于總時間的一半即0x7fffffff我們則認為,實際不是滯后而是超前。
因為傾向于時間間隔短的符合實際情況。
所以如果t比compare_to大的非常多,大于TIME_LESS_THAN,我們也認為t不是滯后compare_to而是提前于compare_to,只是是繞回了。
1.總結
我們可以用跑圈追趕的角度來理解,即t和compare_to在環形世界賽跑,某一刻我們并不知道誰跑在前,誰跑在后,因為有可能”套圈”, 于是我們有一條假設, t和compare_to跑的速度差異不是特別大(類似我們定時器的定時時間不是特別長),所以如果t追趕到compare_to的距離大于跑道的一半我們認為不合理,所以認為是compare_to追趕t。
所以該算法能工作的前提條件是定時時間不能大于LWIP_MAX_TIMEOUT。
當然去處理查詢定時器超時的間隔也不能大于LWIP_MAX_TIMEOUT,否則由于”套多圈”無法區分。
2.4內建定時器
定義了一個數組
const struct lwip_cyclic_timer lwip_cyclic_timers[]
提供構建定時器的必要信息(注意不是定時器本身,而是提供定時器信息,sys_timeouts_init根據此創建定時器)
數組成員結構體如下
struct lwip_cyclic_timer {
u32_t interval_ms;
lwip_cyclic_timer_handler handler;
#if LWIP_DEBUG_TIMERNAMES
const char* handler_name;
#endif /* LWIP_DEBUG_TIMERNAMES */
};
interval_ms為定時器執行周期,上述定時器要求是絕對時間,為什么這里是間隔時間呢,因為now+間隔時間就是絕對時間了,初始化時會自動設置。
handler是回調函數
handler_name是用于打印定時器的名字。
默認根據宏定義了一些內建的定時器
比如,使能了LWIP_ARP則使能該定時器,回調函數是etharp_tmr,間隔時間是1S。
用戶可以配置這些宏來進行定時器的使能配置和周期配置。
#if LWIP_ARP
{ARP_TMR_INTERVAL, HANDLER(etharp_tmr)},
#endif /* LWIP_ARP */
#define ARP_TMR_INTERVAL 1000
初始化sys_timeouts_init時遍歷lwip_cyclic_timers
通過sys_timeout->sys_timeout_abs動態創建定時器,定時器的絕對時間自動會在now基礎上增加間隔(u32_t)(sys_now() + msecs);
這里i = (LWIP_TCP ? 1 : 0),如果有LWIP_TCP則從1開始, 0的TCP定時器單獨處理,因為它不需要總是運行,沒有tcp連接就不需要該定時器了,所以手動調用tcp_timer_needed()處理。
2.5接口代碼
sys_timeouts_sleeptime
后面定時器輪詢有分析,計算定時器鏈表中,頭定時器,離當前時間的時間,
返回0表示頭定時器已經超時需要處理,返回SYS_TIMEOUTS_SLEEPTIME_INFINITE表示沒有定時器,其他值為頭定時器離現在的時間間隔。因為定時器是按照時間從小到大排列,所以只需要判斷頭定時器即可。
sys_restart_timeouts
以定時器鏈表第一個定時器為基準設置為now絕對時間,后續的按照和第一個定時器的偏差設置。
在長時間沒有調用sys_check_timeouts時,重新設置時基,來觸發一次時間調度。
這樣保證在長時間沒有調用sys_check_timeouts的期間導致的定時器沒有執行,這時能彌補下執行一次。
sys_check_timeouts
查詢定時器,從鏈表頭開始查詢,如果超時時間到則執行對應的回調函數,并釋放定時器。
因為已經排序不需要查詢到末尾,查詢到第一個為超時的定時器即可結束,因為后面的值更大肯定不會超時。
無OS時用戶手動調用該函數
有OS時,tcpip線程自動調用。
注意定時器都是單次的,一次執行完后會刪除,周期執行需要重新創建。
這里個人覺得每次都刪除和釋放不是很好,尤其是嵌入式平臺,多了mem等操作一方面內存碎片的問題(如果使用內存池實現還好,如果共用堆管理則會有些影響,尤其堆本來就很小的資源受限平臺),一方面效率降低。
sys_untimeout
從定時器鏈表刪除一個定時器
sys_timeout->sys_timeout_abs
創建定時器,按照定時器值從小大到插入到鏈表
sys_timeouts_init
初始化內建定時器,前面已經分析過
lwip_cyclic_timer
內建定時器回調處理
由于定時器都是單次的,所以周期定時器需要重新創建定時器。
內建定時器時都是設置的該回調函數
sys_timeout(lwip_cyclic_timers[i].interval_ms, lwip_cyclic_timer, LWIP_CONST_CAST(void *, &lwip_cyclic_timers[i]));
通過參數再回調具體的不同的回調函數
const struct lwip_cyclic_timer *cyclic = (const struct lwip_cyclic_timer *)arg;
cyclic- >handler();
tcp_timer_needed/tcpip_tcp_timer
tcp定時器單獨處理,創建一個tcp定時器
tcpip_tcp_timer會根據是否有tcp連接來確認是否需要重復定時器。
2.6定時器輪詢
無OS時手動周期調用
sys_check_timeouts
有OS時在tcpip_thread線程中
TCPIP_MBOX_FETCH即tcpip_timeouts_mbox_fetch會自動調用
sys_check_timeouts。
我們來分析下tcpip_timeouts_mbox_fetch
首先sleeptime = sys_timeouts_sleeptime(); 獲取最近一個將要超時的定時器到現在的時間間隔,這樣mbox_fetch時就以該間隔時間作為超時時間sleeptime,這樣如果在這個超時時間之前獲取到了mbox則處理消息,下一個循環繼續重復上述處理。否則等到超時再調用sys_check_timeouts();處理定時器。
sys_timeouts_sleeptime先要判斷是否有定時器,如果next_timeout為空則說明沒有定時器需要處理則超時時間sleeptime可以設置為無限大。
如果有定時器則只需要判斷next_timeout頭定時器得time與now比較即可,因為定時器是按照time從小到大排列的,所以最先超時得肯定是頭定時器。如果next_timeout得time小于now,說明該定時器已經超時了設置為0,后面會馬上調用sys_check_timeouts()處理。
否則計算next_timeout得time減去now為間隔時間。
也就是對應
if (sleeptime == SYS_TIMEOUTS_SLEEPTIME_INFINITE) {
UNLOCK_TCPIP_CORE();
sys_arch_mbox_fetch(mbox, msg, 0);
LOCK_TCPIP_CORE();
return;
} else if (sleeptime == 0) {
sys_check_timeouts();
/* We try again to fetch a message from the mbox. */
goto again;
}
如果沒有定時器sleeptime == SYS_TIMEOUTS_SLEEPTIME_INFINITE則 sys_arch_mbox_fetch(mbox, msg, 0); 參數0表示無限超時時間。
直到獲取到消息才會return,否則就一直在此等待。
這里個人覺得有個BUG,如果剛開始沒有定時器,且此時沒有消息,則在此之后新創建的定時器將得不到處理,因為一直在這里等待消息了,雖然一開始基本都會有定時器所以不會進到這里,但是邏輯上來說還是不嚴謹。雖然這里無限等待可以有利于效率,因為沒有消息該線程就不執行了,但是個人覺得設置一個固定的超時間隔可能更安全,這樣保證該線程不會卡死在這里,超過時間沒有消息也跳過重新執行,這樣保證新創建的定時器能執行,最大誤差就是該設置的固定間隔。這個間隔可以根據允許誤差和效率均衡考慮設置,這樣也不至于影響效率,也能保證定時器始終能執行。
sleeptime已經有定時器超時了sleeptime == 0則馬上調用sys_check_timeouts()處理。因為沒有消息所以goto again;重復,無需return。
如果sleeptime不是0也不是無限大,則按需設置超時時間
res = sys_arch_mbox_fetch(mbox, msg, sleeptime);
如果res返回超時則調用sys_check_timeouts處理定時器,goto again;重復上述過程,因為沒有消息所以無需return。有消息則return到上一層去處理消息。
2.7DEBUG
lwipopts.h中定義LWIP_DEBUG_TIMERNAMES宏使能相關debug代碼,
否則根據LWIP_DEBUG決定
如果定義了LWIP_DEBUG則LWIP_DEBUG_TIMERNAMES為SYS_DEBUG,否則為0。
SYS_DEBUG默認為LWIP_DBG_OFF,可以該為LWIP_DBG_ON
#ifndef LWIP_DEBUG_TIMERNAMES
#ifdef LWIP_DEBUG
#define LWIP_DEBUG_TIMERNAMES SYS_DEBUG
#else /* LWIP_DEBUG */
#define LWIP_DEBUG_TIMERNAMES 0
#endif /* LWIP_DEBUG*/
#endif
以上使能相關調試代碼之后,還需要lwipopts.h中使能
TIMERS_DEBUG
按如下配置使能
#define TIMERS_DEBUG LWIP_DBG_ON
#define LWIP_DEBUG_TIMERNAMES 1
當然也要使能DEBUG
#define LWIP_DEBUG 1
和LWIP_PLATFORM_DIAG打印的接口宏。
此時可以看到打印信息如下,可以通過打印確定定時是否正確,定時器是否工作
sct calling h=ip_reass_tmr t=0 arg=0x2001548c
tcpip: ip_reass_tmr()
sys_timeout: 0x28213e48 abs_time=6223 handler=ip_reass_tmr arg=0x2001548c
sct calling h=etharp_tmr t=0 arg=0x20015498
tcpip: etharp_tmr()
sys_timeout: 0x28213e68 abs_time=6224 handler=etharp_tmr arg=0x20015498
sct calling h=ip_reass_tmr t=0 arg=0x2001548c
tcpip: ip_reass_tmr()
sys_timeout: 0x28213e48 abs_time=7223 handler=ip_reass_tmr arg=0x2001548c
sct calling h=etharp_tmr t=0 arg=0x20015498
tcpip: etharp_tmr()
sys_timeout: 0x28213e68 abs_time=7224 handler=etharp_tmr arg=0x20015498
sct calling h=ip_reass_tmr t=0 arg=0x2001548c
tcpip: ip_reass_tmr()
sys_timeout: 0x28213e48 abs_time=8223 handler=ip_reass_tmr arg=0x2001548c
sct calling h=ip_reass_tmr t=0 arg=0x2001548c
tcpip: ip_reass_tmr()
sys_timeout: 0x28213e48 abs_time=16223 handler=ip_reass_tmr arg=0x2001548c
sct calling h=etharp_tmr t=0 arg=0x20015498
tcpip: etharp_tmr()
sys_timeout: 0x28213e68 abs_time=16224 handler=etharp_tmr arg=0x20015498
三.總結
重點理解定時器的超時判斷算法,
注意定時器是單次的每次超時處理完都會刪除,需要重新創建,這個需要注意,并且注意頻繁的創建和刪除對堆管理的影響。
了解內建定時器的定時周期的配置,以及定時器的調試方法。
審核編輯 黃宇
-
以太網
+關注
關注
40文章
5385瀏覽量
171161 -
定時器
+關注
關注
23文章
3241瀏覽量
114511 -
LwIP
+關注
關注
2文章
86瀏覽量
27105
發布評論請先 登錄
相關推薦
評論