现在的位置: 首页 > 综合 > 正文

Linux时间子系统之六:高精度定时器(HRTIMER)的原理和实现

2014年01月04日 ⁄ 综合 ⁄ 共 19177字 ⁄ 字号 评论关闭

转自http://blog.csdn.net/droidphone/article/details/8074892

上一篇文章,我介绍了传统的低分辨率定时器的实现原理。而随着内核的不断演进,大牛们已经对这种低分辨率定时器的精度不再满足,而且,硬件也在不断地发展,系统中的定时器硬件的精度也越来越高,这也给高分辨率定时器的出现创造了条件。内核从2.6.16开始加入了高精度定时器架构。在实现方式上,内核的高分辨率定时器的实现代码几乎没有借用低分辨率定时器的数据结构和代码,内核文档给出的解释主要有以下几点:

  • 低分辨率定时器的代码和jiffies的关系太过紧密,并且默认按32位进行设计,并且它的代码已经经过长时间的优化,目前的使用也是没有任何错误,如果硬要基于它来实现高分辨率定时器,势必会打破原有的时间轮概念,并且会引入一大堆#if--#else判断;
  • 虽然大部分时间里,时间轮可以实现O(1)时间复杂度,但是当有进位发生时,不可预测的O(N)定时器级联迁移时间,这对于低分辨率定时器来说问题不大,可是它大大地影响了定时器的精度;
  • 低分辨率定时器几乎是为“超时”而设计的,并为此对它进行了大量的优化,对于这些以“超时”未目的而使用定时器,它们大多数期望在超时到来之前获得正确的结果,然后删除定时器,精确时间并不是它们主要的目的,例如网络通信、设备IO等等。

为此,内核为高精度定时器重新设计了一套软件架构,它可以为我们提供纳秒级的定时精度,以满足对精确时间有迫切需求的应用程序或内核驱动,例如多媒体应用,音频设备的驱动程序等等。以下的讨论用hrtimer(high resolution timer)表示高精度定时器。
/*****************************************************************************************************/
声明:本博内容均由http://blog.csdn.net/droidphone原创,转载请注明出处,谢谢!
/*****************************************************************************************************/

1.  如何组织hrtimer?

我们知道,低分辨率定时器使用5个链表数组来组织timer_list结构,形成了著名的时间轮概念,对于高分辨率定时器,我们期望组织它们的数据结构至少具备以下条件:

  • 稳定而且快速的查找能力;
  • 快速地插入和删除定时器的能力;
  • 排序功能;

内核的开发者考察了多种数据结构,例如基数树、哈希表等等,最终他们选择了红黑树(rbtree)来组织hrtimer,红黑树已经以库的形式存在于内核中,并被成功地使用在内存管理子系统和文件系统中,随着系统的运行,hrtimer不停地被创建和销毁,新的hrtimer按顺序被插入到红黑树中,树的最左边的节点就是最快到期的定时器,内核用一个hrtimer结构来表示一个高精度定时器:

  1. struct hrtimer {  
  2.     struct timerqueue_node      node;  
  3.     ktime_t             _softexpires;  
  4.     enum hrtimer_restart        (*function)(struct hrtimer *);  
  5.     struct hrtimer_clock_base   *base;  
  6.     unsigned long           state;  
  7.         ......  
  8. };  

定时器的到期时间用ktime_t来表示,_softexpires字段记录了时间,定时器一旦到期,function字段指定的回调函数会被调用,该函数的返回值为一个枚举值,它决定了该hrtimer是否需要被重新激活:

  1. enum hrtimer_restart {  
  2.     HRTIMER_NORESTART,  /* Timer is not restarted */  
  3.     HRTIMER_RESTART,    /* Timer must be restarted */  
  4. };  

state字段用于表示hrtimer当前的状态,有几下几种位组合:

  1. #define HRTIMER_STATE_INACTIVE  0x00  // 定时器未激活
      
  2. #define HRTIMER_STATE_ENQUEUED  0x01  // 定时器已经被排入红黑树中
      
  3. #define HRTIMER_STATE_CALLBACK  0x02  // 定时器的回调函数正在被调用
      
  4. #define HRTIMER_STATE_MIGRATE   0x04  // 定时器正在CPU之间做迁移  

