基于timerfd epoll开发的io定时器 [上]

造一个轮子

对的,我又重新造了一个轮子! 关于周期定时器的轮子。 以前在python环境下,用二叉堆和gevent later写过单机的定时器,后来借用redis的sorted set 加lua实现了分布式的定时器任务管理。 但我坚决不满足, 我一直想实现一个类似libevent,libev那种包含各种功能的事件库,但个人能力及其有限…    


该文章写的有些乱,欢迎来喷 ! 另外文章后续不断更新中,请到原文地址查看更新.

http://xiaorui.cc/?p=3681


那么这根我们定时器的实现有啥关系? 因为libevent有定时器的功…    我去年的时候实现过一个类似gevent的io调度器,但一直没想好定时器该如何的实现,一开始是使用epoll wait跟二叉堆结构交叉来实现的,  每次做调度的时候都要看看是否有定时任务触发,  后来是独立一个线程扫描二叉堆,通过fd来让调度器唤醒.    总的来说很恶…. 


Timerfd 的优缺点

首先说说使用epoll管理timerfd的优缺点

优点是,你可以随意的使用timerfd创建定时任务和周期性任务,然后扔到epoll里,让内核去帮你监听事件,非常省事…

缺点是麻烦, 性能没有想象中的高.

如果你本身在使用围绕epoll为核心的调度器,那么很容易把socket事件,信号事件,定时事件都抽象合体在epoll来监听。

二叉堆的实现相对更简单,但是他的问题也有不少, 首先是周期性任务,你开一个独立线程去创建任务,但如果每个任务的周期不同,该怎么适配?
当然,这不是问题.

我很久以前就知道timerfd的特性,但一直没机会去尝试下。  我这次用PyObject封装了timerfd的c api, 因为python标准库里不存在timerfd定时接口。


我猜有人会问我,timerfd是什么?

timerfd是Linux为用户程序提供的一个定时器接口。这个接口基于文件描述符,通过文件描述符的可读事件进行超时通知,一般是被用于select/epoll的应用场景。


timerfd vs select timeout 区别 ?

– timerfd_create 把时间变成了一个文件描述符,该“文件”在定时器超时的那一刻变得可读,这样就能很方便地融入到 select/poll 框架中,用统一的方式来处理 IO 事件和超时事件。
– 利用select, poll的timeout实现定时功能,它们的缺点是定时精度只有毫秒,远低于 timerfd_settime 的定时精度。

timerfd C API 使用方法

讲讲C 的 Timerfd相关函数介绍.   http://man7.org/linux/man-pages/man2/timerfd_create.2.html

#xiaorui.cc

#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函数方法:

int timerfd_create(int clockid, int flags);

它是用来创建一个定时器描述符timerfd

第一个参数:clockid指定时间类型,有两个值:

CLOCK_REALTIME :Systemwide realtime clock. 系统范围内的实时时钟

CLOCK_MONOTONIC:以固定的速率运行,从不进行调整和复位 ,它不受任何系统time-of-day时钟修改的影响

第二个参数:flags可以是0或者O_CLOEXEC/O_NONBLOCK。

返回值:timerfd(文件描述符)

timerfd_settime函数:

用于启动和停止定时器,fd为timerfd_create获得的定时器文件描述符,flags为0表示是相对定时器,为TFD_TIMER_ABSTIME表示是绝对定时器。const struct itimerspec *new_value表示设置超时的时间。

int timerfd_settime(int ufd, int flags, const struct itimerspec * utmr, struct itimerspec * otmr);

此函数用于设置新的超时时间,并开始计时。
ufd,timerfd_create返回的文件句柄。
flags,为1代表设置的是绝对时间;为0代表相对时间。
utmr为需要设置的时间。
otmr为定时器这次设置之前的超时时间。
一般来说函数返回0代表设置成功。

在timerfd里有一个及其重要的数据结构:

  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 */
  };

需要注意的是itimerspec 结构成员表示的意义:

it_value是首次超时时间,需要填写从clock_gettime获取的时间,并加上要超时的时间。 it_interval是后续周期性超时时间,是多少时间就填写多少。

it_interval不为0则表示是周期性定时器,大于0,是周期性的时间.。

it_value和it_interval都为0表示停止定时器。

timerfd_gettime此函数用于获得定时器距离下次超时还剩下的时间。如果调用时定时器已经到期,并且该定时器处于循环模式(设置超时时间时struct itimerspec::it_interval不为0),那么调用此函数之后定时器重新开始计时。 


timerfd的简单使用例子,大家对照上面讲述的timerfd接口学习下。  

#include <sys/timerfd.h>
#include <time.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h> 

#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: %llu; total=%llu\n",
                 (unsigned long long) exp,
                 (unsigned long long) tot_exp);
     }

     exit(EXIT_SUCCESS);
 }

上面的代码是timerfd的基本用法,我们在看看timerfd epoll的C API使用方法.  

#include <sys/epoll.h>
#include <sys/timerfd.h>
#include <time.h>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

#define MX_EVNTS 10
#define EPL_TOUT 3000
#define MX_CNT 5

struct param{
	struct itimerspec its;
	int tfd;
};

