8.3.2 add_timer

来源:互联网 发布:谁杀了宇智波一族 知乎 编辑:程序博客网 时间:2024/06/06 11:04

http://book.51cto.com/art/201202/319703.htm

8.3.2  add_timer

当程序定义了一个定时器对象,并且通过init_timer函数及相应代码对该定时器对象中的expires、data和function等成员初始化之后,程序需要调用add_timer将该定时器对象加入到系统中,这样定时器才会在expires表示的时间点到期后被触发。可以想见,add_timer函数的内部实现将不再独立,它必然会和内核中关于定时器的基础架构发生关联。

内核自身对于定时器的管理与操作设计有一个非常完整的框架,详细讨论这些技术细节需要相当的篇幅,其中大量的内容属于内核实现的范畴。因此我们决定将后续的讨论限定在设备驱动程序员需要关注的范围之内,也即在更广的范围内我们给出定时器内核实现原理的大体架构,在更细分的范围我们重点讨论与驱动程序中对定时器的使用等密切相关的部分。这样的安排相信对于设备驱动程序员而言是合理的:在了解了基本原理的前提下,通过对内核如何组织和调用到期的定时器函数的讨论,现实中我们将知道如何更安全更有效地使用定时器,这也是写作本书的主要目的。

接下来将首先讨论内核如何管理系统中的定时器,然后会看到定时器函数如何在指定的时间到期后被调用,最后会讨论add_timer函数是如何将一个定时器对象加入到系统中的。

内核中定义了一个数据结构struct tvec_base来管理系统中添加的所有定时器,其定义如下:

  1. <kernel/timer.c> 
  2. struct tvec_base {  
  3.     spinlock_t lock;  
  4.     struct timer_list *running_timer;  
  5.     unsigned long timer_jiffies;  
  6.     unsigned long next_timer;  
  7.     struct tvec_root tv1;  
  8.     struct tvec tv2;  
  9.     struct tvec tv3;  
  10.     struct tvec tv4;  
  11.     struct tvec tv5;  
  12. } ____cacheline_aligned;  

其中的tv1、tv2、tv3、tv4和tv5被内核用来对系统中注册的定时器进行散列式的管理,后面会看到其用法。内核为系统中的每个CPU都定义了一个struct tvec_base类型的变量tvec_bases:
  1. <kernel/timer.c> 
  2. static DEFINE_PER_CPU(struct tvec_base *, tvec_bases) = &boot_tvec_bases;  

tvec_bases用来将系统中加入的每个定时器组织管理起来。用简单的单一链表结构当然也可以实现这一目标,然而系统会在每个时钟中断中去扫描该链表并要分辨出哪些定时器已经到期或者是即将到期,所以必须使得这一任务的执行效率非常高以消耗极小的CPU资源。因此内核采用了上述struct tvec_base结构来组织链表,读者可以简单地认为它是基于哈希表的一个实现。每当设备驱动程序通过add_timer向系统添加一个定时器对象时,系统都会对该定时器对象的到期时间expires进行分类,根据到期时间的长短将当前定时器对象放到struct tvec_base对象的成员tv1、tv2、tv3、tv4和tv5领衔的定时器链表中。比如,其中的tv1中定时器的到期时间范围是0~255个时钟周期,前面已经看到了struct tvec_base结构的定义,它的成员tv1其实也是个数组,大小是256,分别对应expires为0~255个jiffies的定时器,如果有多个到期时间相同的定时器,则它们将会以双链表的形式链接到同一数组项中。其他的成员tv2、tv3、tv4和tv5用来存放到期时间更久的定时器,除此之外与tv1的原理是一样的。图8-2为向系统中添加一个定时器对象的示意图。

至此程序只是完成了向系统添加一个定时器对象的工作,接下来讨论添加的定时器对象在指定的时间到期时如何被触发,也就是定时器对象中的定时器函数何时被调用的问题。

