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手册,并加上了自己的理解。

如果有写的不对的地方,希望能留言指正,谢谢阅读。

Logo

更多推荐