【Linux】timerfd——定时器

前言

在 Linux 系统编程中,使用 timerfd 实现定时器功能是一种更加可靠、高效、灵活的方式。本文是对 timerfd 的简单使用,不涉及太过深入知识,熟练掌握几个常用 API,实现定时器功能。

认识 timerfd

timerfd 是 Linux 为用户程序提供的一个定时器接口。这个接口基于文件描述符,通过文件描述符的可读事件进行超时通知,它可以用来实现高精度的定时器,以及与 I/O 多路复用机制(如selectpollepoll等)进行配合,实现异步事件驱动的程序。

优点:

  1. 高精度:timerfd使用内核提供的高精度计时器,精度通常可以达到纳秒级别,相对于使用系统时间的方式,误差更小,定时器的精度更高。

  2. 资源占用低:timerfd使用内核提供的异步I/O机制,可以将定时器超时事件通知给应用程序,而不需要应用程序不断地轮询,从而避免了大量的CPU资源占用,提高了系统性能。

  3. 灵活性强:timerfd可以设置不同的定时器超时时间,并且可以同时管理多个定时器,可以根据应用程序的需要进行灵活配置。

  4. 可移植性好:timerfd是Linux内核提供的标准接口,可以在不同的Linux系统上使用,具有很好的可移植性。

API timerfd

常用的 API 接口:

#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_create
timerfd_create(CLOCK_REALTIME, 0); // 使用系统实时时间作为计时基准,阻塞模式。

clockid:
	CLOCK_REALTIME:使用系统实时时钟,可以被修改;
	CLOCK_MONOTONIC:使用系统单调时钟,不能被修改;
flags:
	TFD_NONBLOCK:非阻塞模式,read操作不会阻塞;
	TFD_CLOEXEC:创建的文件描述符将在执行exec时关闭。
  • timerfd_settime
timerfd_settime(fd, TFD_TIMER_ABSTIME, &new_value, NULL); 
// 计时器文件描述符fd,初始状态new_value,并且采用绝对时间。(0:相对,1:绝对)

fd:   		 timerfd_create函数创建的定时器文件描述符
flags:		 可以设置为0或者TFD_TIMER_ABSTIME,用于指定定时器的绝对时间或者相对时间
new_value:	 是一个指向itimerspec结构体的指针,用于指定新的定时器参数;
old_value:  是一个指向itimerspec结构体的指针,用于获取旧的定时器参数。
  • timerfd_gettime
// timerfd_gettime是一个用于获取timerfd定时器的当前超时时间的系统调用。
// 它可以用于查询定时器的当前状态,以便在需要时进行调整。

curr_value是指向itimerspec结构体的指针,用于存储当前定时器的超时时间和间隔。

  • itimerspec 结构体定义如下:
struct timespec {
    time_t tv_sec;   // seconds
    long   tv_nsec;  // nanoseconds
};

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

API clock

#include <time.h>
int clock_settime(clockid_t clockid, const struct timespec *tp);
int clock_gettime(clockid_t clockid, struct timespec *tp);
  • clock_settime
// 用于设置系统时钟的时间
// clock_settime函数所设置的时间值会影响到所有基于该时钟的时间函数,例如gettimeofday、clock_gettime等。
// 在Linux系统中,只有特权进程(即具有root权限的进程)才能修改系统时钟。
  • clock_gettime
clock_gettime(CLOCK_MONOTONIC, &start); // 基于系统启动时间的单调递增时钟,获取到的结构体值赋给start

clockid:
	CLOCK_MONOTONIC:提供了一个单调递增的时钟,不受系统时间被改变的影响。
	CLOCK_REALTIME:供了当前的日期和时间,但是受系统时间被改变的影响。

官方示例

官方简单示例:(man手册查看)

#include <sys/timerfd.h>
#include <time.h>
#include <unistd.h>
#include <inttypes.h>      /* Definition of PRIu64 */
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>        /* Definition of uint64_t */

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

static void
print_elapsed_time(void)
{
    static struct timespec start; // 静态,只用于第一次赋值
    struct timespec curr;
    static int first_call = 1;
    int secs, nsecs;

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

    if (clock_gettime(CLOCK_MONOTONIC, &curr) == -1)
        handle_error("clock_gettime");

    secs = curr.tv_sec - start.tv_sec;
    nsecs = curr.tv_nsec - start.tv_nsec;
    if (nsecs < 0) {
        secs--;
        nsecs += 1000000000;
    }
    printf("%d.%03d: ", secs, (nsecs + 500000) / 1000000);
}

