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_...
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手册,并加上了自己的理解。
如果有写的不对的地方,希望能留言指正,谢谢阅读。
更多推荐
所有评论(0)