erlang底層c定時器設計-Erlang源碼學習二

Erlang底層的定時器實現位於源碼的erts/emulator/beam/time.c文件,用時間輪的方式動態添加和刪除定時器,結構體名爲typedef struct ErtsTimerWheel_ ErtsTimerWheel,每一個定時器的結構體名爲typedef struct erl_timer ErtsTWheelTimer,看結構體實現大體上可以知道定時器的設計。

  1. 定時器 ErtsTWheelTimer

    typedef struct erl_timer {
        struct erl_timer* next; /* next entry tiw slot or chain */
        struct erl_timer* prev; /* prev entry tiw slot or chain */
        union {
        struct {
            void (*timeout)(void*); /* called when timeout */
            void (*cancel)(void*);  /* called when cancel (may be NULL) */
            void* arg;              /* argument to timeout/cancel procs */
        } func;
        ErtsThrPrgrLaterOp cleanup;
        } u;
        ErtsMonotonicTime timeout_pos; /* Timeout in absolute clock ticks */
        int slot;
    } ErtsTWheelTimer;
    

    每個定時器維護了前後向指針,有定時器到時作爲回調的函數、取消定時器所調用的函數(可能做參數銷燬用)和函數參數,還有定時器的到時點,以及此定時器位於時間輪的槽。

  2. 時間輪 ErtsTimerWheel

    struct ErtsTimerWheel_ {
        ErtsTWheelTimer *w[ERTS_TIW_SIZE];
        ErtsMonotonicTime pos;
        Uint nto;
        struct {
        ErtsTWheelTimer *head;
        ErtsTWheelTimer *tail;
        Uint nto;
        } at_once;
        int yield_slot;
        int yield_slots_left;
        int yield_start_pos;
        ErtsTWheelTimer sentinel;
        int true_next_timeout_time;
        ErtsMonotonicTime next_timeout_time;
    };
    

    時間輪維護了一個ERTS_TIW_SIZE大小的定時器指針數組,看頭文件定義可以得到ERTS_TIW_SIZE在小內存機器上是 1<<13的大小,大內存機器爲1<<16=2^16=2^6*1024=65535大小,這裏只看大內存機器;接着有一個pos字段,類型爲ErtsMonotonicTime,這是一個long long的別名,顧名思義就是erlang的monotonic時間,簡單說就是一個精確到納秒的單調遞增時間;接着有一個at_once空間,有頭head、尾tail指針,至於數據結構可能爲鏈表,可能爲數組實現的棧或隊列等;然後的字段光看名字也無法推斷了。進入時間輪操作函數。


