Linux下使用epoll监听定时器

本文主要讲述如何使用epoll来监听timerfd系列函数创建的定时器,关于timerfd系列函数的使用请看这篇文章


一 epoll使用简介

epoll是event poll的缩写,用于I/O事件通知,可以监听多个文件描述符。其相关api有以下三个:

  1. epoll_create()或epoll_create1():创建一个epoll对象,并返回一个文件描述符指向这个epoll对象
  2. epoll_ctl():添加想要监听的文件描述符
  3. 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个进程间进行通信,一个进程负责读,另外的进程负责写。操作如下,

  1. 读进程把pipe对应的文件描述符rfd添加到epoll里进行监听
  2. 写进程往pipe里写了2KB的数据
  3. 读进程里的epoll_wait监听到有数据进来,把rfd作为ready的描述符返回
  4. 读进程通过rfd读取1KB数据
  5. 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(&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;
    }

}

比较关键的地方就是EPOLLIN这个参数,表示当文件描述符可读的时候触发,这里表示定时器到期时文件描述符就会变成可读,就会触发。

运行结果如下,
在这里插入图片描述
定时器第一次到期时间是1s,后面循环周期是5s,和代码里设置的一样。


三 总结

本文主要讲述如何使用epoll监测timerfd定时器,关于epoll的详细使用可以参考Linux man手册,这里只是简单的使用了一下。

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

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 撸撸猫 设计师:C马雯娟 返回首页