int
main(int argc, char *argv[])
{
    struct itimerspec new_value;
    int max_exp, fd;
    struct timespec now;
    uint64_t exp, tot_exp;
    ssize_t s;

    if ((argc != 2) && (argc != 4)) {
        fprintf(stderr, "%s init-secs [interval-secs max-exp]\n", // 这里需要输入参数:(第一次到期,间隔,次数)
                argv[0]);
        exit(EXIT_FAILURE);
    }

    if (clock_gettime(CLOCK_REALTIME, &now) == -1)
        handle_error("clock_gettime");

    /* Create a CLOCK_REALTIME absolute timer with initial
        expiration and interval as specified in command line */

    new_value.it_value.tv_sec = now.tv_sec + atoi(argv[1]);
    new_value.it_value.tv_nsec = now.tv_nsec;
    if (argc == 2) {
        new_value.it_interval.tv_sec = 0;
        max_exp = 1;
    } else {
        new_value.it_interval.tv_sec = atoi(argv[2]);
        max_exp = atoi(argv[3]);
    }
    new_value.it_interval.tv_nsec = 0;

    fd = timerfd_create(CLOCK_REALTIME, 0); 
    if (fd == -1)
        handle_error("timerfd_create");

    if (timerfd_settime(fd, TFD_TIMER_ABSTIME, &new_value, NULL) == -1)
        handle_error("timerfd_settime");

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

    for (tot_exp = 0; tot_exp < max_exp;) {
        s = read(fd, &exp, sizeof(uint64_t));
        if (s != sizeof(uint64_t))
            handle_error("read");

        tot_exp += exp;
        print_elapsed_time();
        printf("read: %" PRIu64 "; total=%" PRIu64 "\n", exp, tot_exp);
    }

    exit(EXIT_SUCCESS);
}

简单使用

timerfd 的基本使用:

#include <stdio.h>
#include <errno.h> // EAGAIN
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <signal.h>
#include <stdbool.h>
#include <pthread.h>
#include <inttypes.h>
#include <sys/time.h>
#include <sys/timerfd.h>

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

int g_exp = 1; // 第四个参数,执行次数
int g_timerfd = -1; // 计时器fd
struct timespec g_current; // 当前时间结构体
bool g_running = true;

void stop_timer(int); // 前置声明

void signal_handler(int signum) {
    stop_timer(g_timerfd);
    close(g_timerfd);
    g_timerfd = -1;
    g_running = false;
    // exit(EXIT_FAILURE);
}

// man timerfd_create
int create_timer() {
    // int fd = timerfd_create(CLOCK_MONOTONIC, 0);
    int fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
    if (fd == -1) 
        handle_error("create_timer failed");
    return fd;
}

void init_timer(int timerfd, time_t secs, long nsecs, time_t interval_secs, long interval_nsecs) {
    struct itimerspec new_value;
    new_value.it_value.tv_sec = secs;
    new_value.it_value.tv_nsec = nsecs;

    new_value.it_interval.tv_sec = interval_secs;
    new_value.it_interval.tv_nsec = interval_nsecs;
    
    if (timerfd_settime(timerfd, 0, &new_value, NULL) == -1)
        handle_error("init_time failed");
}

