1. 程式人生 > >linux中的定時器

linux中的定時器

定時器

網路程式經常需要處理的一類事件是定時器事件,伺服器程式通常管理著眾多定時事件,因此有效低組織這些定時事件,使之能在預期的時間點被觸發而不影響伺服器的主要邏輯,對於伺服器的效能有著至關重要的影響。為此,將每個定時事件分別封裝成定時器,並使用某種容器類資料結構,比如連結串列、排序連結串列和時間輪,將所有定時器串聯起來,以實現對定時事件的統一管理。

在討論如何組織定時器之前,先要介紹定時的方法。定時是指在一段時間之後觸發某段程式碼的機制,我們可以在這段程式碼中依此處理所有到期的定時器。換言之,定時機制是定時器得以被處理的原動力。

Linux提供的三種定時方法:

socket選項SO_RCVTIMEO 和SO_SNDTIMEO

SIGALRM訊號

I/O複用系統呼叫的超時引數

Socket選項之SO_RCVTIMEO 和SO_SNDTIMEO

SO_RCVTIMEO設定接收資料超時時間,SO_SNDTIMEO設定傳送資料超時時間。這兩個選項僅對資料接收和傳送相關的socket專用系統呼叫有效,這些系統呼叫如下所示:

可見,在程式中可以根據系統呼叫的返回值以及error來判斷超時時間是否已到.進而決定是否開始處理定時任務。

程式碼(設定connect超時時間)

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int timeout_connect( const char* ip, int port, int time )
{
        int ret = 0;
        struct sockaddr_in address;
        memset( &address, 0,sizeof( address ) );
        address.sin_family = AF_INET;
        inet_pton( AF_INET, ip, &address.sin_addr );
        address.sin_port = htons( port );

        int sockfd = socket( PF_INET, SOCK_STREAM, 0 );
        assert( sockfd >= 0 );

        struct timeval timeout;
        timeout.tv_sec = time;
        timeout.tv_usec = 0;
        socklen_t len = sizeof( timeout );
        ret = setsockopt( sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len );
        assert( ret != -1 );

        ret = connect( sockfd, ( struct sockaddr* )&address, sizeof( address ) );
        if ( ret == -1 )
        {
                if( errno == EINPROGRESS )
                {
                        printf( "connecting timeout\n" );
                        return -1;
                }
                printf( "error occur when connecting to server\n" );
                return -1;
        }

        return sockfd;
}

int main( int argc, char* argv[] )
{
        if( argc < 3 )
        {
                printf( "Not enough parameters" );
                return 1;
        }
        const char* ip = argv[1];
        int port = atoi( argv[2] );

        int sockfd = timeout_connect( ip, port, 10 );
        if ( sockfd < 0 )
        {
                return 1;
        }
        return 0;
}

SIGALRM訊號

由alarm和setitimer函式設定的實時鬧鐘一旦超時,將觸發SIGALARM訊號。因此可以利用該訊號的訊號處理函式來處理定時任務。

基於升序連結串列的定時器

定時器通常至少要包含兩個成員:一個超時時間和一個任務回撥函式。有時候還可能包含回撥函式被執行時需要傳入的引數,以及是否重啟定時器等資訊。如果使用連結串列作為容器來串聯所有的定時器,則每個定時器還要包含指向下一個定時器的指標成員。進一步,如果連結串列是雙向的,則每個定時器還需要包含指向前一個定時器的指標成員。 

I/O複用系統呼叫的超時函式

Linux下的3組I/O複用系統呼叫都帶有超時引數,因此它們不僅能統一處理訊號和I/O事件,也能統一處理定時事件。但是由於I/O複用系統呼叫可能在超時時間到期之前就返回(有I/O事件發生),所以我們如果要利用它們來定時,就需要不斷更新定時引數以反映剩餘的事件。 

/*
 *Linux下的3組I/O服用系統呼叫都帶有超時引數,因此它們不僅能統一處理訊號和I/O事件,
也能統一處理定時事件。但是由於I/O複用系統呼叫可能在超時時間到期之前就返回,所以如
果我們要利用它們來定時,就需要不斷更新定時引數以反映剩餘的時間。
 */