hrtimer的到期时间可以基于以下几种时间基准系统:

  1. enum  hrtimer_base_type {  
  2.     HRTIMER_BASE_MONOTONIC,  // 单调递增的monotonic时间,不包含休眠时间
      
  3.     HRTIMER_BASE_REALTIME,   // 平常使用的墙上真实时间
      
  4.     HRTIMER_BASE_BOOTTIME,   // 单调递增的boottime,包含休眠时间
      
  5.     HRTIMER_MAX_CLOCK_BASES, // 用于后续数组的定义
      
  6. };  

和低分辨率定时器一样,处于效率和上锁的考虑,每个cpu单独管理属于自己的hrtimer,为此,专门定义了一个结构hrtimer_cpu_base:

  1. struct hrtimer_cpu_base {  
  2.         ......  
  3.     struct hrtimer_clock_base   clock_base[HRTIMER_MAX_CLOCK_BASES];  
  4. };  

其中,clock_base数组为每种时间基准系统都定义了一个hrtimer_clock_base结构,它的定义如下:

  1. struct hrtimer_clock_base {  
  2.     struct hrtimer_cpu_base *cpu_base;  // 指向所属cpu的hrtimer_cpu_base结构
      
  3.         ......  
  4.     struct timerqueue_head  active;     // 红黑树,包含了所有使用该时间基准系统的hrtimer
      
  5.     ktime_t         resolution; // 时间基准系统的分辨率
      
  6.     ktime_t         (*get_time)(void); // 获取该基准系统的时间函数
      
  7.     ktime_t         softirq_time;// 当用jiffies
      
  8.     ktime_t         offset;      // 
      
  9. };  

active字段是一个timerqueue_head结构,它实际上是对rbtree的进一步封装:

  1. struct timerqueue_node {  
  2.     struct rb_node node;  // 红黑树的节点
      
  3.     ktime_t expires;      // 该节点代表队hrtimer的到期时间,与hrtimer结构中的_softexpires稍有不同
      
  4. };  
  5.   
  6. struct timerqueue_head {  
  7.     struct rb_root head;          // 红黑树的根节点
      
  8.     struct timerqueue_node *next; // 该红黑树中最早到期的节点,也就是最左下的节点
      
  9. };  

timerqueue_head结构在红黑树的基础上,增加了一个next字段,用于保存树中最先到期的定时器节点,实际上就是树的最左下方的节点,有了next字段,当到期事件到来时,系统不必遍历整个红黑树,只要取出next字段对应的节点进行处理即可。timerqueue_node用于表示一个hrtimer节点,它在标准红黑树节点rb_node的基础上增加了expires字段,该字段和hrtimer中的_softexpires字段一起,设定了hrtimer的到期时间的一个范围,hrtimer可以在hrtimer._softexpires至timerqueue_node.expires之间的任何时刻到期,我们也称timerqueue_node.expires为硬过期时间(hard),意思很明显:到了此时刻,定时器一定会到期,有了这个范围可以选择,定时器系统可以让范围接近的多个定时器在同一时刻同时到期,这种设计可以降低进程频繁地被hrtimer进行唤醒。经过以上的讨论,我们可以得出以下的图示,它表明了每个cpu上的hrtimer是如何被组织在一起的:

                                                       图 1.1  每个cpu的hrtimer组织结构

总结一下:

  • 每个cpu有一个hrtimer_cpu_base结构;
  • hrtimer_cpu_base结构管理着3种不同的时间基准系统的hrtimer,分别是:实时时间,启动时间和单调时间;
  • 每种时间基准系统通过它的active字段(timerqueue_head结构指针),指向它们各自的红黑树;
  • 红黑树上,按到期时间进行排序,最先到期的hrtimer位于最左下的节点,并被记录在active.next字段中;
  • 3中时间基准的最先到期时间可能不同,所以,它们之中最先到期的时间被记录在hrtimer_cpu_base的expires_next字段中。

