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_REALTIME或CLOCK_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(¤t, 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(¤t, 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手冊,並加上了自己的理解。
如果有寫的不對的地方,希望能留言指正,謝謝閱讀。