linux定時器時間輪演算法詳解

2021-10-14 00:11:53 字數 3084 閱讀 1959

linux高併發程式設計|紅黑樹實現定時器|時間輪實現定時器

linux多執行緒環境下海量定時任務的定時器設計

linux定時器分為低精度定時器和高精度定時器兩種型別,核心對其均有實現。本文討論的是我們在應用程式開發中比較常見的低精度定時器。作為常用的基礎元件,定時器常用的幾種實現方法包括:基於排序鍊錶實現、基於小根堆實現、基於紅黑樹實現、基於時間輪實現。本文講解的是時間複雜度最優,也是linux核心採用的基於時間輪的實現方式。

採用有序鍊錶實現的定時器,新增定時器的時間複雜度為o(n);採用小根堆或紅黑樹實現的定時器,新增定時器的時間複雜度為o(lgn)。之所以沒法做到o(1)的複雜度,究其原因是所有定時器節點掛在一條鍊錶(或一棵樹)上。時間輪演算法的核心思路是將定時器雜湊到多條鏈上,是典型的空間換時間的策略。下文從單個時間輪出發講解,逐步擴充套件至linux實現定時器所採用的多級時間輪演算法。

單時間輪只有乙個由bucket串起來的輪子,下圖所示的時間輪有8個bucket,每個bucket下鏈結著未來對應時刻到期的節點。假設圖中相鄰bucket到期時間的間隔為slot=1s,從當前時刻0s開始計時,1s時到期的定時器節點掛在bucket[1]下,2s時到期的定時器節點掛在bucket[2]下……當tick檢查到時間過去了1s時,bucket[1]下所有節點執行超時動作,當時間到了2s時,bucket[2]下所有節點執行超時動作…….

由於bucket是乙個陣列,能直接根據下標定位到具體定時器節點鏈,因此新增刪除節點、定時器到期執行的時間複雜度均為o(1)。

但使用這個定時器所受的限制也顯而易見:待新增的timer到期時間必須在8s以內。這顯然不能滿足實際需求。當然要擴充套件也很容易,直接增加bucket的個數就可以了。在 linux 系統中,我們可以設定slot為1個jiffy(1/hz)的定時器,假設最大的到期時間範圍要達到 2^32個 jiffies,如果採用上面這樣的單時間輪,我們就需要2^32個 bucket,這會帶來巨大的記憶體消耗,顯然是需要優化改進的。

【文章福利】需要c/c++ linux伺服器架構師學習資料**812855908(資料報括c/c++,linux,golang技術,nginx,zeromq,mysql,redis,fastdfs,mongodb,zk,流**,cdn,p2p,k8s,docker,tcp/ip,協程,dpdk,ffmpeg等)

改進的單時間輪其實是乙個對時間和空間折中的思路,即不會像單時間輪那樣有o(1)的時間複雜度,但也不會像單時間輪那樣對bucket個數有巨大的需求。其原理也很簡單,就是每個bucket不單可以掛接到期時間expire=slot的定時器,還可掛接expire%n=slot的定時器(n為bucket個數)。這也正好順應時間輪的輪迴作用。如圖2所示,定時器中expire表示到期時間,rotation表示節點在時間輪轉了幾圈後才到期。當當前時間指標指向某個bucket時,不能像簡單時間輪那樣直接對bucket下的所有節點執行超時動作,而是需要對鍊錶中節點遍歷一遍,判斷輪子轉動的次數是否等於節點中的rotation值,當兩者相等時,方可執行超時操作。

上面所述的時間輪都是單槍匹馬戰鬥的,因此很難在時間和空間上都達到理想效果。linux所實現的多時間輪演算法,借鑑了日常生活中水表的度量方法,通過低刻度走得快的輪子帶動高一級刻度輪子走動的方法,達到了僅使用較少刻度即可表示很大範圍度量值的效果。