2.  hrtimer如何运转

hrtimer的实现需要一定的硬件基础,它的实现依赖于我们前几章介绍的timekeeper和clock_event_device,如果你对timekeeper和clock_event_device不了解请参考以下文章:Linux时间子系统之三:时间的维护者:timekeeperLinux时间子系统之四:定时器的引擎:clock_event_device。hrtimer系统需要通过timekeeper获取当前的时间,计算与到期时间的差值,并根据该差值,设定该cpu的tick_device(clock_event_device)的下一次的到期时间,时间一到,在clock_event_device的事件回调函数中处理到期的hrtimer。现在你或许有疑问:前面在介绍clock_event_device时,我们知道,每个cpu有自己的tick_device,通常用于周期性地产生进程调度和时间统计的tick事件,这里又说要用tick_device调度hrtimer系统,通常cpu只有一个tick_device,那他们如何协调工作?这个问题也一度困扰着我,如果再加上NO_HZ配置带来tickless特性,你可能会更晕。这里我们先把这个疑问放下,我将在后面的章节中来讨论这个问题,现在我们只要先知道,一旦开启了hrtimer,tick_device所关联的clock_event_device的事件回调函数会被修改为:hrtimer_interrupt,并且会被设置成工作于CLOCK_EVT_MODE_ONESHOT单触发模式。

2.1  添加一个hrtimer

要添加一个hrtimer,系统提供了一些api供我们使用,首先我们需要定义一个hrtimer结构的实例,然后用hrtimer_init函数对它进行初始化,它的原型如下:

  1. void hrtimer_init(struct hrtimer *timer, clockid_t which_clock,  
  2.              enum hrtimer_mode mode);  

which_clock可以是CLOCK_REALTIME、CLOCK_MONOTONIC、CLOCK_BOOTTIME中的一种,mode则可以是相对时间HRTIMER_MODE_REL,也可以是绝对时间HRTIMER_MODE_ABS。设定回调函数:

  1. timer.function = hr_callback;  

如果定时器无需指定一个到期范围,可以在设定回调函数后直接使用hrtimer_start激活该定时器:

  1. int hrtimer_start(struct hrtimer *timer, ktime_t tim,  
  2.              const enum hrtimer_mode mode);  

如果需要指定到期范围,则可以使用hrtimer_start_range_ns激活定时器:

  1. hrtimer_start_range_ns(struct hrtimer *timer, ktime_t tim,  
  2.             unsigned long range_ns, const enum hrtimer_mode mode);  

要取消一个hrtimer,使用hrtimer_cancel:

  1. int hrtimer_cancel(struct hrtimer *timer);  

以下两个函数用于推后定时器的到期时间:

  1. extern u64  
  2. hrtimer_forward(struct hrtimer *timer, ktime_t now, ktime_t interval);  
  3.   
  4. /* Forward a hrtimer so it expires after the hrtimer's current now */  
  5. static inline u64 hrtimer_forward_now(struct hrtimer *timer,  
  6.                       ktime_t interval)  
  7. {  
  8.     return hrtimer_forward(timer, timer->base->get_time(), interval);  
  9. }  

以下几个函数用于获取定时器的当前状态:

  1. static inline int hrtimer_active(const struct hrtimer *timer)  
  2. {  
  3.     return timer->state != HRTIMER_STATE_INACTIVE;  
  4. }  
  5.   
  6. static inline int hrtimer_is_queued(struct hrtimer *timer)  
  7. {  
  8.     return timer->state & HRTIMER_STATE_ENQUEUED;  
  9. }  
  10.   
  11. static inline int hrtimer_callback_running(struct hrtimer *timer)  
  12. {  
  13.     return timer->state & HRTIMER_STATE_CALLBACK;  
  14. }  

