_[linux]使用epoll+时间堆实现高性能定时器

在开发Linux网络程序时,通常需要维护多个定时器,如维护客户端心跳时间、检查多个数据包的超时重传等。如果采用Linux的SIGALARM信号实现,则会带来较大的系统开销,且不便于管理。

本文在应用层实现了一个基于时间堆的高性能定时器,同时考虑到定时的粒度问题,由于通过alarm系统调用设置的SIGALARM信号只能以秒为单位触发,因此需要采用其它手段实现更细粒度的定时操作,当然,这里不考虑使用多线程+sleep的实现方法,理由性能太低。

通常的做法还有采用基于升序的时间链表,但升序时间链表的插入操作效率较低,需要遍历链表。因此本实现方案使用最小堆来维护多个定时器,插入O(logn)、删除O(1)、查找O(1)的效率较高。

首先是每个定时器的定义:

 
  1. class heap_timer

  2. {

  3. public:

  4. heap_timer( int ms_delay )

  5. {

  6. gettimeofday( &expire, NULL );

  7. expire.tv_usec += ms_delay * 1000;

  8. if ( expire.tv_usec > 1000000 )

  9. {

  10. expire.tv_sec += expire.tv_usec / 1000000;

  11. expire.tv_usec %= 1000000;

  12. }

  13. }

  14. public:

  15. struct timeval expire;

  16. void (*cb_func)( client_data* );

  17. client_data* user_data;

  18. ~heap_timer()

  19. {

  20. delete user_data;

  21. }

  22. };

包括一个超时时间expire、超时回调函数cb_func以及一个user_data变量,user_data用于存储与定时器相关的用户数据,用户数据可以根据不同的应用场合进行修改,这里实现的是一个智能博物馆的网关,网关接收来自zigbee协调器的用户数据,并为每个用户维护一段等待时间T,在T到来之前,同一个用户的所有数据都存放到user_data的target_list中,当T到来时,根据target_list列表选择一个适当的target并发送到ip_address,同时删除定时器(有点扯远了=。=)。总之,要实现的功能就是给每个用户维护一个定时器,定时值到来时做一些操作。

 
  1. class client_data

  2. {

  3. public:

  4. client_data(char *address):target_count(0)

  5. {

  6. strcpy(ip_address,address);

  7. }

  8. private:

  9. char ip_address[32];

  10. target target_list[64];

  11. int target_count;

  12. ......

  13. };

需要C/C++ Linux服务器架构师学习资料加qun获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

2fc1e68f-f008-eb11-8da9-e4434bdf6706.png

以下是时间堆的类定义,包括了一些基本的堆操作:插入、删除、扩容,还包括了定时器溢出时的操作函数tick()

 
  1. class time_heap

  2. {

  3. public:

  4. time_heap( int cap = 1) throw ( std::exception )

  5. : capacity( cap ), cur_size( 0 )

  6. {

  7. array = new heap_timer* [capacity];

  8. if ( ! array )

  9. {

  10. throw std::exception();

  11. }

  12. for( int i = 0; i < capacity; ++i )

  13. {

  14. array[i] = NULL;

  15. }

  16. }

  17. ~time_heap()

  18. {

  19. for ( int i = 0; i < cur_size; ++i )

  20. {

  21. delete array[i];

  22. }

  23. delete [] array;

  24. }

  25. public:

  26. int get_cursize()

  27. {

  28. return cur_size;

  29. }

  30. void add_timer( heap_timer* timer ) throw ( std::exception )

  31. {

  32. if( !timer )

  33. {

  34. return;

  35. }

  36. if( cur_size >= capacity )

  37. {

  38. resize();

  39. }

  40. int hole = cur_size++;

  41. int parent = 0;

  42. for( ; hole > 0; hole=parent )

  43. {

  44. parent = (hole-1)/2;

  45. if ( timercmp( &(array[parent]->expire), &(timer->expire), <= ) )

  46. {

  47. break;

  48. }

  49. array[hole] = array[parent];

  50. }

  51. array[hole] = timer;

  52. }

  53. void del_timer( heap_timer* timer )

  54. {

  55. if( !timer )

  56. {

  57. return;

  58. }

  59. // lazy delelte

  60. timer->cb_func = NULL;

  61. }

  62. int top(struct timeval &time_top) const

  63. {

  64. if ( empty() )

  65. {

  66. return 0;

  67. }

  68. time_top = array[0]->expire;

  69. return 1;

  70. }

  71. void pop_timer()

  72. {

  73. if( empty() )

  74. {

  75. return;

  76. }

  77. if( array[0] )

  78. {

  79. delete array[0];

  80. array[0] = array[--cur_size];

  81. percolate_down( 0 );

  82. }

  83. }

  84. void tick()

  85. {

  86. heap_timer* tmp = array[0];

  87. struct timeval cur;

  88. gettimeofday( &cur, NULL );

  89. while( !empty() )

  90. {

  91. if( !tmp )

  92. {

  93. break;

  94. }

  95. if( timercmp( &cur, &(tmp->expire), < ) )

  96. {

  97. break;

  98. }

  99. if( array[0]->cb_func )

  100. {

  101. array[0]->cb_func( array[0]->user_data );

  102. }

  103. pop_timer();

  104. tmp = array[0];

  105. }

  106. }

  107. bool empty() const

  108. {

  109. return cur_size == 0;

  110. }

  111. heap_timer** get_heap_array()

  112. {

  113. return array;

  114. }

  115. private:

  116. void percolate_down( int hole )

  117. {

  118. heap_timer* temp = array[hole];

  119. int child = 0;

  120. for ( ; ((hole*2+1) <= (cur_size-1)); hole=child )

  121. {

  122. child = hole*2+1;

  123. if ( (child < (cur_size-1)) && timercmp( &(array[child+1]->expire), &(array[child]->expire), < ) )

  124. {

  125. ++child;

  126. }

  127. if ( timercmp( &(array[child]->expire), &(temp->expire), < ) )

  128. {

  129. array[hole] = array[child];

  130. }

  131. else

  132. {

  133. break;

  134. }

  135. }

  136. array[hole] = temp;

  137. }

  138. void resize() throw ( std::exception )

  139. {

  140. heap_timer** temp = new heap_timer* [2*capacity];

  141. for( int i = 0; i < 2*capacity; ++i )

  142. {

  143. temp[i] = NULL;

  144. }

  145. if ( ! temp )

  146. {

  147. throw std::exception();

  148. }

  149. capacity = 2*capacity;

  150. for ( int i = 0; i < cur_size; ++i )

  151. {

  152. temp[i] = array[i];

  153. }

  154. delete [] array;

  155. array = temp;

  156. }

  157. private:

  158. heap_timer** array;

  159. int capacity;

  160. int cur_size;

  161. };

