Title:Implementing Software Timers
By:Don Libes
翻译:CoreyGao
这篇文章提供了实现软件计时器所需的一系列函数。软件计时器是什么?你为什么需要实现软件计时器?软件计时器弥补了硬件计时器的先天不足。比如,对于大部分电脑的硬件计时器,你只能让时钟在未来某个指定时间触发一次中断(译者注:例如UNIX下的alarm函数)。
当运行多任务时,你就会想要让时钟能够同步地追踪多个计时器。即使多个计时器的时间相互重叠,时钟也必须能够正确的产生中断。操作系统始终都在重复上述过程。
Robet Ward在1990年四月的《C User's Journal》上发表了“Practical Schedulers for Real-Time Application”一文。文章研究了一个与本文主题相关的问题--建立一个通用调度器。在那篇文章的"Addition Ideas"一节中,Robert描述了计时器调度队列的作用。“通过在一个指定的队列里插入计时器请求,一个事件就可以指定其他事件的运行时间”。(Events can specify the timing of other events by putting a timer programming request in a special queue).这正是本篇文章内的程序要做的事情。
这些代码也可以做其他事情,比如,在UNIX环境下,每个进程只能有一个计时器,但是用这个程序,你可以设置任意数量的计时器。即使你对计时器不敢兴趣,你也会这篇文章很有趣。通过一些简单的技巧和数据结构,这些C代码可以产生巨大的效果。这些代码很有技巧性。而且如果你阅读并且写过足够多的C代码的话,你会觉得我的注释也很有趣。
计时器
通过组合多个模块来实现计时器,我们可以降低复杂性。有些人喜欢模块化,有些人讨厌。相似的是,有些操作系统是模块化的,而有些操作系统却不是。我喜欢模块化,模块化可以让代码变得容易编写,容易阅读,而且容易调试。
计时器的基本理念就是允许任务在将来的某个时间点运行。当时间点到来时,他们被调度器调度运行。实际运行这些任务的是调度器。为了能够与调度器通信,我们定义了一个叫做timer的数据结构(list 1)。我还在list 1中包括了一些其他必须的定义。例如,TIME是用来代表时间变量的。你可以根据自己的需求实现这些定义。
1 #include <stdio.h> 2 3 #define TRUE 1 4 #define FALSE 0 5 6 #define MAX_TIMERS ... /* number of timers */ 7 typedef ... TIME; /* how time is actually stored */ 8 #define VERY_LONG_TIME ... /* longest time possible */ 9 10 struct timer { 11 int inuse; /* TRUE if in use */ 12 TIME time; /* relative time to wait */ 13 char *event; /* set to TRUE at timeout */ 14 } timers[MAX_TIMERS]; /* set of timers */
每一个timer结构代表一个计时器。计时器集合通过一个timer的数组实现。timer结构的第一个元素代表当前计时器是否被使用。第二个元素代表当前计时器需要等待的时间。*event的初始值是0。当到达指定时间时,*event的值被设为1。调度器同时也关注event指针。当*event == 1时,调度器知道与此计时器相关的任务必须启动。
list 2中的代码初始化timers数组。
1 void timers_init() { 2 struct timer *t; 3 4 for (t=timers;t<&timers[MAX_TIMERS];t++) 5 t->inuse = FALSE; 6 }
现在我们可以开始写调度timers的程序了。首先是timer_undeclare。与相对应的timer_declare相比,timer_undeclare简单一点。
有相当多的方式可以跟踪计时器。没有硬件时钟的电脑通过CPU的时钟周期产生中断来计时。软件通过一个寄存器来维护系统时间和检查计时器是否超时。
现代的电脑都有硬件时钟。只有在给定时间到来时才会中断CPU。通过在给定时间产生中断,系统速度大幅度提升。在“虚拟软件”和“线程的实现”中 都大量有运用了这种技术。
读取时间需要使用“系统调用”。但是受限于本文的主题,我们假设time_now变量自动读取当前时间。volatile关键字说明 这个变量不能在寄存器中缓存,只能从内存中读取.
volatile TIME time_now;
为了方便我们定义了如下变量:timer_next指向下一个将要触发的定时器。time_timer_set保存上一次读取的时间。
1 struct timer *timer_next = NULL;/* timer we expect to run down next */ 2 TIME time_timer_set; /* time when physical timer was set */ 3 4 void timers_update(); /* see discussion below */ 5 6 void timer_undeclare(struct timer *t ) 7 { 8 disable_interrupts(); 9 if (!t->inuse) { 10 enable_interrupts(); 11 return; 12 } 13 14 t->inuse = FALSE; 15 16 /* check if we were waiting on this one */ 17 if (t == timer_next) { 18 timers_update(time_now - time_timer_set); 19 if (timer_next) { 20 start_physical_timer(timer_next->time); 21 time_timer_set = time_now; 22 } 23 } 24 enable_interrupts(); 25 }
取消计时器 - Why and How?
timer_undeclare的名字表明了它的作用-取消计时器。在某些应用中,取消计时器是非常重要的操作。比如,网络通信的代码疯狂的使用计时器。在某些协议中,每发送一个packet就会产生一个计时器。如果发送方在给定的间隔内没收到应答,计时器就会触发相同packet的再次发送。如果发送方收到了应答,发送方就会取消与此packet对应的计时器。如果一切顺利的话,每个计时器都会在一段时间后被取消。
timer_undeclare(list 3)在执行之前首先禁用中断。因为中断处理程序与本程序使用相同的数据。因为数据是共享的,所以必须严格控制权限。disable_interrupts()函数内容是依赖系统实现的。各位读者自行实现。timer_undeclare函数首先检查计时器是否处于使用状态。在稍后的内容中我们可以看到,操作系统可以暗中改变计时器状态。因此我们必须首先检查计时器状态。如果计时器是使用中的,timer_undeclare函数使计时器不可用。但是如果参数所指定的计时器恰巧是下一个将要触发的计时器,那么physical timer必须重新设置为剩余的所有计时器中最短的一个的时间。在这之前,所有的计时器必须更新为当前时间减去上一次设置的时间。timers_update用于完成这个任务,同时也计算了剩余所有时间中最短的那个。
timers_update (listing 4)遍历了timers数组,将每个计时器的剩余时间减去了指定的时间。如果当前计时器的时间小于给定时间,那么这个计时器就会触发。在创建计时器和调用timers_update之间的延迟 与 通过系统调用获得当前时间的延迟 相互抵消。最后,我们在timer_next中记录下一个最短的计时器。
timer_last是临时变量。当所有计时器都被调度完以后,timer_last才会出现,而且永远不会被调度。
1 /* subtract time from all timers, enabling any that run out along the way */ 2 void 3 timers_update(time) 4 TIME time; 5 { 6 static struct timer timer_last = { 7 FALSE /* in use */, 8 VERY_LONG_TIME /* time */, 9 NULL /* event pointer */ 10 }; 11 12 struct timer *t; 13 14 timer_next = &timer_last; 15 16 for (t=timers;t<&timers[MAX_TIMERS];t++) { 17 if (t->inuse) { 18 if (time < t->time) { /* unexpired */ 19 t->time -= time; 20 if (t->time < timer_next->time) 21 timer_next = t; 22 } else { /* expired */ 23 /* tell scheduler */ 24 *t->event = TRUE; 25 t->inuse = 0; /* remove timer */ 26 } 27 } 28 } 29 30 /* reset timer_next if no timers found */ 31 if (!timer_next->inuse) timer_next = 0; 32 }
声明计时器
timer_declare(list 5)函数接受时间和一个event的地址作为参数。当指定时间到达时,*event被设置为1(在timers_update函数中/* tell scheduler*/下面)。timer_declare函数返回一个指向struct timer的指针。这个指针与timer_undeclare函数的参数是同一个。
1 struct timer * 2 timer_declare(time,event) 3 unsigned int time; /* time to wait in 10msec ticks */ 4 char *event; 5 { 6 struct timer *t; 7 8 disable_interrupts(); 9 10 for (t=timers;t<&timers[MAX_TIMERS];t++) { 11 if (!t->inuse) break; 12 } 13 14 /* out of timers? */ 15 if (t == &timers[MAX_TIMERS]) { 16 enable_interrupts(); 17 return(0); 18 } 19 20 /* install new timer */ 21 t->event = event; 22 t->time = time; 23 if (!timer_next) { 24 /* no timers set at all, so this is shortest */ 25 time_timer_set = time_now; 26 start_physical_timer((timer_next = t)->time); 27 } else if ((time + time_now) < (timer_next->time + time_timer_set)) { 28 /* new timer is shorter than current one, so */ 29 timers_update(time_now - time_timer_set); 30 time_timer_set = time_now; 31 start_physical_timer((timer_next = t)->time); 32 } else { 33 /* new timer is longer, than current one */ 34 } 35 t->inuse = TRUE; 36 enable_interrupts(); 37 return(t); 38 }
与它相对应的函数一样,timer_declare中断被禁用,以防产生竞争。
timer_declare首先为定时器分配空间。如果所有定时器被用完,返回NULL。
一旦定时器被初始化,我们必须检查physical timer有没有被改变。有三种情况:
1)没有其他定时器:
在这种情况下,我们将physical timer设置为当前定时器的时间。
2)有其他定时器,但是新声明的定时器是最短的:
在这种情况下,我们必须取消以前的physical timer,并将physical timer的值设置为新声明的定时器的值。但是在此之前,我们必须更新其他所有定时器的时间。
3)有其他定时器,而且新声明的定时器不是最短的:
在这种情况下我们什么也不用做。为了容易理解,在else{}中添加了注释。
在启用中断并返回之前,当前计时器的inuse变量设置为TRUE。在最后设置inuse,是为了防止timers_update错误地将当前定时器更新。
处理计时器中断
剩下的最后一个任务。计时器超时以后调用的interrupt handler。当interrrupt handler被调用以后,我们认为timer_next已经超时。
1 void 2 timer_interrupt_handler() { 3 timers_update(time_now - time_timer_set); 4 5 /* start physical timer for next shortest time if one exists */ 6 if (timer_next) { 7 time_timer_set = time_now; 8 start_physical_timer(timer_next->time); 9 } 10 }
每当interrupt handler被调用时,就有一个计时器超时。
每当timers_update被调用时,所有的inuse状态的计时器都被更新,所有的超时计时器的*event都被设置为1.如果条件满足,timer_next也会被更新为新值。physical timer被重新设置.
让我们通过一个特殊情况来检查一下这个程序。假设我们只设置了一个计时器。现在我们调用timer_undelcare。中断被禁用,physical timer照常运行。当中断被启用时,中断会被立刻传送给进程,但是此时计时器已被取消。所以现在的情况是,计时器被取消,但是中断被传送给了进程。在interrupt handler中会发生什么呢?timers_update被调用。timers_update没有发现任何可供更新的计时器。然后timer_next被设置为0.interrupt handler函数的剩余部分很好的处理了这种情况( if(timer_next) )。
这是上述程序必须处理的一种特殊情况(事实上,在我写的第一个版本中并没有处理这种情况。随后的debug让我十分蛋疼。涉及到中断时,debugger并不好用)。
结论
通过这篇文章我实现了一个软件计时器。通过仔细的设计,这个计时器实现了与调度器的分离。例如,当你同时使用另一种计时器时,这篇文章的计时器不需要关闭调度器。
当你阅读这篇文章时你可能想,为什么不用链表而是用数组来维护计时器。使用链表可以避免溢出。使链表保持排序状态 可以让timers_update的实现更加简单。
但是从另一方面看这个问题,使用链表会让其余的函数变得更复杂。比如,要实现timer_undeclare,就会要求你要么实现一个双向链表,要么从头到尾的遍历整个链表。另外,real-time system通常避免使用动态数据结构。例如,使用进程空间的堆进程malloc/free会产生很难估计的时间消耗。如果我用链表重新实现这个程序,我会自己实现一个malloc函数。该malloc从我自己设定的内存池中分配空间。这种方式 事实上与使用和肃卒没有区别。在time和space上,我们必须进行取舍。
如果你决定重新编写或者修改一下这些代码,一定要自信。要牢记两个进程同时操作相同数据结构的后果。
Happy interruptions!
Thanks