time.c的函數只有幾個,先羅列簡單的:

  1. 創建時間輪 erts_create_timer_wheel

    ErtsTimerWheel *
    erts_create_timer_wheel(ErtsSchedulerData *esdp)
    {
        ErtsMonotonicTime mtime;
        int i;
        ErtsTimerWheel *tiw;
        tiw = erts_alloc_permanent_cache_aligned(ERTS_ALC_T_TIMER_WHEEL,
                             sizeof(ErtsTimerWheel));
        for(i = 0; i < ERTS_TIW_SIZE; i++)
        tiw->w[i] = NULL;
    
        mtime = erts_get_monotonic_time(esdp);
        tiw->pos = ERTS_MONOTONIC_TO_CLKTCKS(mtime);
        tiw->nto = 0;
        tiw->at_once.head = NULL;
        tiw->at_once.tail = NULL;
        tiw->at_once.nto = 0;
        tiw->yield_slot = ERTS_TWHEEL_SLOT_INACTIVE;
        tiw->true_next_timeout_time = 0;
        tiw->next_timeout_time = mtime + ERTS_MONOTONIC_DAY;
        tiw->sentinel.next = &tiw->sentinel;
        tiw->sentinel.prev = &tiw->sentinel;
        tiw->sentinel.u.func.timeout = NULL;
        tiw->sentinel.u.func.cancel = NULL;
        tiw->sentinel.u.func.arg = NULL;
        return tiw;
    }
    

    看操作是先分配內存,然後初始化w定時器指針數組爲NULL,接着獲取一次當前的monotonic時間,將它轉換爲時間輪滴答後賦給pos字段,monotonic時間是精確到納秒,宏ERTS_MONOTONIC_TO_CLKTCKS將它除以了1000*1000,從這裏我們可以知道時間輪每一次走動是1ms,即時間輪的粒度就是1ms了,接下來的操作就是常規的初始化了,到tiw->sentinel.next = $tiw->sentinel語句開始,是將一個sentinel(哨兵)變量變爲一個指向自己的循環雙向鏈表。
    結論:
    時間輪的pos字段初始值爲創建時間輪時的monotonic時間,但時間輪的精度爲ms,故需要將monotonic時間轉換爲ms(除以1000*1000),pos字段爲時間輪的當前指針(想象成鐘的分針)。
     

  2. 插入定時器 insert_timer_into_slot

    static ERTS_INLINE void
    insert_timer_into_slot(ErtsTimerWheel *tiw, int slot, ErtsTWheelTimer *p)
    {
        ERTS_TW_ASSERT(slot >= 0);
        ERTS_TW_ASSERT(slot < ERTS_TIW_SIZE);
        p->slot = slot;
        if (!tiw->w[slot]) {
        tiw->w[slot] = p;
        p->next = p;
        p->prev = p;
        }
        else {
        ErtsTWheelTimer *next, *prev;
        next = tiw->w[slot];
        prev = next->prev;
        p->next = next;
        p->prev = prev;
        prev->next = p;
        next->prev = p;
        }
    }
    

    先看插入的第1、2兩句,斷言slot要介於0-ERTS_TIW_SIZE之間:定時器要插到時間輪的槽上,因此必須介於這個範圍。然後開始插入,先判斷待插入的槽有沒有定時器,如果沒有,就直接將w[slot]指針指向這個定時器,並且賦值next、prev指針保證循環雙向鏈表特性;如果槽上已經有了別的定時器,那麼看else的操作是將待插入的定時器頭插到鏈表中。
    於是看完這個函數,知道了時間輪的主要邏輯如圖:
    這裏寫圖片描述
    結論:
    時間輪的槽大小爲65535;每個槽是一個定時器指針,指針又維護了一個定時器雙向循環鏈表,跟鏈式散列表很像;定時器是頭插。
     

  3. 去除定時器 remove_timer

    static ERTS_INLINE void
    remove_timer(ErtsTimerWheel *tiw, ErtsTWheelTimer *p)
    {
        int slot = p->slot;
        ERTS_TW_ASSERT(slot != ERTS_TWHEEL_SLOT_INACTIVE);
    
        if (slot >= 0) {
            /*
             * Timer in wheel or in circular
             * list of timers currently beeing
             * triggered (referred by sentinel).
             */
            ERTS_TW_ASSERT(slot < ERTS_TIW_SIZE);
            if (p->next == p) {
                ERTS_TW_ASSERT(tiw->w[slot] == p);
                tiw->w[slot] = NULL;
            }
            else {
                if (tiw->w[slot] == p)
                tiw->w[slot] = p->next;
                p->prev->next = p->next;
                p->next->prev = p->prev;
            }
        }
        else {
            /* Timer in "at once" queue... */
            ERTS_TW_ASSERT(slot == ERTS_TWHEEL_SLOT_AT_ONCE);
            if (p->prev)
                p->prev->next = p->next;
            else {
                ERTS_TW_ASSERT(tiw->at_once.head == p);
                tiw->at_once.head = p->next;
            }
            if (p->next)
                p->next->prev = p->prev;
            else {
                ERTS_TW_ASSERT(tiw->at_once.tail == p);
                tiw->at_once.tail = p->prev;
            }
            ERTS_TW_ASSERT(tiw->at_once.nto > 0);
            tiw->at_once.nto--;
        }
    
        p->slot = ERTS_TWHEEL_SLOT_INACTIVE;
        tiw->nto--;
    }   
    

    先看第一個斷言slot != ERTS_TWHEEL_SLOT_INACTIVE,這個宏值爲-2,前面的函數知道槽數一定是介於0-65535之間,所以猜測如果槽數爲-2了,表示定時器未激活。
    往後看,如果槽存在,又分兩種情況,一種是這個定時器所處的槽只有它一個定時器,那麼需要將槽指針w[slot]置爲空,另一種是槽上還有很多定時器,則從循環雙向鏈表中取下一個結點。
    如果槽不存在,且看else的slot爲宏值ERTS_TWHEEL_SLOT_AT_ONCE,那麼就從at_once隊列中去除定時器,並且nto字段減1。
    將定時器的slot字段置爲ERTS_TWHEEL_SLOT_INACTIVE,時間輪的nto字段減1。
    結論:
    定時器有三種狀態分別爲正常、at_once、未激活;at_once隊列實則爲不循環雙向鏈表;at_once的nto字段記錄這個隊列上的定時器個數;tiw的nto字段記錄所有定時器包括at_once隊列上的定時器個數。 

  4. 定時器到時回調 timeout_timer
    回調就很簡單,將定時器的slot字段設置爲未激活,然後調用回調函數
     

  5. 取消定時器 erts_twheel_cancel_timer
    邏輯與4的到時回調差不多,判斷了定時器的slot不能爲未激活狀態,然後調用remove去除定時器,接着調用定時器的cancel回調函數
     
  6. 創建定時器 erts_twheel_set_timer

    void
    erts_twheel_set_timer(ErtsTimerWheel *tiw,
                  ErtsTWheelTimer *p, ErlTimeoutProc timeout,
                  ErlCancelProc cancel, void *arg,
                  ErtsMonotonicTime timeout_pos)
    {
        ErtsMonotonicTime timeout_time;
        ERTS_MSACC_PUSH_AND_SET_STATE_M_X(ERTS_MSACC_STATE_TIMERS);
    
        p->u.func.timeout = timeout;
        p->u.func.cancel = cancel;
        p->u.func.arg = arg;
    
        ERTS_TW_ASSERT(p->slot == ERTS_TWHEEL_SLOT_INACTIVE);
    
        if (timeout_pos <= tiw->pos) {
        tiw->nto++;
        tiw->at_once.nto++;
        p->next = NULL;
        p->prev = tiw->at_once.tail;
        if (tiw->at_once.tail) {
            ERTS_TW_ASSERT(tiw->at_once.head);
            tiw->at_once.tail->next = p;
        }
        else {
            ERTS_TW_ASSERT(!tiw->at_once.head);
            tiw->at_once.head = p;
        }
        tiw->at_once.tail = p;
        p->timeout_pos = tiw->pos;
        p->slot = ERTS_TWHEEL_SLOT_AT_ONCE;
        timeout_time = ERTS_CLKTCKS_TO_MONOTONIC(tiw->pos);
        }
        else {
        int slot;
    
        /* calculate slot */
        slot = (int) (timeout_pos & (ERTS_TIW_SIZE-1));
    
        insert_timer_into_slot(tiw, slot, p);
    
        tiw->nto++;
    
        timeout_time = ERTS_CLKTCKS_TO_MONOTONIC(timeout_pos);
        p->timeout_pos = timeout_pos;
        }
    
        if (timeout_time < tiw->next_timeout_time) {
        tiw->true_next_timeout_time = 1;
        tiw->next_timeout_time = timeout_time;
        }
        ERTS_MSACC_POP_STATE_M_X();
    }
    

    邏輯很清楚:傳入一個時間輪、定時器、以及定時器要用的相關函數、時間輪上的超時位置(monotonic time / 1000*1000)。
    然後判斷超時位置是否小於等於時間輪當前的指針pos,如果是,就把它加入到at_once鏈表,pos的精度爲ms,這個at_once的意思就是加入的定時器差1ms就要到時,而針對這種定時器,再把它插入到槽裏做管理和到時是沒有意義的,因爲馬上就到時了。
    正常的定時器則可以插入到槽裏了,槽的計算是用到時位置與槽總大小做與運算,舉個例子:當前monotonic時間爲10,000,000,000,表示開始或者erlang虛擬機開啓了10s, 此時創建了一個時間輪,它的pos就該爲10,000,然後插入一個5,000,000,000納秒後到時的定時器,因爲時間輪精度爲ms,顧折算爲(10,000,000,000 + 5,000,000,000)/1000*1000=15,000,即timeout_pos就爲15000,那麼timeout_pos & ERTS_TIW_SIZE = 15000,那麼槽就是15000位置,此時槽還在10000位置,要走5000個滴答纔到,同理,如果插入一個距現在65536ms後到時的定時器,則65536超出了65535,但與運算,又變爲了0,實現了定時器的循環相加。
    相應nto計數加一,然後判斷加入的定時器的到時時間是否小於等於時間輪的下一次到時時間,如果是,就更新時間輪的相應到時值。
    結論:
    定時器如果馬上(差1ms)到時的,會加入到at_once隊列,否則加入到時間槽裏做管理;定時器的到時時間爲一個精度爲ms的值,然後用這個值跟ERTS_TIW_SIZE做與運算,保證了槽的循環;時間輪還有字段用來表示下一次最近的到時時間,true_next_timeout_time爲1表示存在這個時間(即槽上至少存在一個激活的定時器還沒到時)。
     

  7. 尋找下一個最近到時時間 find_next_timeout

    static ERTS_INLINE ErtsMonotonicTime
    find_next_timeout(ErtsSchedulerData *esdp,
              ErtsTimerWheel *tiw,
              int search_all,
              ErtsMonotonicTime curr_time,       /* When !search_all */
              ErtsMonotonicTime max_search_time) /* When !search_all */
    {
        int start_ix, tiw_pos_ix;
        ErtsTWheelTimer *p;
        int true_min_timeout = 0;
        ErtsMonotonicTime min_timeout, min_timeout_pos, slot_timeout_pos;
    
        if (tiw->nto == 0) { /* no timeouts in wheel */
            if (!search_all)
                min_timeout_pos = tiw->pos;
            else {
                curr_time = erts_get_monotonic_time(esdp);
                tiw->pos = min_timeout_pos = ERTS_MONOTONIC_TO_CLKTCKS(curr_time);
            }
            min_timeout_pos += ERTS_MONOTONIC_TO_CLKTCKS(ERTS_MONOTONIC_DAY);
            goto found_next;
        }
    
        slot_timeout_pos = min_timeout_pos = tiw->pos;
        if (search_all)
           min_timeout_pos += ERTS_MONOTONIC_TO_CLKTCKS(ERTS_MONOTONIC_DAY);
        else
           min_timeout_pos = ERTS_MONOTONIC_TO_CLKTCKS(curr_time + max_search_time);
    
        start_ix = tiw_pos_ix = (int) (tiw->pos & (ERTS_TIW_SIZE-1));
    
        do {
            if (++slot_timeout_pos >= min_timeout_pos)
                break;
    
            p = tiw->w[tiw_pos_ix];
    
            if (p) {
                ErtsTWheelTimer *end = p;
    
                do  {
                ErtsMonotonicTime timeout_pos;
                timeout_pos = p->timeout_pos;
                if (min_timeout_pos > timeout_pos) {
                    true_min_timeout = 1;
                    min_timeout_pos = timeout_pos;
                    if (min_timeout_pos <= slot_timeout_pos)
                    goto found_next;
                }
                p = p->next;
                } while (p != end);
            }
    
            tiw_pos_ix++;
            if (tiw_pos_ix == ERTS_TIW_SIZE)
                tiw_pos_ix = 0;
        } while (start_ix != tiw_pos_ix);
    
    found_next:
    
        min_timeout = ERTS_CLKTCKS_TO_MONOTONIC(min_timeout_pos);
        tiw->next_timeout_time = min_timeout;
        tiw->true_next_timeout_time = true_min_timeout;
    
        return min_timeout;
    }
    

    函數作用是尋找時間輪所處指針到當前時間curr_time之間最近的一個定時器到時時間。
    函數邏輯分兩種情況,一種是時間輪上沒有定時器,則判斷search_all的值是否要將時間輪的指針撥到當前時間點,然後最小超時時間就爲明天的這個時候(因爲沒有定時器,自然不存在下一個到時的定時器時間);另一種是時間輪上有定時器,則判斷search_all的值是,如果爲1,尋找的間隔就是一天(24*60*60*1000),否則間隔就是時間輪當前指針到curr_time+max_search_time的距離,然後從時間輪當前指針處開始循環判斷每個槽鏈表,有無定時器的到時時間小於curr_time+max_search_time,如果找了一圈(即走過的距離爲ERTS_TIW_SIZE)沒找到,就退出,並設置時間輪的下一次到時時間。
    結論:
    時間輪維護了一個下一次到時時間,避免了一段連續的槽上都沒有定時器,而在做到時判斷時空循環破壞效率。
     

  8. 時間輪嘀嗒 erts_bump_timers

    void
    erts_bump_timers(ErtsTimerWheel *tiw, ErtsMonotonicTime curr_time)
    {
        int tiw_pos_ix, slots, yielded_slot_restarted, yield_count;
        ErtsMonotonicTime bump_to, tmp_slots, old_pos;
        ERTS_MSACC_PUSH_AND_SET_STATE_M_X(ERTS_MSACC_STATE_TIMERS);
    
        yield_count = ERTS_TWHEEL_BUMP_YIELD_LIMIT;
    
        /*
         * In order to be fair we always continue with work
         * where we left off when restarting after a yield.
         */
    
        if (tiw->yield_slot >= 0) {
            yielded_slot_restarted = 1;
            tiw_pos_ix = tiw->yield_slot;
            slots = tiw->yield_slots_left;
            bump_to = tiw->pos;
            old_pos = tiw->yield_start_pos;
            goto restart_yielded_slot;
        }
    
        do {
    
            yielded_slot_restarted = 0;
    
            bump_to = ERTS_MONOTONIC_TO_CLKTCKS(curr_time);
    
            while (1) {
                ErtsTWheelTimer *p;
    
                old_pos = tiw->pos;
    
                if (tiw->nto == 0) {
                    empty_wheel:
                    ERTS_DBG_CHK_SAFE_TO_SKIP_TO(tiw, bump_to);
                    tiw->true_next_timeout_time = 0;
                    tiw->next_timeout_time = curr_time + ERTS_MONOTONIC_DAY;
                    tiw->pos = bump_to;
                    tiw->yield_slot = ERTS_TWHEEL_SLOT_INACTIVE;
                            ERTS_MSACC_POP_STATE_M_X();
                    return;
                }
    
                p = tiw->at_once.head;
                while (p) {
                    if (--yield_count <= 0) {
                        ERTS_TW_ASSERT(tiw->nto > 0);
                        ERTS_TW_ASSERT(tiw->at_once.nto > 0);
                        tiw->yield_slot = ERTS_TWHEEL_SLOT_AT_ONCE;
                        tiw->true_next_timeout_time = 1;
                        tiw->next_timeout_time = ERTS_CLKTCKS_TO_MONOTONIC(old_pos);
                                ERTS_MSACC_POP_STATE_M_X();
                        return;
                    }
    
                    ERTS_TW_ASSERT(tiw->nto > 0);
                    ERTS_TW_ASSERT(tiw->at_once.nto > 0);
                    tiw->nto--;
                    tiw->at_once.nto--;
                    tiw->at_once.head = p->next;
                    if (p->next)
                        p->next->prev = NULL;
                    else
                        tiw->at_once.tail = NULL;
    
                    timeout_timer(p);
    
                    p = tiw->at_once.head;
                }
    
                if (tiw->pos >= bump_to) {
                    ERTS_MSACC_POP_STATE_M_X();
                    break;
                }
    
                if (tiw->nto == 0)
                    goto empty_wheel;
    
                if (tiw->true_next_timeout_time) {
                    ErtsMonotonicTime skip_until_pos;
                    /*
                     * No need inspecting slots where we know no timeouts
                     * to trigger should reside.
                     */
    
                    skip_until_pos = ERTS_MONOTONIC_TO_CLKTCKS(tiw->next_timeout_time);
                    if (skip_until_pos > bump_to)
                        skip_until_pos = bump_to;
    
                    skip_until_pos--;
    
                    if (skip_until_pos > tiw->pos) {
                        ERTS_DBG_CHK_SAFE_TO_SKIP_TO(tiw, skip_until_pos);
    
                        tiw->pos = skip_until_pos;
                    }
                }
    
                tiw_pos_ix = (int) ((tiw->pos+1) & (ERTS_TIW_SIZE-1));
                tmp_slots = (bump_to - tiw->pos);
                if (tmp_slots < (ErtsMonotonicTime) ERTS_TIW_SIZE)
                  slots = (int) tmp_slots;
                else
                  slots = ERTS_TIW_SIZE;
    
                tiw->pos = bump_to;
    
                while (slots > 0) {
    
                    p = tiw->w[tiw_pos_ix];
                    if (p) {
    
                        if (p->next == p) {
                            ERTS_TW_ASSERT(tiw->sentinel.next == &tiw->sentinel);
                            ERTS_TW_ASSERT(tiw->sentinel.prev == &tiw->sentinel);
                        } else {
                            tiw->sentinel.next = p->next;
                            tiw->sentinel.prev = p->prev;
                            tiw->sentinel.next->prev = &tiw->sentinel;
                            tiw->sentinel.prev->next = &tiw->sentinel;
                        }
    
                        tiw->w[tiw_pos_ix] = NULL;
    
                        while (1) {
    
                            if (p->timeout_pos > bump_to) {
                                /* Very unusual case... */
                                ++yield_count;
                                insert_timer_into_slot(tiw, tiw_pos_ix, p);
                            } else {
                                /* Normal case... */
                                timeout_timer(p);
                                tiw->nto--;
                            }
    
                            restart_yielded_slot:
    
                            p = tiw->sentinel.next;
    
                            if (p == &tiw->sentinel) {
                                ERTS_TW_ASSERT(tiw->sentinel.prev == &tiw->sentinel);
                                break;
                            }
    
                            if (--yield_count <= 0) {
                                tiw->true_next_timeout_time = 1;
                                tiw->next_timeout_time = ERTS_CLKTCKS_TO_MONOTONIC(old_pos);
                                tiw->yield_slot = tiw_pos_ix;
                                tiw->yield_slots_left = slots;
                                tiw->yield_start_pos = old_pos;
                                ERTS_MSACC_POP_STATE_M_X();
                                return; /* Yield! */
                            }
    
                            tiw->sentinel.next = p->next;
                            p->next->prev = &tiw->sentinel;
                        }
                    }
                    tiw_pos_ix++;
                    if (tiw_pos_ix == ERTS_TIW_SIZE)
                        tiw_pos_ix = 0;
                    slots--;
                }
            }
    
        } while (yielded_slot_restarted);
    
        tiw->yield_slot = ERTS_TWHEEL_SLOT_INACTIVE;
        tiw->true_next_timeout_time = 0;
        tiw->next_timeout_time = curr_time + ERTS_MONOTONIC_DAY;
    
        /* Search at most two seconds ahead... */
        (void) find_next_timeout(NULL, tiw, 0, curr_time, ERTS_SEC_TO_MONOTONIC(2));
        ERTS_MSACC_POP_STATE_M_X();
    }
    

    這是最重要的一個函數,erlang虛擬機啓動後,有一個線程做週期性調用,來檢測有無定時器到時。
    函數接收一個curr_time形參,將時間輪上小於等於此時間的定時器都視爲到時,所以估計是1ms調用一次。
    函數定義了yield_count=100,如果at_once或者某個槽上大於100個定時器,就丟棄多的。
    這個函數寫得很噁心,又是do while{},又是while(1),又是while,但剝離開,真正的邏輯就一段:循環將at_once鏈表的定時器全部到時,則at_once鏈表清空了;開始判斷時間槽,先利用下一個最近的到時時間next_timeout_time跳過一段槽,然後開始遍歷從時間輪的當前指針pos到curr_time之間的間隔槽,再遍歷每個槽上的鏈表,對每個結點判斷是否大於等於curr_time,即判斷是否到時,如果到時就可以去掉定時器,並執行回調任務。
    以上步驟就做完了到時任務,調用一下find_next_timeout尋找一次最近到時時間。


在看erts_bump_timers函數時候看到一段goto的代碼形如:

goto test_label:

int a = 0;

test_label:

    a = 1;

當時很詫異,a不是沒定義嗎?激動得不行,摩拳擦掌準備提bug,抱着謹慎的態度還是查了一下,這種用法是可以的,真是菜得不行 …… 自己猜想一下可能是編譯期已經將a加入了符號表,goto隻影響運行時。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章