linux定時器時間輪分為5個級別的輪子(tv1 ~ tv5),如圖3所示。每個級別的輪子的刻度值(slot)不同,規律是次級輪子的slot等於上級輪子的slot之和。linux定時器slot單位為1jiffy,tv1輪子分256個刻度,每個刻度大小為1jiffy。tv2輪子分64個刻度,每個刻度大小為256個jiffy,即tv1整個輪子所能表達的範圍。相鄰輪子也只有滿足這個規律,才能達到「低刻度輪子轉一圈,高刻度輪子走一格」的效果。tv3,tv4,tv5也都是分為64個刻度,因此容易算出,最高一級輪子tv5所能表達的slot範圍達到了25664646464 = 2^32 jiffies。

linux時間輪定時器演算法的關鍵在於新增定時器操作和時間輪進製遷移鍊錶操作。先來說新增定時器。新增定時器的關鍵又在於知道每個時間輪每乙個刻度所能表示的到期時間的範圍。圖4列出了每一級時間輪能度量的jiffies的大小。假設有乙個定時器在1000個jiffies後到期,根據圖4容易看出其應該掛在tv2輪上。tv2輪每個刻度表示的大小為256個jiffies,則其應該掛在(1000/256)=3即第三個bucket上。

linux在定時器到期檢查上的操作也實現得很巧妙。假設curr_time=0x12345678,那麼下乙個檢查的時刻為0x12345679。如果tv1.bucket[0x79]上鍊表非空,則下乙個檢查時刻tv1.bucket[0x79]上的定時器節點超時。如果curr_time到了0x12345700,低8位為空,說明有進製產生,這時移出8~13位對應的定時器鍊錶(即正好對應著tv2輪),重新加入定時器系統,這就完成了一次進製遷移操作。同樣地,當curr_time的第8-13位為0時,這表明tv2輪對tv3輪有進製發生,將curr_time第14-19位的值作為下標,移出tv3中對應的定時器鍊錶,然後將它們重新加入到定時器系統中來。tv4,tv5依次類推。之所以能夠根據curr_time來檢查超時鏈,是因為tv1~tv5輪的度量範圍正好依次覆蓋了整型的32位:tv1(1-8位),tv2(9-14位),tv3(15-20位),tv4(21-26位),tv5(27-32位);而curr_time計數的遞增中,低位向高位的進製正是低階時間輪轉圈帶動高階時間輪走動的過程。

最後比較一下多級時間輪和單個簡單時間輪的時間複雜度及空間複雜度:linux使用了總計256+64+64+64+64=512個bucket,即可實現[0,2^32) jiffies的超時範圍。相比簡單的單時間輪,時間上僅僅多了1/256次(為約等於值,忽略了tv2以上產生的進製操作)的鍊錶遷移操作耗時。可以認為其新增、刪除定時器節點及到期check的操作時間複雜度均為o(1)。

Linux的動態定時器 時間輪

定時器 有時也稱為動態定時器或核心定時器 是管理核心時間的基礎。定時器是一種軟體功能,即允許在將來的某個時刻,函式在給定的時間間隔用完時被呼叫。注意的是定時器並不會週期執行,它在超時後就自行銷毀,這也是定時器被稱為動態定時器的乙個原因。動態定時器不斷地建立和銷毀,而且它的執行次數也不受限制。定時器在...

定時器 tick 時間輪定時器學習筆記

參考自時間輪的原理和手錶的指標計時類似,將任務結束的時間加入對應最高端的時間輪轉盤中。例如任務結束時間為4 30 20,時間輪盤有三個,分別對應,小時,分鐘和秒。開始時將任務加入4小時所在的時間輪中。四小時過去後,任務的剩餘時間時間為0 30 20就將任務加入30分所在的時間輪中。30分鐘過去後,將...

linux2 6定時器的時間輪演算法分析

常用的定時器實現演算法有兩種 紅黑樹和時間輪 timing wheel 在linux2.6的 中,kernel timer.c檔案實現了乙個通用定時器機制,使用的是時間輪演算法。每乙個cpu都有乙個struct tvec base結構,代表這個cpu使用的時間輪。struct tvec base s...