做者:LouisozZlinux
日期:2018.08.29編程
因爲在 Linux 系統下一個進程只能設置一個時鐘定時器,因此當應用須要有多個定時器來共同管理程序運行時,就須要自行實現多定時器管理。數據結構
本文就是基於這種需求,在實際編碼工做的基礎上總結而出,但願跟你們共享,也歡迎你們討論指正。多線程
在一個進程只能有一個定時器的前提條件下,想要實現多定時器,就得用到那個進程能利用的惟一的定時器,這個定時器是由操做系統提供的,經過系統提供的接口來設置,經常使用的有 alarm() 和 setitimer(),不論用什麼,後文統一稱做系統定時接口,這兩個接口的區別在不少博客裏都有,不怎麼清楚的能夠自行搜索,這裏就再也不贅述(我比較懶,打字多對腎很差)。經過它們產生的定時信號做爲基準時間,來管理實現多定時器。函數
舉個栗子,糖炒板栗。利用系統定時接口設置了基準定時器,基準定時器每秒產生一個 SIGALRM 信號(系統時鐘的超時時間到了以後會向進程發送信號以通知定時超時,alarm() 和 setitimer() 都是向進程發送 SIGALRM 信號,關於 Linux ‘信號’ 的內容,能夠參考 《UNIX環境高級編程》),產生兩個 SIGALRM 信號的時間間隔,就是多定時器的基準時間。固然,上述的基準時間是一秒,若是你是每隔 50ms 產生一個 SIGALRM 信號,那麼多定時器的基準時間就是 50ms 。當有了基準時間以後,就能夠對它進行管理,能夠設置多個定時任務,現有兩個定時任務,Timer1_Task , Timer2_Task, 其中 Timer1_Task 的定時時長爲 10 個基準時間,Timer2_Task 爲 15 個基準時間,則每產生 10 個 SIGALRM 信號,就表示 Timer1_Task 定時器超時到達,執行一次 Timer1_Task 的超時任務,每產生 15 個 SIGALRM 信號,則執行一次 Timer2_Task 超時任務,當產生的 SIGALRM 信號個數是 30 (10 和 15 的最小公倍數),則 Timer1_Task 和 Timer2_Task 的超時任務都要被執行。ui
好了,原理講完了,下面就是本文的重點了。this
————————————— 說重點專用閹割線 —————————————編碼
————————————— 說重點專用分割線 —————————————spa
由一個全局鏈表 g_pTimeoutCheckListHead 來管理超時任務。鏈表的每一個節點是一個 tMultiTimer 結構體:操作系統
typedef void TimeoutCallBack(void*); //回調函數格式 typedef struct tMultiTimer { uint8_t nTimerID; //設置宏定義 uint32_t nInterval; //定時時長 uint32_t nTimeStamp; //時間戳 bool bIsSingleUse; //是否單次使用 bool bIsOverflow; //用於解決計數溢出問題 TimeoutCallBack *pTimeoutCallbackfunction; //回調函數 void* pTimeoutCallbackParameter; //回調函數參數 struct tMultiTimer* pNextTimer; //雙向鏈表後驅指針 struct tMultiTimer* pPreTimer; //雙向鏈表前驅指針 struct tMultiTimer* pNextHandle; //二維鏈表相同超時Timer節點 }tMultiTimer; tMultiTimer* g_pTimeoutCheckListHead; //管理多定時器的全局鏈表 bool g_bIs_g_nAbsoluteTimeOverFlow; //基準時間計次器溢出標誌位 uint32_t g_nAbsoluteTime; //基準時間計次器
(各個成員變量的意義在後文會逐一介紹,客官莫急)
這個是一個二維雙向鏈表,第一維根據時間戳,即絕對時間,按照前後順序鏈接每個 tMultiTimer 節點,當有多個超時任務的超時時刻是相同的時候,只有一個節點位於第一維,其他接在上一個相同超時時刻 tMultiTimer 節點的 pNextHandle 上,圖示以下:
首先須要調用系統定時接口,設置進程的定時器,產生 SIGALRM 信號,每一次 SIGALRM 到來時,全局的基準時間計次器 g_nAbsoluteTime 自加,因爲 g_nAbsoluteTime 是無符號類型,當其溢出時,是回到 0 ,每次溢出就把 g_bIs_g_nAbsoluteTimeOverFlow 取反。
每一個 tMultiTimer 節點都有 : uint32_t nInterval; //定時時長 uint32_t nTimeStamp; //時間戳
其中 nTimeStamp 這個值,是由 nInterval + g_nAbsoluteTime 計算而來,在把這個節點加入到全局鏈表的時刻計算的 ,這個和做爲超時的絕對時間保存在結構體中,當計算的和溢出時,bIsOverflow 取反。經過這兩個溢出標誌位,能夠用來解決溢出以後判斷是否超時的問題,具體以下:
每一次基準時間超時,就檢查鏈表的第一個節點的超時時間 nTimeStamp 是否小於全局絕對時間 g_nAbsoluteTime ,若是 g_bIs_g_nAbsoluteTimeOverFlow 與 bIsOverflow 不相等,則鏈表第一個節點的超時時間必定未到達,由於 bIsOverflow 的取反操做必定是先於 g_bIs_g_nAbsoluteTimeOverFlow ,若是同樣則比較數值大小(初始化的時候兩個溢出標誌位是同樣的)。當全局絕對時間大於等於第一個節點的時間戳,則把該節點及其 pNextHandle 指向的第二維鏈表取下,並更新 g_pTimeoutCheckListHead,而後依次執行所取下鏈表的回調函數。執行完以後(或者以前,根據實際狀況定),判斷 bIsSingleUse 成員變量,若是爲 true 則表示是單次的計數器,僅執行一次,執行完回調以後則定時任務完成。若是是 false ,怎表示是定時任務,則從新執行一次添加超時任務。(添加超時任務看下一節)
添加超時任務(添加一個 tMultiTimer 節點到全局鏈表 g_pTimeoutCheckListHead 中)的時候,指定超時時長,即間隔多少個基準時間,賦值給這個任務的成員變量 nInterval ,而後計算
nTimeStamp = nInterval + g_nAbsoluteTime;
if(nTimeStamp < g_nAbsoluteTime) bIsOverflow = ~bIsOverflow;
接着搜索 g_pTimeoutCheckListHead ,若是有相同時間戳,則添加到其 pNextHandle 指向位置,若是沒有相同時間戳節點,找到比要插入的節點時間戳大的節點,而後把當前節點插入到其前方。
對於鏈表中已經有相同 ID 的 tMultiTimer 節點的狀況,再次添加則表示更新該定時任務,取消以前的定時任務從新插入到鏈表中。
直接把對應 ID 的 tMultiTimer 節點從 g_pTimeoutCheckListHead 鏈表中摘掉便可。
g_pTimeoutCheckListHead 鏈表中的 tMultiTimer 節點數量,是總共設置的超時任務數量,假設爲 n,添加一個超時任務(節點)的最壞狀況是遍歷 n 個節點。
檢查是否有任務超時所用的時間是常數時間,只檢查第一個節點。
對於定時任務的再次插入問題,若是定時任務間隔時間越短,其反覆被插入的次數越多,可是因爲定時時間短,因此在鏈表中的插入位置也就越靠前,將快速找到插入點;若是定時任務間隔時間越長,越可能遍歷整個鏈表在末尾插入,可是因爲間隔時間長,重複插入的頻率則很低。
與一種簡單的定時器實現相比較:
if(g_nAbsoluteTime % 4) { Timer_Task_1(); } if(g_nAbsoluteTime % 17) { Timer_Task_2(); }
這種簡單實現來講,
1:每次添加、取消一個定時任務都須要修改定時器源碼,複用性不高。
2:每次檢查是否有任務超時須要遍歷 n 個定時任務。
我在實際項目中使用的環境是多線程的,有四個,我把多定時器管理放在了單獨的一個線程裏。因爲系統定時器接口產生的信號是發送給進程的,因此全部的線程都共享這個鬧鐘信號。一開始我是這麼想的,定時器的默認動做是殺死進程,那麼給每一個線程添加信號捕捉函數,這樣的話鬧鐘信號到了以後不論是那個線程接管了,都能到我指定的處理函數去,但是實際狀況並不是如此,進程仍然會被殺死。
後面我用了線程信號屏蔽,把非定時器線程都設置了信號屏蔽字,即鬧鐘信號不被別的線程可見,這樣才能正常運行,至於第一種方法爲什麼不行,如今我尚未找到緣由,仍是對 Linux 的信號機制不熟,之後看有時間的話把這裏搞懂吧。
main.c
//配置信號集 sigset_t g_sigset_mask; sigemptyset(&g_sigset_mask); sigaddset(&g_sigset_mask,SIGALRM);
other_thread.c
sigset_t old_sig_mask;
if(err = pthread_sigmask(SIG_SETMASK,&g_sigset_mask,&old_sig_mask) != 0) { // pthread_sigmask 設置信號屏蔽 return ; }
mulitimer_thread
void* MultiTimer_thread(void *parameter) { int err,signo; struct itimerval new_time_value,old_time_value; new_time_value.it_interval.tv_sec = 0; new_time_value.it_interval.tv_usec = 1000; new_time_value.it_value.tv_sec = 0; new_time_value.it_value.tv_usec = 1; setitimer(ITIMER_REAL, &new_time_value,NULL); for(;;) { err = sigwait(&g_sigset_mask,&signo);//信號捕捉 if(err != 0) { return ; } if(signo == SIGALRM) { SYSTimeoutHandler(signo); } } return ((void*)0); }
multiTimer.c
#include "multiTimer.h" /** * @function 把一個定時任務添加到定時檢測鏈表中 * @parameter 一個定時器對象,能夠由全局變量 g_aSPPMultiTimer 經過 TIMER_ID 映射獲得 */ static void AddTimerToCheckList(tMultiTimer* pTimer) { tMultiTimer* pEarliestTimer = NULL; tMultiTimer* pEarliestTimer_pre = NULL; CDebugAssert(pTimer->nInterval != 0); pTimer->nTimeStamp = g_nAbsoluteTime + pTimer->nInterval; if(pTimer->nTimeStamp < g_nAbsoluteTime) pTimer->bIsOverflow = !(pTimer->bIsOverflow); if(g_pTimeoutCheckListHead == NULL) { g_pTimeoutCheckListHead = pTimer; g_pTimeoutCheckListHead->pNextTimer = NULL; g_pTimeoutCheckListHead->pPreTimer = NULL; g_pTimeoutCheckListHead->pNextHandle = NULL; return; } else { pEarliestTimer = g_pTimeoutCheckListHead; while(pEarliestTimer != NULL) { //若是超時時間小於新加的timer則直接跳過; if((pEarliestTimer->bIsOverflow != pTimer->bIsOverflow) || (pEarliestTimer->nTimeStamp < pTimer->nTimeStamp)) { pEarliestTimer_pre = pEarliestTimer; pEarliestTimer = pEarliestTimer->pNextTimer; } else { if(pEarliestTimer->nTimeStamp == pTimer->nTimeStamp) //超時時刻相等,直接添加到相同時刻處理列表的列表頭 { pTimer->pNextHandle = pEarliestTimer->pNextHandle; pEarliestTimer->pNextHandle = pTimer; return; } else //找到了超時時刻大於新加入timer的第一個節點 { if(pEarliestTimer->pPreTimer == NULL) //新加入的是最先到達超時時刻的,添加到鏈表頭 { pEarliestTimer->pPreTimer = pTimer; pTimer->pNextTimer = pEarliestTimer; pTimer->pPreTimer = NULL; pTimer->pNextHandle = NULL; g_pTimeoutCheckListHead = pTimer; return; } else //中間節點 { pEarliestTimer->pPreTimer->pNextTimer = pTimer; pTimer->pNextTimer = pEarliestTimer; pTimer->pPreTimer = pEarliestTimer->pPreTimer; pEarliestTimer->pPreTimer = pTimer; pTimer->pNextHandle = NULL; return; } } } } if(pEarliestTimer == NULL) //新加入的timer超時時間是最晚的那個 { pEarliestTimer_pre->pNextTimer = pTimer; pTimer->pPreTimer = pEarliestTimer_pre; pTimer->pNextTimer = NULL; pTimer->pNextHandle = NULL; } return; } } /** * @function 設置一個定時任務,指定超時間隔與回調函數,當超時到來,自動執行回調 * @parameter1 TIMER_ID * @parameter2 超時間隔時間 * @parameter3 是不是一次性定時任務 * @parameter4 回調函數,注意,回調函數的函數形式 void function(void*); * @parameter5 void* 回調函數的參數,建議用結構體強轉成 void*,在回調函數中再強轉回來 * @return 錯誤碼 */ uint8_t SetTimer(uint8_t nTimerID,uint32_t nInterval,bool bIsSingleUse,TimeoutCallBack* pCallBackFunction,void* pCallBackParameter) { printf("\nset timer %d\n",nTimerID); tMultiTimer* pChoosedTimer = NULL; pChoosedTimer = g_aSPPMultiTimer[nTimerID]; pChoosedTimer->nInterval = nInterval; pChoosedTimer->bIsSingleUse = bIsSingleUse; pChoosedTimer->pTimeoutCallbackfunction = pCallBackFunction; pChoosedTimer->pTimeoutCallbackParameter = pCallBackParameter; //若是超時任務鏈表中已經有這個任務了,先取消,而後再設置,即重置超時任務 if(pChoosedTimer->pNextTimer != NULL || pChoosedTimer->pPreTimer != NULL) CancelTimerTask(nTimerID,CANCEL_MODE_IMMEDIATELY); AddTimerToCheckList(pChoosedTimer); return 0; } /** * @function 取消超時檢測鏈表中的指定超時任務 * @parameter1 要取消的超時任務的ID * @parameter2 模式選擇,是當即取消,仍是下次執行後取消 * @return 錯誤碼 */ uint8_t CancelTimerTask(uint8_t nTimerID,uint8_t nCancelMode) { printf("\ncancle timer %d\n",nTimerID); tMultiTimer* pEarliestTimer = NULL; tMultiTimer* pHandleTimer = NULL; tMultiTimer* pHandleTimer_pre = NULL; tMultiTimer* pChoosedTimer = NULL; pEarliestTimer = g_pTimeoutCheckListHead; pChoosedTimer = g_aSPPMultiTimer[nTimerID]; if(nCancelMode == CANCEL_MODE_IMMEDIATELY) { while(pEarliestTimer != NULL) { pHandleTimer = pEarliestTimer; pHandleTimer_pre = NULL; while(pHandleTimer != NULL) { if(pHandleTimer->nTimerID == nTimerID) { if(pHandleTimer_pre == NULL) { if(pHandleTimer->pNextHandle != NULL) { pEarliestTimer = pHandleTimer->pNextHandle; pEarliestTimer->pPreTimer = pHandleTimer->pPreTimer; if(pHandleTimer->pPreTimer != NULL) pHandleTimer->pPreTimer->pNextTimer = pEarliestTimer; pEarliestTimer->pNextTimer = pHandleTimer->pNextTimer; if(pHandleTimer->pNextTimer != NULL) pHandleTimer->pNextTimer->pPreTimer = pEarliestTimer; pHandleTimer->pNextTimer = NULL; pHandleTimer->pPreTimer = NULL; pHandleTimer->pNextHandle = NULL; } else { if(pEarliestTimer->pPreTimer == NULL) { g_pTimeoutCheckListHead = pEarliestTimer->pNextTimer; g_pTimeoutCheckListHead->pPreTimer = NULL; pEarliestTimer->pNextTimer = NULL; } else if(pEarliestTimer->pNextTimer == NULL) { pEarliestTimer->pPreTimer->pNextTimer = NULL; pEarliestTimer->pPreTimer = NULL; } else { pEarliestTimer->pPreTimer->pNextTimer = pEarliestTimer->pNextTimer; pEarliestTimer->pNextTimer->pPreTimer = pEarliestTimer->pPreTimer; pEarliestTimer->pPreTimer = NULL; pEarliestTimer->pNextTimer = NULL; } } } else { pHandleTimer_pre->pNextHandle = pHandleTimer->pNextHandle; pHandleTimer->pNextHandle = NULL; } return 0; } else { pHandleTimer_pre = pHandleTimer; pHandleTimer = pHandleTimer_pre->pNextHandle; } } pEarliestTimer = pEarliestTimer->pNextTimer; } #ifdef DEBUG_PRINTF printf("\nThere is no this timer task!\n"); #endif return 2; //出錯,超時檢測鏈表中沒有這個超時任務 } else if(nCancelMode == CANCEL_MODE_AFTER_NEXT_TIMEOUT) { pChoosedTimer->bIsSingleUse = true; return 0; } else { return 1; //出錯,模式錯誤,不認識該模式 } } /** * @function 定時器處理函數,用於檢測是否有定時任務超時,若是有則調用該定時任務的回調函數,並更新超時檢測鏈表 * 更新動做:若是超時的那個定時任務不是一次性的,則將新的節點加入到檢測超時鏈表中,不然直接刪掉該節點; * @parameter * @return */ void SYSTimeoutHandler(int signo) { //printf("\nenter SYSTimeoutHandler\n"); if(signo != SIGALRM) return; tMultiTimer* pEarliestTimer = NULL; tMultiTimer* pWaitingToHandle = NULL; tMultiTimer* pEarliestTimerPreHandle = NULL; if(g_pTimeoutCheckListHead != NULL) { if((g_pTimeoutCheckListHead->nTimeStamp <= g_nAbsoluteTime) && (g_pTimeoutCheckListHead->bIsOverflow == g_bIs_g_nAbsoluteTimeOverFlow)) { pWaitingToHandle = g_pTimeoutCheckListHead; g_pTimeoutCheckListHead = g_pTimeoutCheckListHead->pNextTimer; if(g_pTimeoutCheckListHead != NULL) g_pTimeoutCheckListHead->pPreTimer = NULL; pWaitingToHandle->pNextTimer = NULL; pEarliestTimer = pWaitingToHandle; while(pEarliestTimer != NULL) { pEarliestTimerPreHandle = pEarliestTimer; pEarliestTimer = pEarliestTimer->pNextHandle; pEarliestTimerPreHandle->pNextHandle = NULL; pEarliestTimerPreHandle->pNextTimer = NULL; pEarliestTimerPreHandle->pPreTimer = NULL; pEarliestTimerPreHandle->pTimeoutCallbackfunction(pEarliestTimerPreHandle->pTimeoutCallbackParameter); if(!(pEarliestTimerPreHandle->bIsSingleUse)) AddTimerToCheckList(pEarliestTimerPreHandle); } } } g_nAbsoluteTime++; if(g_nAbsoluteTime == 0) g_bIs_g_nAbsoluteTimeOverFlow = !g_bIs_g_nAbsoluteTimeOverFlow; return ; } void CancleAllTimerTask() { tMultiTimer* pEarliestTimer = NULL; tMultiTimer* pHandleTimer = NULL; while(g_pTimeoutCheckListHead != NULL) { pEarliestTimer = g_pTimeoutCheckListHead; g_pTimeoutCheckListHead = g_pTimeoutCheckListHead->pNextTimer; while(pEarliestTimer != NULL) { pHandleTimer = pEarliestTimer; pEarliestTimer = pEarliestTimer->pNextHandle; pHandleTimer->pNextHandle = NULL; pHandleTimer->pNextTimer = NULL; pHandleTimer->pPreTimer = NULL; pHandleTimer->bIsOverflow = false; } } g_bIs_g_nAbsoluteTimeOverFlow = false; g_nAbsoluteTime = 0; return; } void MultiTimerInit() { g_pTimeoutCheckListHead = NULL; g_bIs_g_nAbsoluteTimeOverFlow = false; g_nAbsoluteTime = 0; for(uint8_t index = 0; index < MAX_TIMER_UPPER_LIMIT; index++) { g_aSPPMultiTimer[index] = (tMultiTimer*)CMALLOC(sizeof(tMultiTimer)); g_aSPPMultiTimer[index]->nTimerID = g_aTimerID[index]; g_aSPPMultiTimer[index]->nInterval = g_aDefaultTimeout[index]; g_aSPPMultiTimer[index]->nTimeStamp = 0; g_aSPPMultiTimer[index]->bIsSingleUse = true; g_aSPPMultiTimer[index]->bIsOverflow = false; g_aSPPMultiTimer[index]->pTimeoutCallbackfunction = NULL; g_aSPPMultiTimer[index]->pTimeoutCallbackParameter = NULL; g_aSPPMultiTimer[index]->pNextTimer = NULL; g_aSPPMultiTimer[index]->pPreTimer = NULL; g_aSPPMultiTimer[index]->pNextHandle = NULL; } /* 若是預先規定了一些定時器,這個時候能夠初始化除時間戳之外的其餘值 */ //開啓應答超時任務 //OPEN_MULTITIMER_MANGMENT(); }
multiTimer.h
#ifndef __MULTITIMER_H__ #define __MULTITIMER_H__ #define MAX_TIMER_UPPER_LIMIT 6 #define TIMER_0 0 #define TIMER_1 1 //timer ID #define TIMER_2 2 #define TIMER_3 3 #define TIMER_4 4 #define TIMER_5 5 #define CANCEL_MODE_IMMEDIATELY 0xf9 #define CANCEL_MODE_AFTER_NEXT_TIMEOUT 0x9f typedef void TimeoutCallBack(void*); //======================================================== // timer結構定義 //======================================================== typedef struct tMultiTimer { uint8_t nTimerID; // uint32_t nInterval; //定時時長 uint32_t nTimeStamp; //時間戳 bool bIsSingleUse; //是否單次使用 bool bIsOverflow; //用於解決計數溢出問題 TimeoutCallBack *pTimeoutCallbackfunction; void* pTimeoutCallbackParameter; //雙向鏈表指針 struct tMultiTimer* pNextTimer; struct tMultiTimer* pPreTimer; //相同時間戳的下一個處理函數 這裏可能會有隱藏的 bug,若是基礎時間中斷比較快,那麼可能在處理多個同一時間節點的 //回調函數的時候被下一次的中斷打斷,這裏會引發時序錯誤, //解決方案有三種, //一是能夠人爲避免,不設置有公約數的定時時間,這樣的話同一個時刻有多個定時任務的狀況就小不少; //二是回調函數儘可能少作事,快速退出定時處理函數; //三是另開一個線程,這個線程僅把回調函數放到一個隊列中,另外一個線程持續從隊列中取回調函數執行,這個是沒有問題的方案,可是須要支持多線程或者多任務,而且須要注意加鎖 struct tMultiTimer* pNextHandle; }tMultiTimer; //======================================================== // 實現多定時任務的相關變量 //======================================================== tMultiTimer* g_pTimeoutCheckListHead; bool g_bIs_g_nAbsoluteTimeOverFlow; uint32_t g_nAbsoluteTime; //======================================================== // 外部接口 //======================================================== void MultiTimerInit(); uint8_t SetTimer(uint8_t nTimerID,uint32_t nInterval,bool bIsSingleUse,TimeoutCallBack* pCallBackFunction,void* pCallBackParameter); uint8_t CancelTimerTask(uint8_t nTimerID,uint8_t nCancelMode); void CancleAllTimerTask(); void SYSTimeoutHandler(int signo); #endif