#define TIMEOUT 5000
 
 
int timeout = TIMEOUT;
time_t start = time( NULL );
time_t end = time( NULL );
while( 1 )
{
    printf( "the timeout is now %d mill-seconds\n", timeout );
    start = time( NULL );
    int number = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, timeout );
    if( ( number < 0 ) && ( errno != EINTR ) )
    {
        printf( "epoll failure\n" );
        break;
    }
    /*如果epoll_wait成功返回0,則說明超時時間到,此時便可處理定時任務,並重置定時
     * 時間*/
    if( number == 0 )
    {
        // timeout
        timeout = TIMEOUT;
        continue;
    }
 
 
    end = time( NULL );
    /*如果epoll_wait返回值大於0,則本次epoll_wait呼叫持續的時間時(end - start)
     *1000ms,我們需要將定時時間timeout減去這段時間,以獲得下次epoll_wait呼叫的超時
     引數
     */
    timeout -= ( end - start ) * 1000;
    /*
     * 重新計算之後的timeout值有可能等於0,說明本次epoll_wait呼叫返回時,不僅有檔案描述
     * 符就緒,並且其超時時間也剛好到達,此時我們也要處理定時任務,並重置定時時間。
     * */
    if( timeout <= 0 )
    {
        // timeout
        timeout = TIMEOUT;
    }
 
 
    // handle connections
}

高效能定時器

時間輪

基於排序連結串列的定時器存在一個問題:新增定時器的效率偏低。下面我們討論的時間輪解決了這個問題。

上面所示的時間輪,(實線)指標指向輪子上的一個槽slot,它以恆定的速度順時針轉動,每轉動一步就指向下一個槽(虛線指標指向的槽),每次轉動稱為一個滴答(tick),一個滴答時間稱為時間輪槽間隔si,它實際上就是心搏時間。該時間輪有N個槽,因此它沒轉動一週的時間就是N*si。每個槽指向一條定時器連結串列,每條連結串列上的定時器具有相同的特徵:他們的定時時間相差N*si的整數倍。時間輪正是利用這個關係將定時器雜湊到不同的連結串列中。假如現在指標指向槽cs,我們要新增一個定時時間為ti的定時器,則該定時器被插入槽ts對應的連結串列中:      

ts=(cs+(ti/si))%N    

基於排序連結串列的定時器使用唯一的一條連結串列來管理所有定時器,所以插入操作的效率隨著定時器目的增多而降低。而時間輪使用雜湊表的思想,將定時器雜湊到不同的連結串列中。這樣每條連結串列上的定時器數目都將明顯少於原來的排序連結串列上的定時器資料,插入操作的效率基本不受定時器的數目影響。    

很顯然,對時間輪而言,要提高定時精度,就要使si值足夠小,要提高執行效率,則要求N值足夠大(這樣的每個槽內的連結串列中的定時器數目就會減少)。 

對時間輪而言,新增一個定時器的時間複雜度是O(1),刪除一個定時器的時間複雜度也是O(1),執行一個定時器的時間複雜度是O(n),但實際上執行一個定時器任務的效率比O(n)好的多,因為時間輪將所有的定時器雜湊到不同的連結串列上了。時間輪的槽越多,等價散列表的入口越多,從而每條連結串列上的定時器數量越少。效率提升。 

時間堆

時間輪中的定時方案是以固定的頻率呼叫心搏函式tick,並在其中依此檢測到期的定時器,然後執行到期定時器上的回撥函式。設計定時器的另一種思路是:將所有定時器中超時時間最小的一個定時器的超時值作為心搏間隔。這樣,一旦心搏函式tick被呼叫,超時時間最小的定時器必然到期,我們就可以在tick函式中處理該定時器。然後,再次從剩餘的定時器中找出超時時間最小的一個,並將這段最小時間設定為笑一次心搏間隔。如此反覆,就實現了較為精確的定時。最小堆很適合處理這種定時方案,而且由於最小堆是一種完全二叉樹,可以用陣列來組織其中的元素。

對時間堆而言,新增一個定時器的複雜度是O(lgn),刪除一個定時器的時間複雜度是O(1),執行一個定時器的時間複雜度是O(1)。因此,時間堆的效率是很高的。