hrtimer_init最终会进入__hrtimer_init函数,该函数的主要目的是初始化hrtimer的base字段,同时初始化作为红黑树的节点的node字段:

  1. static void __hrtimer_init(struct hrtimer *timer, clockid_t clock_id,  
  2.                enum hrtimer_mode mode)  
  3. {  
  4.     struct hrtimer_cpu_base *cpu_base;  
  5.     int base;  
  6.   
  7.     memset(timer, 0, sizeof(struct hrtimer));  
  8.   
  9.     cpu_base = &__raw_get_cpu_var(hrtimer_bases);  
  10.   
  11.     if (clock_id == CLOCK_REALTIME && mode != HRTIMER_MODE_ABS)  
  12.         clock_id = CLOCK_MONOTONIC;  
  13.   
  14.     base = hrtimer_clockid_to_base(clock_id);  
  15.     timer->base = &cpu_base->clock_base[base];  
  16.     timerqueue_init(&timer->node);  
  17.         ......  
  18. }  

hrtimer_start和hrtimer_start_range_ns最终会把实际的工作交由__hrtimer_start_range_ns来完成:

  1. int __hrtimer_start_range_ns(struct hrtimer *timer, ktime_t tim,  
  2.         unsigned long delta_ns, const enum hrtimer_mode mode,  
  3.         int wakeup)  
  4. {  
  5.         ......          
  6.         /* 取得hrtimer_clock_base指针 */  
  7.         base = lock_hrtimer_base(timer, &flags);   
  8.         /* 如果已经在红黑树中,先移除它: */  
  9.         ret = remove_hrtimer(timer, base); ......  
  10.         /* 如果是相对时间,则需要加上当前时间,因为内部是使用绝对时间 */  
  11.         if (mode & HRTIMER_MODE_REL) {  
  12.                 tim = ktime_add_safe(tim, new_base->get_time());  
  13.                 ......  
  14.         }   
  15.         /* 设置到期的时间范围 */  
  16.         hrtimer_set_expires_range_ns(timer, tim, delta_ns);  
  17.         ......   
  18.         /* 把hrtime按到期时间排序,加入到对应时间基准系统的红黑树中 */  
  19.         /* 如果该定时器的是最早到期的,将会返回true */  
  20.         leftmost = enqueue_hrtimer(timer, new_base);  
  21.         /*  
  22.         * Only allow reprogramming if the new base is on this CPU.  
  23.         * (it might still be on another CPU if the timer was pending)  
  24.         *  
  25.         * XXX send_remote_softirq() ? 
  26.         * 定时器比之前的到期时间要早,所以需要重新对tick_device进行编程,重新设定的的到期时间 
  27.         */  
  28.         if (leftmost && new_base->cpu_base == &__get_cpu_var(hrtimer_bases))  
  29.                 hrtimer_enqueue_reprogram(timer, new_base, wakeup);  
  30.         unlock_hrtimer_base(timer, &flags);  
  31.         return ret;  
  32. }  
  33. <P>  
  34. </P>  
2.2  hrtimer的到期处理

高精度定时器系统有3个入口可以对到期定时器进行处理,它们分别是:

  • 没有切换到高精度模式时,在每个jiffie的tick事件中断中进行查询和处理;
  • 在HRTIMER_SOFTIRQ软中断中进行查询和处理;
  • 切换到高精度模式后,在每个clock_event_device的到期事件中断中进行查询和处理;