void stop_timer(int timerfd) {
    struct itimerspec new_value;
    new_value.it_value.tv_sec = 0;
    new_value.it_value.tv_nsec = 0;

    new_value.it_interval.tv_sec = 0;
    new_value.it_interval.tv_nsec = 0;

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

// 打印间隔时间
static void print_elapsed_time() {
    static struct timespec start;
    static int first_call = 1; 

    if (first_call) {
        first_call = 0;
        if (clock_gettime(CLOCK_MONOTONIC, &start) == -1)
            handle_error("clock_gettime failed");
    }

    if (clock_gettime(CLOCK_MONOTONIC, &g_current) == -1)
        handle_error("clock_gettime failed");

    time_t secs = g_current.tv_sec - start.tv_sec;
    long  nsecs = g_current.tv_nsec - start.tv_nsec;
    if (nsecs < 0) {
        secs -= 1;
        nsecs += 1000000000;
    }
    printf("%ld.%03ld: ", secs, (nsecs + 500000) / 1000000);
}

// 工作线程
void *work_function(void *timerfd) {
    int fd = *(int *)timerfd; 

    uint64_t exp; 
    for (int cnt = 0; cnt < g_exp && g_running; cnt++) {
        int ret = read(fd, &exp, sizeof(uint64_t));
        if (ret == sizeof(uint64_t)) {
            print_elapsed_time();
        } else { // 非阻塞模式,死循环,消耗CPU
            cnt -= 1;
        }
    }
    return NULL;
}

int main(int argc, char *argv[]) {
    // 注册信号
    signal(SIGINT, signal_handler); // kill -l 

    if (argc != 4) {
        fprintf(stderr, "Usage %s init-secs interval-secs max-exp\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    
    g_timerfd = create_timer();
    init_timer(g_timerfd, atoi(argv[1]), 0, atoi(argv[2]), 0);
    g_exp = atoi(argv[3]);
    print_elapsed_time(); // 第一次

    pthread_t tid; 
    pthread_create(&tid, NULL, work_function, &g_timerfd);
    pthread_join(tid, NULL);
    return 0;   
}

上例程序是非阻塞的timerfd,极大占用CPU资源。

在这里插入图片描述


epoll实现

timerfd、eventfd、signalfd配合epoll使用,可以构造出一个零轮询的程序,但程序没有处理的事件时,程序是被阻塞的。epoll_wait阻塞监听事件。

  • eventfd
#include <sys/eventfd.h>
int eventfd(unsigned int initval, int flags);
// 其中initval参数是eventfd的初始值,通常设置为0即可,表示初始时没有事件发生。
// flags参数是eventfd的标志位,通常设置为0即可,表示没有特殊要求。
// 函数返回值是eventfd的文件描述符,可以用于后续的读写操作。

eventfd是Linux系统调用中的一种文件描述符类型,它可以用于在进程间传递事件通知。当一个进程调用eventfd创建一个事件文件描述符时,它会得到一个文件描述符,可以使用该文件描述符进行读写操作。当另一个进程写入数据到该文件描述符时,它会唤醒正在等待读取该文件描述符的进程。这样,可以实现进程间的事件通知,而无需使用显式的信号量或者管道等机制。

eventfd的使用非常简单,只需要调用eventfd函数创建一个事件文件描述符,然后使用read和write函数进行读写操作即可。eventfd还支持select、poll、epoll等多路复用机制,可以方便地将其集成到事件驱动的程序中。


代码如下:

  • timerfd设置为非阻塞状态:
fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
  • 创建eventfd
g_eventfd = eventfd(0, 0);
  • 注册epoll事件监听
int epollfd = epoll_create(2);
if (epollfd == -1)
    handle_error("epoll_create failed");

struct epoll_event ev_timer;
ev_timer.events = EPOLLIN;
ev_timer.data.fd = fd;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev_timer);

struct epoll_event ev_event;
ev_event.events = EPOLLIN;
ev_event.data.fd = g_eventfd;
epoll_ctl(epollfd, EPOLL_CTL_ADD, g_eventfd, &ev_event);

const int max_events = 2;
struct epoll_event events[max_events];

详见代码:

#include <stdio.h>
#include <errno.h> // EAGAIN
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <signal.h>
#include <stdbool.h>
#include <pthread.h>
#include <inttypes.h>
#include <sys/time.h>
#include <sys/epoll.h> 
#include <sys/eventfd.h>
#include <sys/timerfd.h>

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

int g_exp = 1; // 第四个参数,执行次数
int g_timerfd = -1; // 计时器fd
struct timespec g_current; // 当前时间结构体
bool g_running = true;
int g_eventfd; // a file descriptor for event notification

void stop_timer(int); // 前置声明

void signal_handler(int signum) {
    stop_timer(g_timerfd);

    g_timerfd = -1;
    g_running = false;
    
    uint64_t g_eventfd_val = 10086; // 64位无符号!!!;不要用 -1
    write(g_eventfd, &g_eventfd_val, sizeof(uint64_t));
}

// man timerfd_create
int create_timer() {
    // int fd = timerfd_create(CLOCK_MONOTONIC, 0);
    int fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
    if (fd == -1) 
        handle_error("create_timer failed");
    return fd;
}

void init_timer(int timerfd, time_t secs, long nsecs, time_t interval_secs, long interval_nsecs) {
    struct itimerspec new_value;
    new_value.it_value.tv_sec = secs;
    new_value.it_value.tv_nsec = nsecs;

    new_value.it_interval.tv_sec = interval_secs;
    new_value.it_interval.tv_nsec = interval_nsecs;
    
    if (timerfd_settime(timerfd, 0, &new_value, NULL) == -1)
        handle_error("init_time failed");
}

void stop_timer(int timerfd) {
    struct itimerspec new_value;
    new_value.it_value.tv_sec = 0;
    new_value.it_value.tv_nsec = 0;

    new_value.it_interval.tv_sec = 0;
    new_value.it_interval.tv_nsec = 0;

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

// 打印间隔时间
static void print_elapsed_time() {
    static struct timespec start;
    static int first_call = 1; 

    if (first_call) {
        first_call = 0;
        if (clock_gettime(CLOCK_MONOTONIC, &start) == -1)
            handle_error("clock_gettime failed");
    }

    if (clock_gettime(CLOCK_MONOTONIC, &g_current) == -1)
        handle_error("clock_gettime failed");

    time_t secs = g_current.tv_sec - start.tv_sec;
    long  nsecs = g_current.tv_nsec - start.tv_nsec;
    if (nsecs < 0) {
        secs -= 1;
        nsecs += 1000000000;
    }
    printf("%ld.%03ld\n", secs, (nsecs + 500000) / 1000000);
}

// 工作线程
void *work_function(void *timerfd) {
    int fd = *(int *)timerfd; 

    int epollfd = epoll_create(2);
    if (epollfd == -1)
        handle_error("epoll_create failed");
    
    struct epoll_event ev_timer;
    ev_timer.events = EPOLLIN;
    ev_timer.data.fd = fd;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev_timer);

    struct epoll_event ev_event;
    ev_event.events = EPOLLIN;
    ev_event.data.fd = g_eventfd;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, g_eventfd, &ev_event);

    const int max_events = 2;
    struct epoll_event events[max_events];

    uint64_t exp; 
    for (int cnt = 0; cnt < g_exp && g_running; cnt++) {
        int nfd = epoll_wait(epollfd, events, max_events, -1);

        if (nfd > 0) {
            for (int i = 0; i < nfd; i++) {
                if (events[i].data.fd == fd) {
                    int ret = read(fd, &exp, sizeof(uint64_t));
                    if (ret == sizeof(uint64_t)) {
                        print_elapsed_time();
                    }
                } else if (events[i].data.fd == g_eventfd) {
                    int ret = read(g_eventfd, &exp, sizeof(uint64_t));
                    if (ret == sizeof(uint64_t)) {
                        if (exp == 10086) { // 64位无符号!!!;不要用 -1
                            close(fd);
                            epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, NULL);
                            close(g_eventfd);
                            epoll_ctl(epollfd, EPOLL_CTL_DEL, g_eventfd, NULL);
                            return NULL;
                        }
                    }
                }
            }
        }
    }
    close(fd);
    epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, NULL);
    close(g_eventfd);
    epoll_ctl(epollfd, EPOLL_CTL_DEL, g_eventfd, NULL);
    return NULL;
}

int main(int argc, char *argv[]) {
    // 注册信号
    // signal(SIGINT, signal_handler); // kill -l 
    signal(SIGUSR1, signal_handler); // kill -SIGUSR1 <pid>: 使用 SIGUSR1 和 SIGUSR2 信号来进行信号传递。这两个信号是专门为用户定义的信号,可以用于进程间通信或者在同一进程的不同线程之间进行通信。

    if (argc != 4) {
        fprintf(stderr, "Usage %s init-secs interval-secs max-exp\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    
    g_eventfd = eventfd(0, 0);
    if (g_eventfd == -1) 
        handle_error("eventfd failed");

    g_timerfd = create_timer();
    init_timer(g_timerfd, atoi(argv[1]), 0, atoi(argv[2]), 0);
    g_exp = atoi(argv[3]);
    print_elapsed_time(); // 第一次

    pthread_t tid; 
    pthread_create(&tid, NULL, work_function, &g_timerfd);
    pthread_join(tid, NULL);
    return 0;   
}

运行示例:(1s第一次到期,每个3s定时一次,总共10次,kill杀死信号后结束程序)

在这里插入图片描述

  • 2
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ღCauchyོꦿ࿐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值