Linux下定時函數timerfd_xxx()的使用

Linux系統提供了timerfd系列的定時函數,其具體函數名如下,

#include <sys/timerfd.h>

int timerfd_create(int clockid, int flags);

int timerfd_settime(int fd, int flags,
                    const struct itimerspec *new_value,
                    struct itimerspec *old_value);

int timerfd_gettime(int fd, struct itimerspec *curr_value);

timerfd函數可以通過文件描述符來通知定時事件,這就使得定時器也符合Linux下一切皆文件的哲學思想,並且可以很方便的使用select, poll和epoll去監測定時器事件。

下面就介紹這三個系統函數的基本用法。


timerfd_create()

原型如下,

int timerfd_create(int clockid, int flags);

該函數創建一個新的定時器對象,並返回一個文件描述符,指向這個新生成的定時器。其第一個參數clockid用來選擇定時器的時鐘,有以下選擇,

  • CLOCK_REALTIME:系統實時時鐘,用戶可以修改,可以簡單理解爲win10電腦右下角的時間
  • CLOCK_MONOTONIC:一個不可變的單調遞增時鐘,當系統掛起時,時鐘不會包含掛起的這段時間值,相當於系統掛起時就停止計時,系統恢復運行後繼續計時。可以簡單理解爲一個CPU寄存器,當系統啓動後就開始遞增計時,系統關閉後就清0,用戶只能讀無法寫。
  • CLOCK_BOOTTIME (since Linux 3.15):與CLOCK_MONOTONIC類似,只是當系統掛起時,該時鐘會把系統掛起的這段時間算進來,相當於系統掛起時該時鐘仍然會繼續計時
  • CLOCK_REALTIME_ALARM (since Linux 3.11):與CLOCK_REALTIME類似,但是當系統掛起時,這個時鐘會把系統喚醒,調用者需要具有CAP_WAKE_ALARM的能力
  • CLOCK_BOOTTIME_ALARM (since Linux 3.11):與CLOCK_BOOTTIME 類似,但是當系統掛起時,這個時鐘會把系統喚醒,調用者需要具有CAP_WAKE_ALARM的能力

以上是翻譯man手冊,其實看完還是有點懵。簡單的說,CLOCK_REALTIME對應的時鐘記錄的是相對時間,即從1970年1月1日0點0分到目前的時間;CLOCK_MONOTONIC對應的時鐘記錄的是系統啓動後到現在的時間

對於CLOCK_REALTIME來說,更改系統時間會影響算出來的時間值;對於CLOCK_MONOTONIC來說更改系統時間不會影響算出來的時間值。因爲用戶可以隨意修改系統時間,但是對於CLOCK_MONOTONIC來說,它的計時是由CPU上的一個計時器提供,不受系統時間影響。

可以根據實際需要來選擇時鐘,一般來說,選擇CLOCK_REALTIMECLOCK_MONOTONIC就可以了。

第二個參數flags,從Linux 2.6.27開始,可以是以下2個標誌之一,也可以使用or操作符"|"來把2個標誌都置起來,也可以直接給個0,具體根據實際來,

  • TFD_NONBLOCK :對定時器的文件描述符設置O_NONBLOCK的文件狀態標誌,形成非阻塞調用
  • TFD_CLOEXEC :對定時器的文件描述符設置close-on-exec (FD_CLOEXEC)標誌,這個標誌的意思就是如果創建定時器的進程在創建了定時器後又fork了一個新進程,那麼新進程在生成過程中就把原本共享的定時器文件描述符給關掉,免得互相影響

timerfd_settime()

原型如下,

int timerfd_settime(int fd, int flags,
                    const struct itimerspec *new_value,
                    struct itimerspec *old_value);

該函數可以啓動或停止一個定時器,這個定時器由第一個參數fd表示,這個fd就是timerfd_create()創建定時器時返回的文件描述符。

參數new_value用於指定定時器的第一次到期時間和後續的定時週期,其類型爲struct itimerspec,定義如下,

struct itimerspec {
    struct timespec it_interval;  /* Interval for periodic timer */
    struct timespec it_value;     /* Initial expiration */
};

struct timespec {
    time_t tv_sec;                /* Seconds */
    long   tv_nsec;               /* Nanoseconds */
};

