【網絡編程】處理定時事件(三)---看看Libco的時間輪

前言

你以爲我鴿了其實我沒有鴿,這也算是一種鴿。
繼續來填坑啦。

在上兩篇中,我們都是使用的鏈表進行保存定時事件,當我們需要增加一個或者刪除一個事件時都需要O(n)的時間複雜度,本篇我們通過時間輪(time wheel)這種數據結構來對其進行優化,而libco也是通過時間輪來進行處理的,所以就拿着它的代碼來講啦。

正文

Libco的作爲一個協程庫,相當於在用戶態完成了邏輯流的切換,這裏的調度便是一旦遇到阻塞的系統調用(如read)時,將其註冊到epoll_wait中並切換邏輯流,等待其I/O事件的到達,一旦到達則進行處理,將同步阻塞I/O換成了I/O多路複用。

而這裏便是將I/O事件當作定時事件來處理,將I/O事件設置超時事件,如果超時則直接處理,避免一直等待的情況。

libco管理定時事件便是使用時間輪這種數據結構,通過一種hash的思想使得添加定時事件的時間複雜度降到O(1),大大提高了效率。
我們先來看看時間輪是怎樣的東西。

時間輪是個啥

在之前我們通過鏈表,按照超時時間進行升序或者降序的排列,這樣添加事件就需要O(N)的時間複雜度。
而時間輪則將多條鏈表組合起來,每條鏈表上的事件都是同樣的超時時間,而兩條鏈表超時時間的差值t就是處理超時事件的時間間隔。時間輪內部有一個指針指向當前的鏈表,t時間過去,t指向下一個鏈表,判斷是否超時。
而當我們想要添加一個定時事件,只需要知道它的超時時間,再除以t,就是它應該插入的位置。

如圖,當前指向1號鏈表,t爲50ms,當需要添加一個定時爲100ms的定時事件時,直接添加到3號鏈表即可(O(1))。
(圖轉自https://www.ibm.com/developerworks/cn/linux/l-cn-timers/index.html
pic

libco的主循環分析

讓我們看看這裏的主循環,爲了思路清晰,刪除部分無關代碼

void co_eventloop( stCoEpoll_t *ctx,pfn_co_eventloop_t pfn,void *arg )
{

    co_epoll_res *result = ctx->result;


    for(;;)
    {
    /*在之前的博客中,爲了達到定時查看的效果,我們使用epoll_wait的超時參數或者定時信號,而這裏則是讓epoll_wait以一個非常短的間隙(1ms)返回*/
        int ret = co_epoll_wait( ctx->iEpollFd,result,stCoEpoll_t::_EPOLL_SIZE, 1 );

        stTimeoutItemLink_t *active = (ctx->pstActiveList);
        stTimeoutItemLink_t *timeout = (ctx->pstTimeoutList);

        memset( timeout,0,sizeof(stTimeoutItemLink_t) );

        for(int i=0;i<ret;i++)
        {
            stTimeoutItem_t *item = (stTimeoutItem_t*)result->events[i].data.ptr;
            if( item->pfnPrepare )//如果有預處理函數則調用預處理函數
            {
                item->pfnPrepare( item,result->events[i],active );
            }
            else
            {
                AddTail( active,item );//否則添加到active鏈,準備下一步處理
            }
        }


        unsigned long long now = GetTickMS();//獲取現在的時間,這裏我們下文會敘述。
        TakeAllTimeout( ctx->pTimeout,now,timeout );//將時間輪上的超時事件取出,並且讓時間輪向前滾動

        stTimeoutItem_t *lp = timeout->head;
        while( lp )
        {
            //printf("raise timeout %p\n",lp);
            lp->bTimeout = true;
            lp = lp->pNext;
        }

        Join<stTimeoutItem_t,stTimeoutItemLink_t>( active,timeout );

        lp = active->head;
        while( lp )
        {

            PopHead<stTimeoutItem_t,stTimeoutItemLink_t>( active );
            if( lp->pfnProcess )
            {
                lp->pfnProcess( lp );//處理active鏈上的事件
            }
            lp = active->head;
        }
    }
}

可以看到這個eventlopp和我之前幾篇博客的思路差不多,都是:
epoll_wait監聽–>等待事件–>處理I/O事件–>得到現在時間,判斷是否超時–>處理超時事件。

獲取現在的時間

這裏比較有趣的是GetTickMS,這個用於獲取現在時間的函數,


static unsigned long long GetTickMS()
{
#if defined( __LIBCO_RDTSCP__) 
    static uint32_t khz = getCpuKhz();//法1
    return counter() / khz;
#else
    struct timeval now = { 0 };
    gettimeofday( &now,NULL );//法2 使用gettimeofday
    unsigned long long u = now.tv_sec;
    u *= 1000;
    u += now.tv_usec / 1000;
    return u;
#endif
}

gettimeofday自然不用多說,它的好處是跨平臺,不用切換到內核態。

而上面的法1使用的函數如下

#if defined( __LIBCO_RDTSCP__) 
static unsigned long long counter(void)
{
    register uint32_t lo, hi;
    register unsigned long long o;
    //__asm__ 內嵌彙編代碼 __volatile__阻止編譯器優化
    __asm__ __volatile__ (
            "rdtscp" : "=a"(lo), "=d"(hi)
            );//eax寄存器的值賦給lo,edx賦給hi
    o = hi;//o爲64位,將hi先放在低32位
    o <<= 32;//移到高位
    return (o | lo);//將lo放在低32位return

}
static unsigned long long getCpuKhz()
{
    FILE *fp = fopen("/proc/cpuinfo","r");
    if(!fp) return 1;
    char buf[4096] = {0};
    fread(buf,1,sizeof(buf),fp);
    fclose(fp);

    char *lp = strstr(buf,"cpu MHz");
    if(!lp) return 1;
    lp += strlen("cpu MHz");
    while(*lp == ' ' || *lp == '\t' || *lp == ':')
    {
        ++lp;
    }

    double mhz = atof(lp);
    unsigned long long u = (unsigned long long)(mhz * 1000);
    return u;
}
#endif

如果你看不懂getCpuKhz這個函數,可以打開/proc/cpuinfo看一眼,就可以知道這裏記載的是cpu的動態信息。

而counter函數則主要是調用rdtscp這條彙編指令,將計數(來一個時鐘脈衝+1)讀出來。
我的理解是counter()將總共的時鐘脈衝數讀出再除以cpu的頻率(每秒時鐘脈衝)就是時間,但是cpu的頻率不是個恆定值啊,對於現代cpu來說。。。所以不是很明白這裏爲什麼要這樣計時,希望能有人給出解答

參考資料

C++開源協程庫libco-原理與應用 — 滴滴平臺技術部·王亮
__asm__ __volatile__ 的含義 —– stackoverflow
Linux 下定時器的實現方式分析(時間輪部分) — 趙軍
Understanding Processor Frequency part of cat /proc/cpuinfo
再論 Time stamp counter —– 一念天堂的博客
如何精確測量一段代碼的執行時間 —– 淺墨的部落格

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