简谈一下时间轮(Time Wheel)

来源:互联网 发布:淘宝装修全套教程 编辑:程序博客网 时间:2024/05/17 02:50
转自: http://bookjovi.iteye.com/blog/1329614

 

如果一个程序员不知道 Time Wheel,那么那个程序员一定不是个合格的程序员。

 

timer对于操作系统还是一个虚拟机语言或大型中间件都起着重要的作用,同时timer算法的选择也直接影响着性能。

 

Time Wheel翻译为时间轮,是用于实现定时器timer的经典算法,算法细节就不多说了,这里主要是看看Erlang中和Linux kernel的time wheel实现有哪些不同。

 

Erlang中的Time Wheel实现文件是time.c,kernel中的实现文件是timer.c,好了,先看看kernel中的实现吧!

 

Linux kernel中的time wheel这么多年一直没怎么改变,主要特点是以下几点:

1)kernel中的timer是在softirq中执行

2)多CPU同时执行,和process差不多,timer也可在cpu中migrate

3)使用percpu

4) 核心数据结构:

view plain
  1. struct tvec_base {  
  2.         spinlock_t lock;  
  3.         struct timer_list *running_timer;  
  4.         unsigned long timer_jiffies;  
  5.         unsigned long next_timer;  
  6.         struct tvec_root tv1;  
  7.         struct tvec tv2;  
  8.         struct tvec tv3;  
  9.         struct tvec tv4;  
  10.         struct tvec tv5;  
  11. } ____cacheline_aligned;  
 

这里的base属于percpu数据,即每个cpu拥有一个base,这样每个cpu执行自己base里面的timer。这里有tv1/tv2/tv3/tv4/tv5,这几个vector维护着所有timer,每次加timer时根据timeout的时间分别加入到不同的vector中,tv1是最近的,tv5是最远的,kernel首先会在tv1中遍历timeout的timer,如果遍历完tv1,则从tv2中的timer list加到tv1中,如果tv2中的timer list用完后,再从tv3中取,注意tv3中的timer可以分布到tv1和tv2中,以此类推,实现代码如下:

view plain
  1. #define INDEX(N) ((base->timer_jiffies >> (TVR_BITS + (N) * TVN_BITS)) & TVN_MASK)  
  2.   
  3. /** 
  4.  * __run_timers - run all expired timers (if any) on this CPU. 
  5.  * @base: the timer vector to be processed. 
  6.  * 
  7.  * This function cascades all vectors and executes all expired timer 
  8.  * vectors. 
  9.  */  
  10. static inline void __run_timers(struct tvec_base *base)  
  11. {  
  12.         struct timer_list *timer;  
  13.   
  14.         spin_lock_irq(&base->lock);  
  15.         while (time_after_eq(jiffies, base->timer_jiffies)) {  
  16.                 struct list_head work_list;  
  17.                 struct list_head *head = &work_list;  
  18.                 int index = base->timer_jiffies & TVR_MASK;  
  19.   
  20.                 /* 
  21.                  * Cascade timers: 
  22.                  */  
  23.                 if (!index &&  
  24.                         (!cascade(base, &base->tv2, INDEX(0))) &&  
  25.                                 (!cascade(base, &base->tv3, INDEX(1))) &&  
  26.                                         !cascade(base, &base->tv4, INDEX(2)))  
  27.                         cascade(base, &base->tv5, INDEX(3));  
  28.                 ++base->timer_jiffies;  
  29.                 list_replace_init(base->tv1.vec + index, &work_list);  
  30.                 while (!list_empty(head)) {  
  31.                         void (*fn)(unsigned long);  
  32.                         unsigned long data;  
  33.   
  34.                         timer = list_first_entry(head, struct timer_list,entry);  
  35.                         fn = timer->function;  
  36.                         data = timer->data;  
  37.   
  38.                         timer_stats_account_timer(timer);  
  39.   
  40.                         base->running_timer = timer;  
  41.                         detach_timer(timer, 1);  
  42.   
  43.                         spin_unlock_irq(&base->lock);  
  44.                         call_timer_fn(timer, fn, data);  
  45.                         spin_lock_irq(&base->lock);  
  46.                 }  
  47.         }  
  48.         base->running_timer = NULL;  
  49.         spin_unlock_irq(&base->lock);  
  50. }  
  51.   
  52. static int cascade(struct tvec_base *base, struct tvec *tv, int index)  
  53. {  
  54.         /* cascade all the timers from tv up one level */  
  55.         struct timer_list *timer, *tmp;  
  56.         struct list_head tv_list;  
  57.   
  58.         list_replace_init(tv->vec + index, &tv_list);  
  59.   
  60.         /* 
  61.          * We are removing _all_ timers from the list, so we 
  62.          * don't have to detach them individually. 
  63.          */  
  64.         list_for_each_entry_safe(timer, tmp, &tv_list, entry) {  
  65.                 BUG_ON(tbase_get_base(timer->base) != base);  
  66.                 internal_add_timer(base, timer);  
  67.         }  
  68.   
  69.         return index;  
  70. }  
 