void *strt_eplth(void *arg)
{
	struct epoll_event evnts[MX_EVNTS];
	int *eplfd = (int *)arg;
	int n = -1;
	size_t i,cnt = 0;
	while(1){
		n = epoll_wait(*eplfd,evnts,MX_EVNTS,EPL_TOUT);
		if(n == -1){
			perror("epoll_wait() error");
			break;
		}else if(n == 0){
			printf("time out %d sec expired\n",EPL_TOUT / 1000);
			break;
		}
		for(i = 0; i < n;i++){
			struct param *pm = (struct param *)(evnts[i].data.ptr);
			printf("tfd: %d\ninitial expiration: %ld\ninterval: %ld\n\n",
				pm->tfd,
				(long)(pm->its.it_value.tv_sec),
				(long)(pm->its.it_interval.tv_sec));
			if(epoll_ctl(*eplfd,EPOLL_CTL_DEL,pm->tfd,NULL) != 0){
				perror("epoll_ctl(DEL) error in thread");
				break;
			}
			struct epoll_event ev;
			ev.events = EPOLLIN | EPOLLET;
			pm->its.it_value.tv_sec =
				pm->its.it_value.tv_sec +
				pm->its.it_interval.tv_sec;
			ev.data.ptr = pm;
			if(timerfd_settime(pm->tfd,TFD_TIMER_ABSTIME,&(pm->its),NULL) != 0){
				perror("timerfd_settime() error in thread");
				break;
			}
			if(epoll_ctl(*eplfd,EPOLL_CTL_ADD,pm->tfd,&ev) != 0){
				perror("epoll_ctl(ADD) error in thread");
				break;
			}
		}
		if(++cnt == MX_CNT){
			printf("cnt reached MX_CNT, %d\n",MX_CNT);
			break;
		}
	}
	close(*eplfd);
	pthread_exit(NULL);
}

int create_timerfd(struct itimerspec *its,time_t interval)
{
	int tfd = timerfd_create(CLOCK_MONOTONIC,TFD_NONBLOCK);
	if(tfd < 0){
		perror("timerfd_create() error");
		return -2;
	}
	struct timespec nw;
	if(clock_gettime(CLOCK_MONOTONIC,&nw) != 0){
		perror("clock_gettime() error");
		return -1;
	}
	its->it_value.tv_sec = nw.tv_sec + interval;
	its->it_value.tv_nsec = 0;
	its->it_interval.tv_sec = interval;
	its->it_interval.tv_nsec = 0;
	return tfd;
}

int main()
{
	time_t INTERVAL = 2;
	struct itimerspec its;
	int tfd = create_timerfd(&its,INTERVAL);
	if(tfd < 0)
		return -1;
	int eplfd = epoll_create1(0);
	if(eplfd < 0){
		perror("epoll_create1() error");
		return -1;
	}
	struct param pm;
	pm.its = its;
	pm.tfd = tfd;
	if(timerfd_settime(pm.tfd,TFD_TIMER_ABSTIME,&(pm.its),NULL) != 0){
		perror("timerfd_settime() error");
		return -1;
	}
	struct epoll_event ev;
	ev.events = EPOLLIN | EPOLLET;
	ev.data.ptr = &pm;
	if(epoll_ctl(eplfd,EPOLL_CTL_ADD,pm.tfd,&ev) != 0){
		perror("epoll_ctl() error");
		return -1;
	}
	pthread_t pid;
	if(pthread_create(&pid,NULL,strt_eplth,(void *)&eplfd) != 0){
		perror("pthread_create() error");
		return -1;
	}

	//// add another timerfd.
	INTERVAL = 1;
	struct itimerspec its2;
	int tfd2 = create_timerfd(&its2,INTERVAL);
	if(tfd2 < 0)
		return -1;
	struct param pm2;
	pm2.its = its2;
	pm2.tfd = tfd2;
	if(timerfd_settime(pm2.tfd,TFD_TIMER_ABSTIME,&(pm2.its),NULL) != 0){
		perror("timerfd_settime() error");
		return -1;
	}
	struct epoll_event ev2;
	ev2.events = EPOLLIN | EPOLLET;
	ev2.data.ptr = &pm2;
	if(epoll_ctl(eplfd,EPOLL_CTL_ADD,pm2.tfd,&ev2) != 0){
		perror("epoll_ctl() error");
		return -1;
	}

	if(pthread_join(pid,NULL) != 0){
		perror("pthread_join() error");
		return -1;
	}
	close(tfd);
	close(tfd2);
	return 0;
}

此文为上小结,下小节会说下python timerfd epoll的使用方法。  我已经把python epoll timerfd封装一个包,大家直接调用就可以了。 


END.


大家觉得文章对你有些作用! 如果想赏钱,可以用微信扫描下面的二维码,感谢!
另外再次标注博客原地址  xiaorui.cc

5 Responses

  1. 依云 2016年9月20日 / 下午4:20

    select 的时间精度是微秒啊……

    • 峰云就她了 2016年9月21日 / 上午10:22

      timeval结构体中虽然指定了一个微妙级别的分辨率,但内核支持的分别率往往没有这么高。 此外,加上内核调度延时现象,即定时器时间到后,内核还需要花一定时间调度相应进程的运行。因此,定时器的精度,最终还是由内核支持的分别率决定。

      • 依云 2016年9月22日 / 上午10:29

        还好吧,内核支持的时间精度应该挺高的。你有具体的文档或者代码来明确地说明内核的时间精度吗?调试延迟的话,timerfd 也有啊。

        • 峰云就她了 2016年9月22日 / 下午10:53

          你可以搜搜linux hz ,jeffie节拍 ,tick, 你会发现基本是1ms一次时间中断 , 一般不会进行微妙和纳秒级别的中断,我记得微妙和纳秒级别应用是那种忙轮询的方式。 内核定时器依赖于系统时钟发出的中断,只有在系统时钟中断发生后内核才会去检查当前是否有超时的动态定时器。

        • 峰云就她了 2016年9月22日 / 下午10:56

          在usleep和nanosleep看起来是可以配置us的,但不靠谱的… 因为还要看你的配置和硬件支持 以前听一个资深系统专家说的。 你可以用while sleep counter++ 试试?

发表评论

邮箱地址不会被公开。 必填项已用*标注