skynet源码分析之定时器skynet_timer.c(转)

skynet自带定时器功能skynet-src/skynet_timer.c,在skynet启动时会创建一个线程专门跑定时器。每帧(0.0025秒/帧)调用skynet_updatetime()

复制代码
 1 // skynet-src/skynet_start.c
 2 
 3 create_thread(&pid[1], thread_timer, m);
 4 
 5 static void *
 6 thread_timer(void *p) {
 7     struct monitor * m = p;
 8     skynet_initthread(THREAD_TIMER);
 9     for (;;) {
10         skynet_updatetime();
11         CHECK_ABORT
12         wakeup(m,m->count-1);
13         usleep(2500);  //2500微妙=0.0025秒
14         if (SIG) {
15             signal_hup();
16             SIG = 0;
17         }
18     }
19     ...
20 }
复制代码

1. 设计思想

skynet的设计思想参考Linux内核动态定时器的机制,参考Linux动态内核定时器介绍http://www.cnblogs.com/leaven/archive/2010/08/19/1803382.html

在skynet里,时间精度是0.01秒,这对于游戏服务器来说已经足够了,定义1滴答=0.01秒,1秒=100滴答。其核心思想是:每个定时器设置一个到期的滴答数,与当前系统的滴答数(启动时是0,然后1滴答1滴答往后跳)比较差值,如果差值interval比较小(0<=interval<=2^8-1),表示定时器即将到来,需要严格关注,把它们保存在2^8个定时器链表里;如果interval越大,表示定时器越远,可以不用太关注,划分成4个等级,2^8<=interval<=2^(8+6)-1,2^(8+6)<=interval<=2^(8+6+6),...,每个等级只需要2^6个定时器链表保存,比如对于2^8<=interval<=2^(8+6)-1的定时器,将interval>>8相同的值idx保存在第一个等级位置为idx的链表里。

这样做的优势是:不用为每一个interval创建一个链表,而只需要2^8+4*(2^6)个链表,大大节省了内存。

之后,在不同情况下,分配不同等级的定时器,等级越高,表示越遥远,需要重新分配的次数越少。

2. 源码分析

数据结构:timer->near,保存2^8个即将到来的定时器链表;timer->t,保存4个分级数组,数组的每一项是一个链表;timer->time保存从skynet启动到现在走过的滴答数

复制代码
 1 // skynet-src/skynet_timer.c
 2 struct timer_event {
 3     uint32_t handle;
 4     int session;
 5 };
 6 
 7 struct timer_node { //单个定时器节点
 8     struct timer_node *next;
 9     uint32_t expire; //到期滴答数
10 };
11 
12 struct link_list { //定时器链表
13     struct timer_node head;
14     struct timer_node *tail;
15 };
16 
17 struct timer {
18     struct link_list near[TIME_NEAR];
19     struct link_list t[4][TIME_LEVEL]; 
20     struct spinlock lock;
21     uint32_t time; //启动到现在走过的滴答数,等同于current
22     ...
23 };
复制代码

调用skynet_timeout创建一个定时器

然后调用timer_add(第8行),给timer_node指针分配空间,timer_node结构里并没有timer_event字段,除了分配node自身大小外,额外再分配timer_event大小的空间用来存放event,之后通过node+1的位置可以获取到timer_event数据

然后调用add_node(第21行),添加到定时器链表里,如果定时器的到期滴答数跟当前比较近(<2^8),表示即将触发定时器添加到T->near数组里,否则根据差值大小添加到对应的T->T[i]中

复制代码
 1 // skynet-src/skynet_timer.c
 2 int
 3 skynet_timeout(uint32_t handle, int time, int session) {
 4     ...
 5     struct timer_event event;
 6     event.handle = handle;
 7     event.session = session;
 8     timer_add(TI, &event, sizeof(event), time);
 9 
10     return session;
11 }
12 
13 static void
14 timer_add(struct timer *T,void *arg,size_t sz,int time) {
15     struct timer_node *node = (struct timer_node *)skynet_malloc(sizeof(*node)+sz);
16     memcpy(node+1,arg,sz);
17 
18     SPIN_LOCK(T);
19 
20     node->expire=time+T->time;
21     add_node(T,node);
22 
23     SPIN_UNLOCK(T);
24 }
25 
26 static void
27 add_node(struct timer *T,struct timer_node *node) {
28     uint32_t time=node->expire;
29     uint32_t current_time=T->time;
30         
31     if ((time|TIME_NEAR_MASK)==(current_time|TIME_NEAR_MASK)) {
32         link(&T->near[time&TIME_NEAR_MASK],node);
33     } else {
34         int i;
35         uint32_t mask=TIME_NEAR << TIME_LEVEL_SHIFT;
36         for (i=0;i<3;i++) {
37             if ((time|(mask-1))==(current_time|(mask-1))) {
38                  break;
39             }
40             mask <<= TIME_LEVEL_SHIFT;
41         }
42 
43         link(&T->t[i][((time>>(TIME_NEAR_SHIFT + i*TIME_LEVEL_SHIFT)) & TIME_LEVEL_MASK)],node);        
44     }
45 }
复制代码