低精度模式  因为系统并不是一开始就会支持高精度模式,而是在系统启动后的某个阶段,等待所有的条件都满足后,才会切换到高精度模式,当系统还没有切换到高精度模式时,所有的高精度定时器运行在低精度模式下,在每个jiffie的tick事件中断中进行到期定时器的查询和处理,显然这时候的精度和低分辨率定时器是一样的(HZ级别)。低精度模式下,每个tick事件中断中,hrtimer_run_queues函数会被调用,由它完成定时器的到期处理。hrtimer_run_queues首先判断目前高精度模式是否已经启用,如果已经切换到了高精度模式,什么也不做,直接返回:

  1. void hrtimer_run_queues(void)  
  2. {  
  3.   
  4.     if (hrtimer_hres_active())  
  5.         return;  

如果hrtimer_hres_active返回false,说明目前处于低精度模式下,则继续处理,它用一个for循环遍历各个时间基准系统,查询每个hrtimer_clock_base对应红黑树的左下节点,判断它的时间是否到期,如果到期,通过__run_hrtimer函数,对到期定时器进行处理,包括:调用定时器的回调函数、从红黑树中移除该定时器、根据回调函数的返回值决定是否重新启动该定时器等等:

  1. for (index = 0; index < HRTIMER_MAX_CLOCK_BASES; index++) {  
  2.     base = &cpu_base->clock_base[index];  
  3.     if (!timerqueue_getnext(&base->active))  
  4.         continue;  
  5.   
  6.     if (gettime) {  
  7.         hrtimer_get_softirq_time(cpu_base);  
  8.         gettime = 0;  
  9.     }  
  10.   
  11.     raw_spin_lock(&cpu_base->lock);  
  12.   
  13.     while ((node = timerqueue_getnext(&base->active))) {  
  14.         struct hrtimer *timer;  
  15.   
  16.         timer = container_of(node, struct hrtimer, node);  
  17.         if (base->softirq_time.tv64 <=  
  18.                 hrtimer_get_expires_tv64(timer))  
  19.             break;  
  20.   
  21.         __run_hrtimer(timer, &base->softirq_time);  
  22.     }  
  23.     raw_spin_unlock(&cpu_base->lock);  
  24. }  

上面的timerqueue_getnext函数返回红黑树中的左下节点,之所以可以在while循环中使用该函数,是因为__run_hrtimer会在移除旧的左下节点时,新的左下节点会被更新到base->active->next字段中,使得循环可以继续执行,直到没有新的到期定时器为止。

高精度模式  切换到高精度模式后,原来给cpu提供tick事件的tick_device(clock_event_device)会被高精度定时器系统接管,它的中断事件回调函数被设置为hrtimer_interrupt,红黑树中最左下的节点的定时器的到期时间被编程到该clock_event_device中,这样每次clock_event_device的中断意味着至少有一个高精度定时器到期。另外,当timekeeper系统中的时间需要修正,或者clock_event_device的到期事件时间被重新编程时,系统会发出HRTIMER_SOFTIRQ软中断,软中断的处理函数run_hrtimer_softirq最终也会调用hrtimer_interrupt函数对到期定时器进行处理,所以在这里我们只要讨论hrtimer_interrupt函数的实现即可。

hrtimer_interrupt函数的前半部分和低精度模式下的hrtimer_run_queues函数完成相同的事情:它用一个for循环遍历各个时间基准系统,查询每个hrtimer_clock_base对应红黑树的左下节点,判断它的时间是否到期,如果到期,通过__run_hrtimer函数,对到期定时器进行处理,所以我们只讨论后半部分,在处理完所有到期定时器后,下一个到期定时器的到期时间保存在变量expires_next中,接下来的工作就是把这个到期时间编程到tick_device中:

  1. void hrtimer_interrupt(struct clock_event_device *dev)  
  2. {  
  3.         ......  
  4.     for (i = 0; i < HRTIMER_MAX_CLOCK_BASES; i++) {  
  5.                 ......  
  6.         while ((node = timerqueue_getnext(&base->active))) {  
  7.                         ......  
  8.             if (basenow.tv64 < hrtimer_get_softexpires_tv64(timer)) {  
  9.                 ktime_t expires;  
  10.   
  11.                 expires = ktime_sub(hrtimer_get_expires(timer),  
  12.                             base->offset);  
  13.                 if (expires.tv64 < expires_next.tv64)  
  14.                     expires_next = expires;  
  15.                 break;  
  16.             }  
  17.   
  18.             __run_hrtimer(timer, &basenow);  
  19.         }  
  20.     }  
  21.   
  22.     /* 
  23.      * Store the new expiry value so the migration code can verify 
  24.      * against it. 
  25.      */  
  26.     cpu_base->expires_next = expires_next;  
  27.     raw_spin_unlock(&cpu_base->lock);  
  28.   
  29.     /* Reprogramming necessary ? */  
  30.     if (expires_next.tv64 == KTIME_MAX ||  
  31.         !tick_program_event(expires_next, 0)) {  
  32.         cpu_base->hang_detected = 0;  
  33.         return;  
  34.     }  

如果这时的tick_program_event返回了非0值,表示过期时间已经在当前时间的前面,这通常由以下原因造成:

  • 系统正在被调试跟踪,导致时间在走,程序不走;
  • 定时器的回调函数花了太长的时间;
  • 系统运行在虚拟机中,而虚拟机被调度导致停止运行;

为了避免这些情况的发生,接下来系统提供3次机会,重新执行前面的循环,处理到期的定时器:

  1. raw_spin_lock(&cpu_base->lock);  
  2. now = hrtimer_update_base(cpu_base);  
  3. cpu_base->nr_retries++;  
  4. if (++retries < 3)  
  5.     goto retry;  

如果3次循环后还无法完成到期处理,系统不再循环,转为计算本次总循环的时间,然后把tick_device的到期时间强制设置为当前时间加上本次的总循环时间,不过推后的时间被限制在100ms以内:

  1.     delta = ktime_sub(now, entry_time);  
  2.     if (delta.tv64 > cpu_base->max_hang_time.tv64)  
  3.         cpu_base->max_hang_time = delta;  
  4.     /* 
  5.      * Limit it to a sensible value as we enforce a longer 
  6.      * delay. Give the CPU at least 100ms to catch up. 
  7.      */  
  8.     if (delta.tv64 > 100 * NSEC_PER_MSEC)  
  9.         expires_next = ktime_add_ns(now, 100 * NSEC_PER_MSEC);  
  10.     else  
  11.         expires_next = ktime_add(now, delta);  
  12.     tick_program_event(expires_next, 1);  
  13.     printk_once(KERN_WARNING "hrtimer: interrupt took %llu ns\n",  
  14.             ktime_to_ns(delta));  
  15. }  

3.  切换到高精度模式

上面提到,尽管内核配置成支持高精度定时器,但并不是一开始就工作于高精度模式,系统在启动的开始阶段,还是按照传统的模式在运行:tick_device按HZ频率定期地产生tick事件,这时的hrtimer工作在低分辨率模式,到期事件在每个tick事件中断中由hrtimer_run_queues函数处理,同时,在低分辨率定时器(时间轮)的软件中断TIMER_SOFTIRQ中,hrtimer_run_pending会被调用,系统在这个函数中判断系统的条件是否满足切换到高精度模式,如果条件满足,则会切换至高分辨率模式,另外提一下,NO_HZ模式也是在该函数中判断并切换。

  1. void hrtimer_run_pending(void)  
  2. {  
  3.     if (hrtimer_hres_active())  
  4.         return;  
  5.         ......  
  6.     if (tick_check_oneshot_change(!hrtimer_is_hres_enabled()))  
  7.         hrtimer_switch_to_hres();  
  8. }  

因为不管系统是否工作于高精度模式,每个TIMER_SOFTIRQ期间,该函数都会被调用,所以函数一开始先用hrtimer_hres_active判断目前高精度模式是否已经激活,如果已经激活,则说明之前的调用中已经切换了工作模式,不必再次切换,直接返回。hrtimer_hres_active很简单:

  1. DEFINE_PER_CPU(struct hrtimer_cpu_base, hrtimer_bases) = {  
  2.         ......  
  3. }  
  4.   
  5. static inline int hrtimer_hres_active(void)  
  6. {  
  7.     return __this_cpu_read(hrtimer_bases.hres_active);  
  8. }  

hrtimer_run_pending函数接着通过tick_check_oneshot_change判断系统是否可以切换到高精度模式,

【上篇】
【下篇】

抱歉!评论已关闭.