如何用epoll实现多个定时器的操作是本设计的关键,我们知道,epoll_wait的最后一个参数是阻塞等待的时候,单位是毫秒。可以这样设计:

1、当时间堆中没有定时器时,epoll_wait的超时时间T设为-1,表示一直阻塞等待新用户的到来;

2、当时间堆中有定时器时,epoll_wait的超时时间T设为最小堆堆顶的超时值,这样可以保证让最近触发的定时器能得以执行;

3、在epoll_wait阻塞等待期间,若有其它的用户到来,则epoll_wait返回n>0,进行常规的处理,随后应重新设置epoll_wait为小顶堆堆顶的超时时间。

为此,本实现对epoll_wait进行了封装,名为tepoll_wait,调用接口与epoll_wait差不多,但返回值有所不同:tepoll_wait不返回n=0的情况(即超时),因为超时事件在tepoll_wait中进行处理,只有等到n>0(即在等待过程中有用户数据到来)或者n<0(出现错误)才进行返回。

废话不多说,看代码最清楚:

 
  1. void timer_handler()

  2. {

  3. heap.tick();

  4. //setalarm();

  5. }

  6. /* tselect - select with timers */

  7. int tepoll_wait( int epollfd, epoll_event *events, int max_event_number )

  8. {

  9. struct timeval now;

  10. struct timeval tv;

  11. struct timeval *tvp;

  12. //tevent_t *tp;

  13. int n;

  14. for ( ;; )

  15. {

  16. if ( gettimeofday( &now, NULL ) < 0 )

  17. perror("gettimeofday");

  18. struct timeval time_top;

  19. if ( heap.top(time_top) )

  20. {

  21. tv.tv_sec = time_top.tv_sec - now.tv_sec;;

  22. tv.tv_usec = time_top.tv_usec - now.tv_usec;

  23. if ( tv.tv_usec < 0 )

  24. {

  25. tv.tv_usec += 1000000;

  26. tv.tv_sec--;

  27. }

  28. tvp = &tv;

  29. }

  30. else

  31. tvp = NULL;

  32. if(tvp == NULL)

  33. n = epoll_wait( epollfd, events, max_event_number, -1 );

  34. else

  35. n = epoll_wait( epollfd, events, max_event_number, tvp->tv_sec*1000 + tvp->tv_usec/1000 );

  36. if ( n < 0 )

  37. return -1;

  38. if ( n > 0 )

  39. return n;

  40. timer_handler();

  41. }

  42. }

代码一目了然,在tepoll_wait中,是个死循环,只有等到上述两种情况发生时,才进行返回,此时在调用方进行处理,处理过程跟epoll_wait一样。

 
  1. while( !stop_server )

  2. {

  3. number = tepoll_wait( epollfd, events, MAX_EVENT_NUMBER);

  4. for ( i= 0; i < number; i++ )

  5. {

  6. int fd = events[i].data.fd;

  7. if ( (events[i].events & EPOLLIN)&& (fd == uart_fd) )

  8. {

  9. //读取用户数据

  10. if( (timer_id = find_exist_timer(ip_address)) != -1)

  11. {

  12. //add to the exist timer

  13. heap_timer ** heap_array = heap.get_heap_array();

  14. heap_array[timer_id]->user_data->add_target(RSSI,target_id);

  15. continue;

  16. }

  17. //new timer

  18. heap_timer *timer = new heap_timer(200);

  19. timer->cb_func = cb_func;

  20. timer->user_data = new client_data(ip_address);

  21. timer->user_data->add_target(RSSI,target_id);

  22. heap.add_timer(timer);

  23. }

  24. else if( ( fd == pipefd[0] ) && ( events[i].events & EPOLLIN ) )

  25. {

  26. //此处进行了统一信号源处理,通过双向管道来获取SIGTERM以及SIGINT的信号,在主循环中进行统一处理

  27. char signals[1024];

  28. ret = recv( pipefd[0], signals, sizeof( signals ), 0 );

  29. if( ret == -1 )

  30. {

  31. continue;

  32. }

  33. else if( ret == 0 )

  34. {

  35. continue;

  36. }

  37. else

  38. {

  39. for( int i = 0; i < ret; ++i )

  40. {

  41. switch( signals[i] )

  42. {

  43. case SIGTERM:

  44. case SIGINT:

  45. {

  46. stop_server = true;

  47. }

  48. }

  49. }

  50. }

  51. }

  52. }

  53. }

_[linux]使用epoll+时间堆实现高性能定时器-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值