可以看出,即使你加了很多长时间的timer,kernel的timer性能并没有减少,因为长时间time被分布到不同的vector中,因此Linux kernel中的time wheel算法适合大容量的timer应用场景。

(注意kernel中每个base的lock用的是spin lock,而不是mutex,下面会讲到)

 

 

下面再来看看Erlang中的timer实现,erlang普遍应用于并发量比较高的场景,erlang的process通信是通过message,message的发送接收显然离不开timer,erlang甚至把timer提升到语言语法的层次,从此可看出timer在

erlang中使用是多么的广泛。

和Linux kernel的time wheel比较,erlang有以下几点不同:

1)erlang的timer执行过程是在erlang process schedule时发生,而不是像很多中间件timer实现那样用单独的线程,这是有历史原因的(Erlang应兼顾到plain cpu的情形)。

2)erlang的scheduler线程可以有多个,所以timer wheel需要lock的支持

3)没有percpu,由于erlang在user space,所以percpu是个很难的问题,原因是抢占的问题,kernel实现的percpu可以显著提高性能,但也是有代价的,代价就是在很多percpu的处理过程中要关闭抢占,这也就是为什么RT kernel的人比较头疼percpu的原因。而在用户空间,抢占被操作系统强制执行,导致用户空间程序无法使用percpu。

4)Erlang中time wheel没有像Linux kernel那样把timeout根据相对时间挂载到tv1/tv2/tv3/tv4/tv5中,但是erlang中的wheel slot却比较大(kernel中的slot是16或64),可以是8192或65536,这在一定程度上缓解了大量长时间timer对性能带来的影响,如果把 每个wheel的slot的间隔时间算作是1ms,wheel算作8192,那么几乎是8s一个wheel就遍历完,如果程序中有大量的timer超时时间大于8s,那么那些timer就会对8192取模挂载在相应的slot下,这就意味着每次遍历是会有很多并未超时的timer被访问到,而这在Linux kernel中则不存在。核心代码如下:

view plain
  1. static ERTS_INLINE void bump_timer_internal(erts_short_time_t dt) /* PRE: tiw_lock is write-locked */  
  2. {  
  3.     Uint keep_pos;  
  4.     Uint count;  
  5.     ErlTimer *p, **prev, *timeout_head, **timeout_tail;  
  6.     Uint dtime = (Uint) dt;  
  7.   
  8.     /* no need to bump the position if there aren't any timeouts */  
  9.     if (tiw_nto == 0) {  
  10.         erts_smp_mtx_unlock(&tiw_lock);  
  11.         return;  
  12.     }  
  13.   
  14.     /* if do_time > TIW_SIZE we want to go around just once */  
  15.     count = (Uint)(dtime / TIW_SIZE) + 1;  
  16.     keep_pos = (tiw_pos + dtime) % TIW_SIZE;  
  17.     if (dtime > TIW_SIZE) dtime = TIW_SIZE;  
  18.   
  19.     timeout_head = NULL;  
  20.     timeout_tail = &timeout_head;  
  21.     while (dtime > 0) {  
  22.         /* this is to decrease the counters with the right amount */  
  23.         /* when dtime >= TIW_SIZE */  
  24.         if (tiw_pos == keep_pos) count--;  
  25.         prev = &tiw[tiw_pos];  
  26.         while ((p = *prev) != NULL) {  
  27.             ASSERT( p != p->next);  
  28.             if (p->count < count) {     /* we have a timeout */  
  29.                 /* remove min time */  
  30.                 if (tiw_min_ptr == p) {  
  31.                     tiw_min_ptr = NULL;  
  32.                     tiw_min = 0;  
  33.                 }  
  34.   
  35.                 /* Remove from list */  
  36.                 remove_timer(p);  
  37.                 *timeout_tail = p;      /* Insert in timeout queue */  
  38.                 timeout_tail = &p->next;  
  39.             }  
  40.             else {  
  41.                 /* no timeout, just decrease counter */  
  42.                 p->count -= count;  
  43.                 prev = &p->next;  
  44.             }  
  45.         }  
  46.         tiw_pos = (tiw_pos + 1) % TIW_SIZE;  
  47.         dtime--;  
  48.     }  
  49.     tiw_pos = keep_pos;  
  50.     if (tiw_min_ptr)  
  51.         tiw_min -= dt;  
  52.   
  53.     erts_smp_mtx_unlock(&tiw_lock);  
 

综上比较,在面对大容量timer的情况下Linux kernel的time wheel算法会比Erlang更有效率一些。最后还有一点要注意,Erlang的time wheel使用的lock是mutex(上面说过Linux kernel使用spin lock),在这里那种lock会更适合time wheel呢?个人觉得spin lock会好些,毕竟临界区代码处理应该会很快。当然如果erlang中ethread mutex使用的是mutex spin机制(mutex使用的是futex,在进入kernel futex前,进行spin lock很短一段时间),那就无所谓了。

0 0
原创粉丝点击