可以看出定時器可以精確到納秒。

new_value參數解析:

  • new_value裏的it_value表示啓動定時器後,定時器第一次定時到期的時間,如果it_value的tv_sec或tv_nsec值非0,那麼定時器在調用timerfd_settime()之後就會啓動,如果it_value的tv_sec和tv_nsec值都是0,那麼定時器在調用timerfd_settime()之後就會停止。
  • new_value裏的it_interval用來設置後續的定時週期,如果it_interval的tv_sec或tv_nsec值非0,那麼該定時器就具有周期性,如果tv_sec和tv_nsec值都是0,那麼這個定時器就是one shot的,只定時一次。

第二個參數flags可以是0,意思是使用相對定時器,也可以有以下2個選項(使用其中之一或兩個都要,如果2個都要就使用or操作符"|"來把2個標誌都置起來),

  • TFD_TIMER_ABSTIME:使用絕對定時器,new_value裏的it_value是相對於選擇的時鐘的絕對時間(假如調用timerfd_settime()時clock的時間是8:00,此時flag爲0,使用相對定時器,傳遞的new_value.it_value是10分鐘,那麼定時器就會在8:10觸發;如果flag使用了TFD_TIMER_ABSTIME標誌,使用絕對定時器,那麼new_value.it_value就需要傳遞8:10,使用絕對時間)
  • TFD_TIMER_CANCEL_ON_SET:只有在timerfd_create()裏選擇的時鐘是CLOCK_REALTIME或CLOCK_REALTIME_ALARM纔有意義,並且必須與標誌TFD_TIMER_ABSTIME一起使用。當時鍾經常發生不連續的變化時,這個定時器是可以取消的。前面也提到過,CLOCK_REALTIME對應的時鐘是可變的。

對於old_value這個參數,一般來說傳個NULL就行了,如果不是NULL,那麼就會返回定時器的先前配置信息,可以這樣理解:假如在調用timerfd_settime()之前,已經調用過timerfd_settime()來配置定時器了,那麼本次調用timerfd_settime()就會在old_value裏返回之前的定時器配置;或者剛剛創建定時器,那麼調用timerfd_settime()就會在old_value裏返回定時器的默認配置。


timerfd_gettime()

原型如下,

int timerfd_gettime(int fd, struct itimerspec *curr_value);

這個函數相對來說比較簡單,就是獲取fd指向的定時器的當前設置,注意這裏說的是當前設置,一旦定時器啓動,那麼離第一次到期的定時時間就會越來越近,其當前設置就會不斷的變化,所以curr_value裏的it_value表示的是當前時間(調用timerfd_gettime時)到第一次到期時間之間的剩餘時間


舉例

下面以代碼來講解如何使用timerfd系列函數來實現定時功能,促進對前面用法的理解。本示例使用read()對定時器進行監測,如果有多個定時器需要監測,則推薦使用select,poll或epoll。

1. one shot定時器:
#include <sys/timerfd.h>
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>


#define handle_error(msg) \
       do { perror(msg); exit(EXIT_FAILURE); } while (0)

void print_elapsed_time(void);

int main(void)
{
    int timerfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
    if (timerfd == -1)
    {
        handle_error("timerfd_create");
    }

    struct itimerspec new_value = {};
    new_value.it_value.tv_sec  = 10; // 10s
    new_value.it_value.tv_nsec = 0;
    
    new_value.it_interval.tv_sec  = 0; // one shot
    new_value.it_interval.tv_nsec = 0;

    if (timerfd_settime(timerfd, 0, &new_value, NULL) == -1)
    {
        handle_error("timerfd_settime");
    }

    print_elapsed_time();
    printf("timer started\n");


    uint64_t exp = 0;
    while (1)
    {
        int ret = read(timerfd, &exp, sizeof(uint64_t));
        
        if (ret == sizeof(uint64_t)) // 第一次定時到期
        {
            printf("ret: %d\n", ret);
            printf("expired times: %llu\n", exp);
            break;
        }

        // struct itimerspec curr;
        // if (timerfd_gettime(timerfd, &curr) == -1)
        // {
        //     handle_error("timerfd_gettime");
        // }
        // printf("remained time: %lds\n", curr.it_value.tv_sec);

        print_elapsed_time();
    }

    return 0;

}



