文章目录
- 前言
- 认识 timerfd
- API timerfd
- API clock
- 官方示例
- 简单使用
- epoll实现
前言
在 Linux 系统编程中,使用 timerfd
实现定时器功能是一种更加可靠、高效、灵活的方式。本文是对 timerfd
的简单使用,不涉及太过深入知识,熟练掌握几个常用 API,实现定时器功能。
认识 timerfd
timerfd
是 Linux 为用户程序提供的一个定时器接口。这个接口基于文件描述符,通过文件描述符的可读事件进行超时通知,它可以用来实现高精度的定时器,以及与 I/O
多路复用机制(如select
、poll
、epoll
等)进行配合,实现异步事件驱动的程序。
优点:
-
高精度:timerfd使用内核提供的高精度计时器,精度通常可以达到纳秒级别,相对于使用系统时间的方式,误差更小,定时器的精度更高。
-
资源占用低:timerfd使用内核提供的异步I/O机制,可以将定时器超时事件通知给应用程序,而不需要应用程序不断地轮询,从而避免了大量的CPU资源占用,提高了系统性能。
-
灵活性强:timerfd可以设置不同的定时器超时时间,并且可以同时管理多个定时器,可以根据应用程序的需要进行灵活配置。
-
可移植性好: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杀死信号后结束程序)