一. 前言
LWIP的定時(shí)器模塊,實(shí)現(xiàn)了通用的軟件定時(shí)器,用于內(nèi)部的周期事件處理,比如arp,tcp的超時(shí)等,用戶也可以使用。這一篇來分析該模塊的實(shí)現(xiàn)。
二.代碼分析
2.1源碼
源碼位于
timeouts.c
timeouts.h
會(huì)按照如下條件編譯
#if LWIP_TIMERS && !LWIP_TIMERS_CUSTOM
即LWIP_TIMERS為1 ,LWIP_TIMERS_CUSTOM為0才會(huì)編譯,也是默認(rèn)配置。
2.2數(shù)據(jù)結(jié)構(gòu)
定時(shí)器的核心數(shù)據(jù)結(jié)構(gòu)是一個(gè)單向鏈表,鏈表的節(jié)點(diǎn)如下
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構(gòu)成單向鏈表
time為絕對(duì)時(shí)間,即當(dāng)前絕對(duì)時(shí)間滯后該值則表示定時(shí)器超時(shí)需要執(zhí)行回調(diào)函數(shù)h。
arg可以傳入參數(shù),
handler_name是debug打印信息用。
2.3超時(shí)比較算法
定時(shí)器使用的是絕對(duì)時(shí)間,即定時(shí)器的time和當(dāng)前now時(shí)間比較,time<=now則表示定時(shí)器已經(jīng)超時(shí)了需要處理,否則定時(shí)器還未到時(shí)間無需處理。
但是這里會(huì)有個(gè)問題,溢出的問題,time<=now就一定表示time的時(shí)刻提前于now嗎,不一定,也可能是到了定時(shí)器值的最大值繞回了,
比如如果定時(shí)器的值是32位的,
now為0xFFFFFFFF,time為0x00000001,
time
也可能time滯后于now為2的時(shí)間 即(0x100000000-0xFFFFFFFF)+0x00000001.
我們更傾向于是后者,因?yàn)楹笳叩臅r(shí)間差更小,更符合實(shí)際情況,因?yàn)槲覀兌〞r(shí)時(shí)間一般都很小。
這里的實(shí)現(xiàn)是
#define LWIP_MAX_TIMEOUT 0x7fffffff
#define TIME_LESS_THAN(t, compare_to) ( (((u32_t)((t)-(compare_to))) > LWIP_MAX_TIMEOUT) ? 1 : 0 )
實(shí)際上用到的就是我們上面提到的思想,我們更傾向于時(shí)間差更小的為實(shí)際情況,
實(shí)際該算法還有專門的文章進(jìn)行討論,網(wǎng)上可以搜到。
即定義定時(shí)器最大范圍的一半,比如32位最大范圍是00xFFFFFFFF,共0x100000000的范圍,其一半的范圍是0x80000000,即00x7fffffff,作為基準(zhǔn),最大就只能定時(shí)器該時(shí)間,大于該時(shí)間認(rèn)為不合理,實(shí)際是繞回反向的。
這里((u32_t)((t)-(compare_to)))按照無符號(hào)32位進(jìn)行計(jì)算
如果t
如果t為1, compare_to為2則((u32_t)((t)-(compare_to)))結(jié)果為0xFFFFFFFF。
實(shí)際上就是0x100000000-0x02 + 0x01.
如果t>compare_to
t為2, compare_to為1
則((u32_t)((t)-(compare_to)))結(jié)果為0x1
該表達(dá)式即可以理解為t滯后compare_to的時(shí)間(未來時(shí)間),
更形象的理解是a-b即b需要追趕多少到a,即compare_to追趕到t需要多久,有可能繞回。
如果該滯后時(shí)間比較大,大于總時(shí)間的一半即0x7fffffff我們則認(rèn)為,實(shí)際不是滯后而是超前。
因?yàn)閮A向于時(shí)間間隔短的符合實(shí)際情況。
所以如果t比compare_to大的非常多,大于TIME_LESS_THAN,我們也認(rèn)為t不是滯后compare_to而是提前于compare_to,只是是繞回了。
1.總結(jié)
我們可以用跑圈追趕的角度來理解,即t和compare_to在環(huán)形世界賽跑,某一刻我們并不知道誰跑在前,誰跑在后,因?yàn)橛锌赡堋碧兹Α? 于是我們有一條假設(shè), t和compare_to跑的速度差異不是特別大(類似我們定時(shí)器的定時(shí)時(shí)間不是特別長),所以如果t追趕到compare_to的距離大于跑道的一半我們認(rèn)為不合理,所以認(rèn)為是compare_to追趕t。
所以該算法能工作的前提條件是定時(shí)時(shí)間不能大于LWIP_MAX_TIMEOUT。
當(dāng)然去處理查詢定時(shí)器超時(shí)的間隔也不能大于LWIP_MAX_TIMEOUT,否則由于”套多圈”無法區(qū)分。
2.4內(nèi)建定時(shí)器
定義了一個(gè)數(shù)組
const struct lwip_cyclic_timer lwip_cyclic_timers[]
提供構(gòu)建定時(shí)器的必要信息(注意不是定時(shí)器本身,而是提供定時(shí)器信息,sys_timeouts_init根據(jù)此創(chuàng)建定時(shí)器)
數(shù)組成員結(jié)構(gòu)體如下
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為定時(shí)器執(zhí)行周期,上述定時(shí)器要求是絕對(duì)時(shí)間,為什么這里是間隔時(shí)間呢,因?yàn)閚ow+間隔時(shí)間就是絕對(duì)時(shí)間了,初始化時(shí)會(huì)自動(dòng)設(shè)置。
handler是回調(diào)函數(shù)
handler_name是用于打印定時(shí)器的名字。
默認(rèn)根據(jù)宏定義了一些內(nèi)建的定時(shí)器
比如,使能了LWIP_ARP則使能該定時(shí)器,回調(diào)函數(shù)是etharp_tmr,間隔時(shí)間是1S。
用戶可以配置這些宏來進(jìn)行定時(shí)器的使能配置和周期配置。
#if LWIP_ARP
{ARP_TMR_INTERVAL, HANDLER(etharp_tmr)},
#endif /* LWIP_ARP */
#define ARP_TMR_INTERVAL 1000
初始化sys_timeouts_init時(shí)遍歷lwip_cyclic_timers
通過sys_timeout->sys_timeout_abs動(dòng)態(tài)創(chuàng)建定時(shí)器,定時(shí)器的絕對(duì)時(shí)間自動(dòng)會(huì)在now基礎(chǔ)上增加間隔(u32_t)(sys_now() + msecs);
這里i = (LWIP_TCP ? 1 : 0),如果有LWIP_TCP則從1開始, 0的TCP定時(shí)器單獨(dú)處理,因?yàn)樗恍枰偸沁\(yùn)行,沒有tcp連接就不需要該定時(shí)器了,所以手動(dòng)調(diào)用tcp_timer_needed()處理。
2.5接口代碼
sys_timeouts_sleeptime
后面定時(shí)器輪詢有分析,計(jì)算定時(shí)器鏈表中,頭定時(shí)器,離當(dāng)前時(shí)間的時(shí)間,
返回0表示頭定時(shí)器已經(jīng)超時(shí)需要處理,返回SYS_TIMEOUTS_SLEEPTIME_INFINITE表示沒有定時(shí)器,其他值為頭定時(shí)器離現(xiàn)在的時(shí)間間隔。因?yàn)槎〞r(shí)器是按照時(shí)間從小到大排列,所以只需要判斷頭定時(shí)器即可。
sys_restart_timeouts
以定時(shí)器鏈表第一個(gè)定時(shí)器為基準(zhǔn)設(shè)置為now絕對(duì)時(shí)間,后續(xù)的按照和第一個(gè)定時(shí)器的偏差設(shè)置。
在長時(shí)間沒有調(diào)用sys_check_timeouts時(shí),重新設(shè)置時(shí)基,來觸發(fā)一次時(shí)間調(diào)度。
這樣保證在長時(shí)間沒有調(diào)用sys_check_timeouts的期間導(dǎo)致的定時(shí)器沒有執(zhí)行,這時(shí)能彌補(bǔ)下執(zhí)行一次。
sys_check_timeouts
查詢定時(shí)器,從鏈表頭開始查詢,如果超時(shí)時(shí)間到則執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù),并釋放定時(shí)器。
因?yàn)橐呀?jīng)排序不需要查詢到末尾,查詢到第一個(gè)為超時(shí)的定時(shí)器即可結(jié)束,因?yàn)楹竺娴闹蹈罂隙ú粫?huì)超時(shí)。
無OS時(shí)用戶手動(dòng)調(diào)用該函數(shù)
有OS時(shí),tcpip線程自動(dòng)調(diào)用。
注意定時(shí)器都是單次的,一次執(zhí)行完后會(huì)刪除,周期執(zhí)行需要重新創(chuàng)建。
這里個(gè)人覺得每次都刪除和釋放不是很好,尤其是嵌入式平臺(tái),多了mem等操作一方面內(nèi)存碎片的問題(如果使用內(nèi)存池實(shí)現(xiàn)還好,如果共用堆管理則會(huì)有些影響,尤其堆本來就很小的資源受限平臺(tái)),一方面效率降低。
sys_untimeout
從定時(shí)器鏈表刪除一個(gè)定時(shí)器
sys_timeout->sys_timeout_abs
創(chuàng)建定時(shí)器,按照定時(shí)器值從小大到插入到鏈表
sys_timeouts_init
初始化內(nèi)建定時(shí)器,前面已經(jīng)分析過
lwip_cyclic_timer
內(nèi)建定時(shí)器回調(diào)處理
由于定時(shí)器都是單次的,所以周期定時(shí)器需要重新創(chuàng)建定時(shí)器。
內(nèi)建定時(shí)器時(shí)都是設(shè)置的該回調(diào)函數(shù)
sys_timeout(lwip_cyclic_timers[i].interval_ms, lwip_cyclic_timer, LWIP_CONST_CAST(void *, &lwip_cyclic_timers[i]));
通過參數(shù)再回調(diào)具體的不同的回調(diào)函數(shù)
const struct lwip_cyclic_timer *cyclic = (const struct lwip_cyclic_timer *)arg;
cyclic- >handler();
tcp_timer_needed/tcpip_tcp_timer
tcp定時(shí)器單獨(dú)處理,創(chuàng)建一個(gè)tcp定時(shí)器
tcpip_tcp_timer會(huì)根據(jù)是否有tcp連接來確認(rèn)是否需要重復(fù)定時(shí)器。
2.6定時(shí)器輪詢
無OS時(shí)手動(dòng)周期調(diào)用
sys_check_timeouts
有OS時(shí)在tcpip_thread線程中
TCPIP_MBOX_FETCH即tcpip_timeouts_mbox_fetch會(huì)自動(dòng)調(diào)用
sys_check_timeouts。
我們來分析下tcpip_timeouts_mbox_fetch
首先sleeptime = sys_timeouts_sleeptime(); 獲取最近一個(gè)將要超時(shí)的定時(shí)器到現(xiàn)在的時(shí)間間隔,這樣mbox_fetch時(shí)就以該間隔時(shí)間作為超時(shí)時(shí)間sleeptime,這樣如果在這個(gè)超時(shí)時(shí)間之前獲取到了mbox則處理消息,下一個(gè)循環(huán)繼續(xù)重復(fù)上述處理。否則等到超時(shí)再調(diào)用sys_check_timeouts();處理定時(shí)器。
sys_timeouts_sleeptime先要判斷是否有定時(shí)器,如果next_timeout為空則說明沒有定時(shí)器需要處理則超時(shí)時(shí)間sleeptime可以設(shè)置為無限大。
如果有定時(shí)器則只需要判斷next_timeout頭定時(shí)器得time與now比較即可,因?yàn)槎〞r(shí)器是按照time從小到大排列的,所以最先超時(shí)得肯定是頭定時(shí)器。如果next_timeout得time小于now,說明該定時(shí)器已經(jīng)超時(shí)了設(shè)置為0,后面會(huì)馬上調(diào)用sys_check_timeouts()處理。
否則計(jì)算next_timeout得time減去now為間隔時(shí)間。
也就是對(duì)應(yīng)
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;
}
如果沒有定時(shí)器sleeptime == SYS_TIMEOUTS_SLEEPTIME_INFINITE則 sys_arch_mbox_fetch(mbox, msg, 0); 參數(shù)0表示無限超時(shí)時(shí)間。
直到獲取到消息才會(huì)return,否則就一直在此等待。
這里個(gè)人覺得有個(gè)BUG,如果剛開始沒有定時(shí)器,且此時(shí)沒有消息,則在此之后新創(chuàng)建的定時(shí)器將得不到處理,因?yàn)橐恢痹谶@里等待消息了,雖然一開始基本都會(huì)有定時(shí)器所以不會(huì)進(jìn)到這里,但是邏輯上來說還是不嚴(yán)謹(jǐn)。雖然這里無限等待可以有利于效率,因?yàn)闆]有消息該線程就不執(zhí)行了,但是個(gè)人覺得設(shè)置一個(gè)固定的超時(shí)間隔可能更安全,這樣保證該線程不會(huì)卡死在這里,超過時(shí)間沒有消息也跳過重新執(zhí)行,這樣保證新創(chuàng)建的定時(shí)器能執(zhí)行,最大誤差就是該設(shè)置的固定間隔。這個(gè)間隔可以根據(jù)允許誤差和效率均衡考慮設(shè)置,這樣也不至于影響效率,也能保證定時(shí)器始終能執(zhí)行。
sleeptime已經(jīng)有定時(shí)器超時(shí)了sleeptime == 0則馬上調(diào)用sys_check_timeouts()處理。因?yàn)闆]有消息所以goto again;重復(fù),無需return。
如果sleeptime不是0也不是無限大,則按需設(shè)置超時(shí)時(shí)間
res = sys_arch_mbox_fetch(mbox, msg, sleeptime);
如果res返回超時(shí)則調(diào)用sys_check_timeouts處理定時(shí)器,goto again;重復(fù)上述過程,因?yàn)闆]有消息所以無需return。有消息則return到上一層去處理消息。
2.7DEBUG
lwipopts.h中定義LWIP_DEBUG_TIMERNAMES宏使能相關(guān)debug代碼,
否則根據(jù)LWIP_DEBUG決定
如果定義了LWIP_DEBUG則LWIP_DEBUG_TIMERNAMES為SYS_DEBUG,否則為0。
SYS_DEBUG默認(rèn)為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
以上使能相關(guān)調(diào)試代碼之后,還需要lwipopts.h中使能
TIMERS_DEBUG
按如下配置使能
#define TIMERS_DEBUG LWIP_DBG_ON
#define LWIP_DEBUG_TIMERNAMES 1
當(dāng)然也要使能DEBUG
#define LWIP_DEBUG 1
和LWIP_PLATFORM_DIAG打印的接口宏。
此時(shí)可以看到打印信息如下,可以通過打印確定定時(shí)是否正確,定時(shí)器是否工作
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
三.總結(jié)
重點(diǎn)理解定時(shí)器的超時(shí)判斷算法,
注意定時(shí)器是單次的每次超時(shí)處理完都會(huì)刪除,需要重新創(chuàng)建,這個(gè)需要注意,并且注意頻繁的創(chuàng)建和刪除對(duì)堆管理的影響。
了解內(nèi)建定時(shí)器的定時(shí)周期的配置,以及定時(shí)器的調(diào)試方法。
審核編輯 黃宇
-
以太網(wǎng)
+關(guān)注
關(guān)注
40文章
5423瀏覽量
171684 -
定時(shí)器
+關(guān)注
關(guān)注
23文章
3248瀏覽量
114781 -
LwIP
+關(guān)注
關(guān)注
2文章
86瀏覽量
27166
發(fā)布評(píng)論請先 登錄
相關(guān)推薦
評(píng)論