每帧从T->near中触发到期的定时器链表,near数组里每一项的链表中的所有节点的到期滴答数是相同的。

调用dispatch_list进行分发,通过current+1获取timer_event数据(第18行),然后给event->handle push一条消息表示触发定时器(第25行)

复制代码
 1 // skynet-src/skynet_timer.c
 2 static inline void
 3 timer_execute(struct timer *T) {
 4     int idx = T->time & TIME_NEAR_MASK;
 5         
 6     while (T->near[idx].head.next) {
 7         struct timer_node *current = link_clear(&T->near[idx]);
 8         SPIN_UNLOCK(T);
 9         // dispatch_list don't need lock T
10         dispatch_list(current);
11         SPIN_LOCK(T);
12     }
13 }
14 
15 static inline void
16 dispatch_list(struct timer_node *current) {
17     do {
18         struct timer_event * event = (struct timer_event *)(current+1);
19         struct skynet_message message;
20         message.source = 0;
21         message.session = event->session;
22         message.data = NULL;
23         message.sz = (size_t)PTYPE_RESPONSE << MESSAGE_TYPE_SHIFT;
24 
25        skynet_context_push(event->handle, &message);
26                
27        struct timer_node * temp = current;
28        current=current->next;
29        skynet_free(temp);      
30     } while (current);
31 }
复制代码

每帧除了触发定时器外,还需重新分配定时器所在区间(timer_shift),因为T->near里保存即将触发的定时器,所以每TIME_NEAR-1(2^8-1)个滴答数才有可能需要分配(第22行)。否则,分配T->t中某个等级即可。

当T->time的低8位不全为0时,不需要分配,所以每2^8个滴答数才有需要分配一次;

当T->time的第9-14位不全为0时,重新分配T[0]等级,每2^8个滴答数分配一次,idx从1开始,每次分配+1;

当T->time的第15-20位不全为0时,重新分配T[1]等级,每2^(8+6)个滴答数分配一次,idx从1开始,每次分配+1;

当T->time的第21-26位不全为0时,重新分配T[2]等级,每2^(8+6+6)个滴答数分配一次,idx从1开始,每次分配+1;

当T->time的第27-32位不全为0时,重新分配T[3]等级,每2^(8+6+6+6)个滴答数分配一次,idx从1开始,每次分配+1;

即等级越大的定时器越遥远,越不关注,需要重新分配的次数也就越少。

复制代码
 1 // skynet-src/skynet_timer.c
 2 static void
 3 move_list(struct timer *T, int level, int idx) {
 4     struct timer_node *current = link_clear(&T->t[level][idx]);
 5     while (current) {
 6         struct timer_node *temp=current->next;
 7         add_node(T,current);
 8         current=temp;
 9     }
10 }
11 
12 static void
13 timer_shift(struct timer *T) {
14     int mask = TIME_NEAR;
15     uint32_t ct = ++T->time;
16     if (ct == 0) {
17         move_list(T, 3, 0);
18     } else {
19         uint32_t time = ct >> TIME_NEAR_SHIFT;
20         int i=0;
21 
22         while ((ct & (mask-1))==0) {
23             int idx=time & TIME_LEVEL_MASK;
24             if (idx!=0) {
25                 move_list(T, i, idx);
26                 break;                          
27             }
28             mask <<= TIME_LEVEL_SHIFT;
29             time >>= TIME_LEVEL_SHIFT;
30             ++i;
31         }
32     }
33 }
复制代码

3. 如何使用

在C层通过skynet_timeout创建一个定时器

在Lua层通过skynet.timeout创建一个定时器,比如,skynet.timeout(200, f),即经过200个滴答数(2秒钟)后触发回调函数f。

posted on 2022-02-28 13:56  &大飞  阅读(283)  评论(0编辑  收藏  举报

导航