我们知道,Linux内核一秒中都会发生很多次的时间中断,在每个时钟中断处理函数中,严格地说是时钟中断处理的下半部也就是softirq部分,会对tvec_bases管理的定时器队列进行扫描,以确定当前队列中有哪些定时器已经到期。

 图8-2  通过add_timer向系统新增一个定时器对象Linux内核中时钟中断的softirq为TIMER_SOFTIRQ,对应的软中断处理函数的安装发生在系统初始化阶段的init_timers函数中:
  1. <kernel/timer.c> 
  2. void __init init_timers(void)  
  3. {  
  4.     …  
  5.     open_softirq(TIMER_SOFTIRQ, run_timer_softirq);  
  6. }  

所以,当时钟中断的softirq被调度执行时,它将运行对应的run_timer_softirq函数。在每个时钟中断处理的上半部分,都会调用run_local_timers函数,后者则通过使用raise_softirq函数来触发时钟中断的softirq部分:
  1. <kernel/timer.c> 
  2. void run_local_timers(void)  
  3. {  
  4.     hrtimer_run_queues();  
  5.     raise_softirq(TIMER_SOFTIRQ);  
  6.     softlockup_tick();  
  7. }  

所以,当时钟中断的softirq部分被调度执行时,run_timer_softirq会负责扫描tvec_bases所在的定时器管理队列,找到已经到期的函数,然后调用到期定时器对象节点上的定时器函数:
  1. <kernel/timer.c> 
  2. static void run_timer_softirq(struct softirq_action *h)  
  3. {  
  4.     struct tvec_base *base = __get_cpu_var(tvec_bases);  
  5.     …  
  6.     if (time_after_eq(jiffies, base->timer_jiffies))  
  7.         __run_timers(base);  
  8. }  

run_timer_softirq对于那些到期的定时器队列调用__run_timers函数进一步处理,后者的部分核心代码如下:
  1. <kernel/timer.c> 
  2. static inline void __run_timers(struct tvec_base *base)  
  3. {  
  4.     struct timer_list *timer;  
  5.  
  6.     spin_lock_irq(&base->lock);  
  7.     while (time_after_eq(jiffies, base->timer_jiffies)) {  
  8.         …  
  9.         while (!list_empty(head)) {  
  10.             void (*fn)(unsigned long);  
  11.             unsigned long data;  
  12.  
  13.             timer = list_first_entry(head, struct timer_list,entry);  
  14.             fn = timer->function;  
  15.             data = timer->data;  
  16.             …  
  17.             detach_timer(timer, 1);  
  18.  
  19.             spin_unlock_irq(&base->lock);  
  20.             call_timer_fn(timer, fn, data);  
  21.             spin_lock_irq(&base->lock);  
  22.         }  
  23.     }  
  24. }  

函数的总体思想是,对tvec_bases管理的定时器队列进行扫描,如果发现有定时器到期(代码中用time_after_eq来进行判断),则调用该定时器对象的fn函数(fn = timer->function,fn(data)),这个过程发生在call_timer_fn函数中。读者需要注意,在调用call_timer_fn前__run_timers调用了detach_timer(timer, 1),该函数会把当前正在处理的定时器对象从tvec_bases中删除,所以当一个定时器对象中的定时函数被调用时,该定时器对象已经从系统的定时器队列中删除了,所以如果要让该定时器对象在以后能继续被系统所调用,则需要再次调用add_timer或者是mod_timer来将该定时器对象重新加入到系统中去,这是设备驱动程序用定时器来实现轮询机制的基本原理。

通过上面的讨论可以知道,由于内核对系统中的定时器队列的扫描发生在时钟中断的softirq部分,鉴于softirq的实现机制 ,在某些情况下可能会导致当一个定时器对象中的定时器函数被调用时,实际的jiffies值已经超出了当时安装定时器时预设的jiffies值,换句话说,使用定时器也同样存在着实际到期时间点延伸的问题,如果使用当中对定时精度有严格的要求,那么也许要考虑在现有的通用内核上加入某些实时性的扩展。


原创粉丝点击