本文主要讲述如何使用epoll来监听timerfd系列函数创建的定时器,关于timerfd系列函数的使用请看这篇文章。
一 epoll使用简介
epoll是event poll的缩写,用于I/O事件通知,可以监听多个文件描述符。其相关api有以下三个:
- epoll_create()或epoll_create1():创建一个epoll对象,并返回一个文件描述符指向这个epoll对象
- epoll_ctl():添加想要监听的文件描述符
- epoll_wait():等待I/O事件的发生,如果没有事件发生就会阻塞调用线程
这3个api的原型如下,
#include <sys/epoll.h>
int epoll_create(int size); // 旧版,size取值必须大于0
int epoll_create1(int flags); // 新版,flags可以是0,或者EPOLL_CLOEXEC
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
关键结构体是struct epoll_event,其定义如下,
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
edge-triggered和level-triggered
epoll api可以使用edge-triggered或level-triggered的方式来监听文件描述符,可以翻译为边沿触发和电平触发,学过数字电路的比较好理解,如下图
绿色的部分属于电平触发,当处于高电平或低电平时就会触发;橘黄色属于边沿触发,当电平发生由高变到低或者由低变成高就会触发。
这里再使用Linux man手册里举的例子来解释一下,现在有一个pipe,用于2个进程间进行通信,一个进程负责读,另外的进程负责写。操作如下,
- 读进程把pipe对应的文件描述符rfd添加到epoll里进行监听
- 写进程往pipe里写了2KB的数据
- 读进程里的epoll_wait监听到有数据进来,把rfd作为ready的描述符返回
- 读进程通过rfd读取1KB数据
- epoll_wait继续监听rfd
如果在第1步里使用的是边沿触发的方式监听rfd,那么第5步里的epoll_wait就不会再把rfd作为ready的描述符返回,这样读进程就无法读取剩余的1KB数据了。如果使用的是电平触发,就可以继续读取剩余1KB数据,这是为什么?
对于读进程来说,初始时,buffer为空,可以看做电平为0状态,当写进程写入数据后,buffer里就会有数据,这样buffer状态发生变化,此时可以看做是电平1的状态,这样就发生了电平由0到1的变化,如果采用的是边沿触发,就会触发,rfd就会作为ready的描述符返回。但是一次并没有读取完,buffer里还有剩余数据,那么此时还是处于电平1状态,这样的话再次epoll_wait就不会触发,因为电平状态没有发生变化。
但是如果使用电平触发,buffer里有数据就触发,即电平为1就触发,那么当buffer由空变成有数据状态,就会触发,而且一次没读完,再次进入epoll_wait时还会触发,直到把数据读完,buffer变成空,就不再触发了。
epoll默认使用电平触发。
二 监测定时器
源码如下,
#include <sys/timerfd.h>
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/epoll.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 = 1; // 第一次1s到期
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");
int epollfd = epoll_create1(EPOLL_CLOEXEC); // or epoll_create(1)
if (epollfd == -1)
{
handle_error("epoll_create1");
}
struct epoll_event ev;
ev.events = EPOLLIN; // 表示该文件描述符可以读的时候就触发
ev.data.fd = timerfd;
epoll_ctl(epollfd, EPOLL_CTL_ADD, timerfd, &ev);
const int maxEvents = 5; // 也可以设置为1
struct epoll_event events[maxEvents];
while (1)
{
int nfd = epoll_wait(epollfd, events, maxEvents, -1);
if (nfd > 0)
{
for (int i = 0; i < nfd; ++i)
{
if (events[i].data.fd == timerfd)
{
uint64_t exp = 0;
int ret = read(timerfd, &exp, sizeof(uint64_t));
if (ret != sizeof(uint64_t))
{
handle_error("read timerfd");
}
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;
}
}
比较关键的地方就是EPOLLIN这个参数,表示当文件描述符可读的时候触发,这里表示定时器到期时文件描述符就会变成可读,就会触发。
运行结果如下,
定时器第一次到期时间是1s,后面循环周期是5s,和代码里设置的一样。
三 总结
本文主要讲述如何使用epoll监测timerfd定时器,关于epoll的详细使用可以参考Linux man手册,这里只是简单的使用了一下。
如果有写的不对的地方,希望能留言指正,谢谢阅读。