timerfd API
从版本2.6.25开始,Linux内核提供了另一种创建定时器的API。Linux特有的timerfd API,可以从文件描述符中读取器所创建定时器的到期通知。因为可以使用select()、poll()和epoll()将这种文件描述符与其他描述符一同进行监控,所以非常使用。
NAME
timerfd_create, timerfd_settime, timerfd_gettime - 通过文件描述符通知的计时器
SYNOPSIS
#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);
DESCRIPTION
这些系统调用创建并操作通过文件描述符传递计时器过期通知的计时器。它们提供了使用setitimer(2) / timer_create(2)的替代方法,其优点是文件描述符可以由 select(2), poll(2), epoll(7)监视。
这组 API 中的 3 个新系统调用,其操作与 timer_create()、timer_settime()和timer_gettime()
相类似 (没有类似的timer_getoverrun(2),因为该功能由 read(2), 提供,如下所述)
-
新加入的第 1 个系统调用是 timerfd_create(),它会创建一个新的定时器对象,并返回一个指代该对象的文件描述
- 参数 clockid 的值可以设置为 CLOCK_REALTIME 或 CLOCK_MONOTONIC。
- timerfd_create()的最初实现将参数 flags 预留供未来使用,必须设置为 0。不过,Linux 内核从 2.6.27 版本开始支持下面两种 flags 标志。
- TFD_CLOEXEC : 为新的文件描述符设置运行时关闭标志(FD_CLOEXEC)。与 4.3.1 节介绍的 open()标志O_CLOEXEC 适用于相同情况
- TFD_NONBLOCK :为底层的打开文件描述设置 O_NONBLOCK 标志,随后的读操作将是非阻塞式的。这样设置省却了对 fcntl()的额外调用,却能达到相同效果
-
timerfd_create()创建的定时器使用完毕后,应调用 close()关闭相应的文件描述符,以便于内核能够释放与定时器相关的资源。
-
系统调用 timerfd_settime()可以启动或停止由文件描述符 fd 所指代的定时器
- 参数 new_value 为定时器指定新设置。
- 参数 old_value 可用来返回定时器的前一设置。如果不关心定时器的前一设置,可将 old_value 置为 NULL
- 参数 flags 与 timer_settime()中的对应参数类似
- 可以是 0,此时将 new_value.it_value 的值视为相对于调用 timerfd_settime()时间点的相对时间
- 也可以设为 TFD_TIMER_ABSTIME,将其视为一个绝对时间(从时钟的 0 点开始测量)
-
系统调用 timerfd_gettime()返回文件描述符 fd 所标识定时器的间隔及剩余时间
- 同 timer_gettime()一样,间隔以及距离下次到期的时间均返回 curr_value 指向的结构itimerspec 中。
- 即使是以 TFD_TIMER_ABSTIME 标志创建的绝对时间定时器,curr_vallue. it_value 字段中返回值的意义也会保持不变
- 如果返回的结构 curr_value.it_value 中所有字段值均为 0,那么该定时器已经被解除。如果返回的结构 curr_value.it_interval 中两字段值均为 0,那么定时器只会到期一次,到期时间在 curr_value.it_value 中给出。
timerfd 与 fork()及 exec()之间的交互
- 调用fork()期间,子进程会继承timerfd_create()所创建文件描述符的拷贝。这些描述符与父进程的对应描述符均指代相同的定时器对象,任一进程都可读取定时器的到期时间
- timerfd_create()创建的文件描述符能跨越 exec()得以保存(除非将描述符置为运行时关闭,),已配备的定时器在 exec()之后会继续生成到期通知。
timerfd 文件描述符读取
- 一旦以 timerfd_settime()启动了定时器,就可以从相应文件描述符中调用 read()来读取定时器的到期信息。出于这一目的,传给 read()的缓冲区必须足以容纳一个无符号 8 字节整型(uint64_t)数
- 在上次使用timefd_settime()修改设置以后,或是最后一次执行read()后,如果发生了一起到多起定时器到期事件,那么read()会立即返回,且返回的缓冲区中包含了已发生的到期次数。如果并无定时器到期,read()会一直阻塞直至产生下一个到期。
- 也可以执行fcntl()的 F_SETFL 操作为文件描述符设置 O_NONBLOCK 标志,这时的读动作是非阻塞式的,且如果没有定时器到期,则返回错误,并将 errno 值置为 EAGAIN。
- 可以利用 select()、poll()和 epoll()对 timerfd 文件描述符进行监控。如果定时器到期,会将对应的文件描述符标记为可读
示例
#include <cstring>
#include <signal.h>
#include <stdio.h>
#include <cstdlib>
#include <zconf.h>
#include <sys/signalfd.h>
#include <signal.h>
#include <sys/timerfd.h>
#include <time.h>
#include <stdint.h>
void
itimerspecFromStr(char *str, struct itimerspec *tsp)
{
char *dupstr ,*cptr, *sptr;
dupstr = strdup(str);
cptr = strchr(dupstr, ':');
if (cptr != NULL)
*cptr = '\0';
sptr = strchr(dupstr, '/');
if (sptr != NULL)
*sptr = '\0';
tsp->it_value.tv_sec = atoi(dupstr);
tsp->it_value.tv_nsec = (sptr != NULL) ? atoi(sptr + 1) : 0;
if (cptr == NULL) {
tsp->it_interval.tv_sec = 0;
tsp->it_interval.tv_nsec = 0;
} else {
sptr = strchr(cptr + 1, '/');
if (sptr != NULL)
*sptr = '\0';
tsp->it_interval.tv_sec = atoi(cptr + 1);
tsp->it_interval.tv_nsec = (sptr != NULL) ? atoi(sptr + 1) : 0;
}
free(dupstr);
}
int main(int argc, char *argv[])
{
struct itimerspec ts;
struct timespec start, now;
int maxExp, fd, secs, nanosecs;
uint64_t numExp, totalExp;
ssize_t s;
if (argc < 2 || strcmp(argv[1], "--help") == 0){
printf("%s secs[/nsecs][:int-secs[/int-nsecs]] [max-exp]\n", argv[0]);
exit(EXIT_FAILURE);
}
itimerspecFromStr(argv[1], &ts);
maxExp = (argc > 2) ? atoi(argv[2]): 1;
fd = timerfd_create(CLOCK_REALTIME, 0);
if (fd == -1){
perror("timerfd_create");
exit(EXIT_FAILURE);
}
if (timerfd_settime(fd, 0, &ts, NULL) == -1){
perror("timerfd_settime");
exit(EXIT_FAILURE);
}
if (clock_gettime(CLOCK_MONOTONIC, &start) == -1){
perror("clock_gettime");
exit(EXIT_FAILURE);
}
for (totalExp = 0; totalExp < maxExp;) {
/* 读取计时器上的过期次数,然后显示自计时器启动以来经过的时间,
* 然后是读取的过期次数和到目前为止的总过期次数。 */
s = read(fd, &numExp, sizeof(uint64_t));
if (s != sizeof(uint64_t)){
perror("read");
exit(EXIT_FAILURE);
}
totalExp += numExp;
if (clock_gettime(CLOCK_MONOTONIC, &now) == -1){
perror("clock_gettime");
exit(EXIT_FAILURE);
}
secs = now.tv_sec - start.tv_sec;
nanosecs = now.tv_nsec - start.tv_nsec;
if (nanosecs < 0) {
secs--;
nanosecs += 1000000000;
}
printf("%d.%03d: expirations read: %llu; total=%llu\n",
secs, (nanosecs + 500000) / 1000000,
(unsigned long long) numExp, (unsigned long long) totalExp);
}
exit(EXIT_SUCCESS);
}
下面的 shell 会话日志中,通过命令行参数创建了一个初始时间为 1 秒,间隔为 1 秒,最大到期次数为 100 次的定时器。