void print_elapsed_time(void)
{
    static struct timeval start = {};
    static int first_call = 1;

    if (first_call == 1)
    {
        first_call = 0;
        if (gettimeofday(&start, NULL) == -1)
        {
            handle_error("gettimeofday");
        }
    }

    struct timeval current = {};
    if (gettimeofday(&current, NULL) == -1)
    {
        handle_error("gettimeofday");
    }

    static int old_secs = 0, old_usecs = 0;

    int secs  = current.tv_sec - start.tv_sec;
    int usecs = current.tv_usec - start.tv_usec;
    if (usecs < 0)
    {
        --secs;
        usecs += 1000000;
    }

    usecs = (usecs + 500)/1000; // 四捨五入

    if (secs != old_secs || usecs != old_usecs)
    {
    	printf("%d.%03d\n", secs, usecs);
    	old_secs = secs;
    	old_usecs = usecs;
    }
}

代碼解析:

  • 定時器第一次到期時間是10s
  • print_elapsed_time()裏使用gettimeofday()來打印定時器啓動後流逝的時間,gettimeofday的使用可以參照這篇文章
  • 使用read來查看定時器是否到期,一旦到期,read就會返回一個8字節的整數,類型是uint64_t,這個整數表示定時器到期的次數,由於本代碼是one shot的,所以會返回1,意思是隻到期一次
  • 定時器創建時指定了TFD_NONBLOCK標誌,所以read時不會阻塞
  • 代碼裏調用了timerfd_gettime,可以看到定時器的配置是不斷變化的,這部分代碼暫時註釋了,可以自己打開編譯運行,然後看看效果
2. 週期定時器
#include <sys/timerfd.h>
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>


#define handle_error(msg) \
       do { perror(msg); exit(EXIT_FAILURE); } while (0)

void print_elapsed_time(void);

int main(void)
{
    int timerfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
    if (timerfd == -1)
    {
        handle_error("timerfd_create");
    }

    struct itimerspec new_value = {};
    new_value.it_value.tv_sec  = 10; // 10s
    new_value.it_value.tv_nsec = 0;

    new_value.it_interval.tv_sec  = 5; // 5s cycle
    new_value.it_interval.tv_nsec = 0;

    if (timerfd_settime(timerfd, 0, &new_value, NULL) == -1)
    {
        handle_error("timerfd_settime");
    }

    print_elapsed_time();
    printf("timer started\n");

    uint64_t exp = 0;
    while (1)
    {
        int ret = read(timerfd, &exp, sizeof(uint64_t));
        if (ret == sizeof(uint64_t)) 
        {
            printf("ret: %d\n", ret);
            printf("===> expired times: %llu\n", exp);
        }
        print_elapsed_time();
    }

    return 0;

}



void print_elapsed_time(void)
{
    static struct timeval start = {};
    static int first_call = 1;

    if (first_call == 1)
    {
        first_call = 0;
        if (gettimeofday(&start, NULL) == -1)
        {
            handle_error("gettimeofday");
        }
    }

    struct timeval current = {};
    if (gettimeofday(&current, NULL) == -1)
    {
        handle_error("gettimeofday");
    }

    static int old_secs = 0, old_usecs = 0;

    int secs  = current.tv_sec - start.tv_sec;
    int usecs = current.tv_usec - start.tv_usec;
    if (usecs < 0)
    {
        --secs;
        usecs += 1000000;
    }

    usecs = (usecs + 500)/1000; // 四捨五入

    if (secs != old_secs || usecs != old_usecs)
    {
    	printf("%d.%03d\n", secs, usecs);
    	old_secs = secs;
    	old_usecs = usecs;
    }

}

代碼解析:

  • 定時器第一次到期時間是10s,後續定時週期爲5s
  • 因爲每次到期我們都會去做read,所以每次到期時,read的返回值都是1;如果到期了沒有去做read,那麼此時去做read,就會返回先前的到期次數累加和

總結

本文講述timerfd系列函數的基本用法,主要參考了Linux的man手冊,並加上了自己的理解。

如果有寫的不對的地方,希望能留言指正,謝謝閱讀。

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