void add_timer(struct timer_list *timer) { BUG_ON(timer_pending(timer)); mod_timer(timer, timer->expires); } ====> int mod_timer(struct timer_list *timer, unsigned long expires) { /* * This is a common optimization triggered by the * networking code - if the timer is re-modified * to be the same thing then just return: */ if (timer_pending(timer) && timer->expires == expires) return 1; /* * Decide where to put the timer while taking the slack into account * * Algorithm: * 1) calculate the maximum (absolute) time * 2) calculate the highest bit where the expires and new max are different * 3) use this bit to make a mask * 4) use the bitmask to round down the maximum time, so that all last * bits are zeros */ expires = apply_slack(timer, expires); return __mod_timer(timer, expires, false, TIMER_NOT_PINNED); } ====> static inline int __mod_timer(struct timer_list *timer, unsigned long expires, bool pending_only, int pinned) { struct tvec_base *base, *new_base; unsigned long flags; int ret = 0 , cpu; /* noused */ timer_stats_timer_set_start_info(timer); BUG_ON(!timer->function); /* * spin lock the tvec_t_base_s, * the table can used by one thread */ base = lock_timer_base(timer, &flags); if (timer_pending(timer)) { detach_timer(timer, 0); if (timer->expires == base->next_timer && !tbase_get_deferrable(timer->base)) base->next_timer = base->timer_jiffies; ret = 1; } else { if (pending_only) goto out_unlock; } debug_activate(timer, expires); cpu = smp_processor_id(); #if defined(CONFIG_NO_HZ) && defined(CONFIG_SMP) if (!pinned && get_sysctl_timer_migration() && idle_cpu(cpu)) { int preferred_cpu = get_nohz_load_balancer(); if (preferred_cpu >= 0) cpu = preferred_cpu; } #endif new_base = per_cpu(tvec_bases, cpu); if (base != new_base) { /* * We are trying to schedule the timer on the local CPU. * However we can't change timer's base while it is running, * otherwise del_timer_sync() can't detect that the timer's * handler yet has not finished. This also guarantees that * the timer is serialized wrt itself. */ if (likely(base->running_timer != timer)) { /* See the comment in lock_timer_base() */ timer_set_base(timer, NULL); spin_unlock(&base->lock); base = new_base; spin_lock(&base->lock); timer_set_base(timer, base); } } timer->expires = expires; /* cmp the time */ if (time_before(timer->expires, base->next_timer) && !tbase_get_deferrable(timer->base)) base->next_timer = timer->expires; /* * just a list, * timer is fifo */ internal_add_timer(base, timer); out_unlock: spin_unlock_irqrestore(&base->lock, flags); return ret; }
在内核中,定时器无处不在,比如延迟的工作队列,比如时钟中断,比如msleep()(调用schedule_timeout()),但是mdelay()是靠循环达到目的的。
转载:
定时器在操作系统中起到了举足轻重的作用。在做IO操作时,需要超时机制保证任务不处于无休止的等待状态;在延时处理时,可以通过“闹表”进行相对准点的唤醒操作。在多任务操作系统中,定时器是一种非常常用的资源。
对于熟悉硬件的工程师,定时器一般是芯片中的硬件定时器资源,实际上在操作系统中指的定时器资源并非局限于硬件资源,更重要的是软件定时器资源。硬件定时器资源通常实现操作系统的心跳,在uc/os中心跳频率默认值为200Hz,也就是5ms产生一次操作系统心跳。软件定时器是在操作系统心跳的基础上实现的。
下面对Linux中的定时器实现算法作详细分析。
Linux中定时器的实现
Linux中的定时器实现有点意思,在定时器较多的情况下实现效率较高,并且该算法思想可以在其它嵌入式系统中得以应用。
定时器实现的核心思想是采用了多级hash链表,并且每级hash的长度都不一样,多级hash链表可以类比为秒、分、时、天、月,每级的时间跨度都不一样。Linux中实现的五级hash链表关系如下图所示:
如上图所示为5级hash链表,V1为最低层的hash链表,V5为最顶层的hash链表。V1~V5 hash table的每一项为一条定时器链表,新添加的定时器会通过expire(定时时间值)和base->timer_jiffies(当前定时值)的差值expire_time来索引应该挂接到哪一层hash table中,并且索引到hash table中的具体项,然后将新增加的定时器加入到该项的timer list中。hash长度从V1到V5变得越来越大。V1的hash长度为1个jiffies,也就是说V1的hash table每一项为同一jiffies的定时器链表;V5的hash长度为64M jiffies,也就是hash table的每一项会链接连续64M jiffies定时长度的timer。不同层次的hash table具有不同的hash长度,离当前时间点越远的timer位于hash长度越长的table中,因为这些定时器还需要等待较长时间才能得以处理,所以可以采用大块分类的方法,离当前时间点越近的timer需要细粒度的切分,因为jiffies每变化一次都需要处理timer,所以最低层的table按一个jiffies进行切分。随着时间点的后移,不断的对最底层的hash table进行处理,并且通过当前时间点索引上层hash table,将定时器分配到下几级的hash table中。每个hash table与定时器之间的关系如下图所示。
Linux中描述多级hash table的数据结构如下:
struct tvec_t_base_s {
spinlock_t lock; /*多级hash table自旋锁 */
struct timer_list *running_timer; /*当前运行的定时器 */
unsigned long timer_jiffies; /*当前运行的jiffies */
tvec_root_t tv1; /* V1 hash table */
tvec_t tv2; /* V2 hash table */
tvec_t tv3; /* V3 hash table */
tvec_t tv4; /* V4 hash table */
tvec_t tv5; /* V5 hash table */
} ____cacheline_aligned_in_smp;
添加一个定时器需要进行hash table操作,根据定时值索引具体的定时器list,该算法描述如下:
1、 通过定时器的expire(定时时间点,以jiffies为基本度量单位)和当前的时间点(timer_jiffies)计算得到定时间隔idx。
2、 如果idx小于TVR_SIZE(通常为256个jiffies),那么可以将定时器加入到V1 hash table中,通过expire计算得到V1 hash table的索引块,然后加入到该块所对应的定时器链表中。
3、 如果idx位于V2~V5的hash table中,那么同样通过expire,根据(expire >> (TVR_BITS + N * TVN_BITS)) & TVN_MASK得到hash table中的块索引号,然后加入到该块所对应的定时器链表中。
当硬件定时器计完一个jiffies之后,会引起硬件中断,在硬件中断服务程序中会触发软中断,在定时器软中断服务程序中会调用__run_timers()完成定时器多级hash table的处理,并且处理定时时间到的所有timer。__run_timers算法实现描述如下:
1、 根据当前jiffes和base->timer_jiffies循环判断多级hash table扫描条件,如果满足条件,那么继续(2),否则退出循环。
2、 通过base->timer_jiffies计算得到V1 table中需要处理的索引项。并且将索引高层hash table中的具体项,将该项中的timer分散到低层table中去。
3、 增加base->timer_jiffies值,提取出V1中索引得到的定时器链表。
4、 如果该定时器链表不为空,那么依次处理链表中的定时器,处理过程为调用定时器的处理函数timer->function。
5、 循环至(1)。
Linux中定时器实现的优缺点
个人认为Linux中软定时器的实现还是非常漂亮的,特别对于定时器应用较多的场合效率非常高。其通过hash索引的方法避免了定时器链表的反复查询,并且其通过多级链表分类对待离当前时间点不同的定时器,离当前时间点较远的定时器可以在一个时间区间内共享一个定时器链表,离当前时间点较近的定时器只能是相同的定时时间点才能共享一个定时器链表。在uc/os嵌入式操作系统中,延时定时器是通过扫描、递减每个任务的当前定时值实现的,所以在硬件定时器的中断服务程序中不断扫描任务链,这样的做法可扩展性很差,效率较低。当定时的任务一多,中断服务程序就忙不过来了,成为系统瓶颈,甚至会影响心跳频率的选择。相比而言,Linux的定时器实现漂亮的多。但是,Linux的定时器实现会占用一定量的内存资源,一般来说,所有定时器链表为空时还需要占用2K的存储空间,所以,对于一些对内存资源非常在意的小型应用而言,需要慎重考虑这个算法。
用add_timer()函数来看timer_base的作用
static inline void add_timer(struct timer_list *timer) { BUG_ON(timer_pending(timer)); __mod_timer(timer, timer->expires); } int __mod_timer(struct timer_list *timer, unsigned long expires) { tvec_base_t *base, *new_base; unsigned long flags; int ret = 0; timer_stats_timer_set_start_info(timer); BUG_ON(!timer->function); base = lock_timer_base(timer, &flags);
如果timer已经放到定时链表中,则释放开
|--------------------------------| | if (timer_pending(timer)) { -| | detach_timer(timer, 0); -| | ret = 1; | | } | |--------------------------------|
获取当前CPU的timer base
|-----------------------------------------| | new_base = __get_cpu_var(tvec_bases); | |-----------------------------------------|
如果当前CPU的timer base不是当前timer中的base, 更新timer的base
|----------------------------------------------------| | if (base != new_base) { | | if (likely(base->running_timer != timer)) { -| | timer->base = NULL; | | spin_unlock(&base->lock); | | base = new_base; | | spin_lock(&base->lock); | | timer->base = base; | | } | | } | |----------------------------------------------------|
给定时器timer设置超时时间;并添加该时钟
|-------------------------------------| | timer->expires = expires; | | internal_add_timer(base, timer); -| |-------------------------------------| spin_unlock_irqrestore(&base->lock, flags); return ret; }
struct timer_list |-----------------------------------| |struct list_head entry | |unsigned long expires | |void (*function)(unsigned long)| |unsigned long data | |struct tvec_t_base_s *base |--+ |-----------------------------------| -| |void *start_site | -| CONFIG_TIMER_STATS |char start_comm[16]| -| |int start_pid | -| |-----------------------------------| -| | +--------------------------------------+ | 时钟向量base结构tvec_t_base_s | struct tvec_t_base_s +-->|---------------------------------| |spinlock_t lock | |struct timer_list *running_timer | |unsigned long timer_jiffies -| base的基准时间 |tvec_root_t tv1 | root hash链表 |tvec_t tv2 | 二级hash链表 |tvec_t tv3 | 三级hash链表 |tvec_t tv4 | 四级hash链表 |tvec_t tv5 | 五级hash链表 |---------------------------------| ____cacheline_aligned_in_smp
expires 定时器定时的滴答数(当前的滴答数为jiffies)
function 到那个时刻内核调用的函数
data 由于可能多个定时器调用一个函数,为了使得这个函数能够区分不同的定时器,通过在结构中data来标识这个定时器,并且通过调用function(data)使得function能区分它们,也就是 data起到ID的作用。
使用时钟,先声明一个timer_list结构,调用init_timer对它进行初始化。 time_list结构里expires是标明这个时钟的周期,单位采用jiffies的单位。function就是时间到了以后的回调函数,它的参数就是timer_list中的data。 data这个参数在初始化时钟的时候赋值,一般赋给它设备的device结构指针。在预置时间到系统调用function,同时系统把这个time_list从定时队列里清除。所以如果需要一直使用定时函数,要在function里再次调用add_timer()把这个timer_list加进定时队列。
管理定时器的接口
----------------------------------------------------------
创建定时器需要先定义它:
struct timer_list my_timer;
接着需要通过一个辅助函数初始化定时器数据结构的内部值,初始化必须在使用其他定时器管理函数对定时器进行操作前完成。
init_timer(&my_timer);
现在可以填充结构中需要的值了(与定时器直接相关的三个参数):
my_timer.expires = jiffies + delay; // 定时器超时时的节拍数
my_timer.data = 0; // 给定时器处理函数传入参数0
my_timer.function = my_function; // 定时器超时时调用的函数
my_timer.expires表示超时时间,它是以节拍为单位的绝对计数值。如果当前jiffies计数等于或大于my_timer.expires,那么my_function指向的处理函数就会开始执行,另外该函数还要使用长整型参数my_timer.data。如果你不需要这个参数,可以简单地传递0(或任何其他值)给处理函数。
最后,必须激活定时器:
add_timer(&my_timer);
大功告成,定时器可以工作了!但请注意定时值的重要性。当前节拍计数大于或等于指定的超时值时,内核就开始执行定时器处理函数。虽然内核可以保证不会在超时时间到期前运行定时器处理函数,但是有可能延误定时器的执行。一般来说,定时器都在超时后马上就回执行,但是也有可能被推迟到一下时钟节拍才能运行,所以不能用定时器来实现任何硬实时任务。
有时可能需要更改已经激活的定时器超时时间,所以内核通过函数mod_timer()来实现该功能,该函数可以改变指定的定时器超时时间:
mod_timer(&my_timer, jiffies+new_delay);
mod_timer()函数也可操作那些已经初始化,但还没有被激活的定时器,如果定时器未被激活,mod_timer()会激活它。如果调用时定时器未被激活,该函数返回0;否则返回1。但不论哪种情况,一旦从mod_timer()函数返回,定时器都将被激活而且设置了新的定时值。
如果需要在定时器超时前停止定时器,可以使用del_timer()函数:
del_timer(&my_timer);
被激活或未被激活的定时器都可以使用该函数,如果定时器还未被激活,该函数返回0;否则返回1。注意,你不需要为已经超时的定时器调用该函数,因为它们超时后会自动被删除。
当删除定时器时,必须小心一个潜在的竞争条件。当del_timer()返回后,可以保真的只是: 定时器不会再被激活(也就是,将来不会执行),但是在多处理机器上定时器中断可能已经在其他处理器上运行了,所以删除定时器时需要等待可能在其他处理器上运行的定时器处理程序都退出,这时就要使用del_timer_sync()函数执行删除工作:
del_timer_sync(&my_timer);
和del_timer()函数不同,del_timer_sync()函数不能在中断上下文中使用。
tvec_t, tvec_root_t hash链表 -- tvec_t_base_s中的hash链表 ------------------------------------------------------- --typedef struct tvec_s |------------------------| |struct list_head vec[0] | |------------------------| | | |------------------------| | | |------------------------| | | |------------------------| | | |------------------------| |struct list_head vec[31]| |------------------------| tvec_t typedef struct tvec_root_s |-------------------------| |struct list_head vec[0] -| |-------------------------| | | |-------------------------| | | |-------------------------| | | |-------------------------| | | |-------------------------| |struct list_head vec[128]| |-------------------------| tvec_root_t
TVN_SIZE = 32 (or 128)
TVR_SIZE = 128 (or 512)
-------------------------------------------------------
根据内核是否配置了CONFIG_BASE_SMALL来确定向量链表数是32还是128
#define TVN_BITS (CONFIG_BASE_SMALL ? 4 : 6)
根据内核是否配置了CONFIG_BASE_SMALL来确定root链表数是128还是512
#define TVR_BITS (CONFIG_BASE_SMALL ? 6 : 8)
#define TVN_SIZE (1 << TVN_BITS)
#define TVR_SIZE (1 << TVR_BITS)
#define TVN_MASK (TVN_SIZE - 1)
#define TVR_MASK (TVR_SIZE - 1)
boot_tvec_bases
-------------------------------------------------------
定义内核时钟base全局变量
typedef struct tvec_t_base_s tvec_base_t;
tvec_base_t boot_tvec_bases;
EXPORT_SYMBOL(boot_tvec_bases);
为每个CPU定义一个时钟向量基表
static DEFINE_PER_CPU(tvec_base_t *, tvec_bases) = { &boot_tvec_bases };
就位后的timer和tvec_t_bash_s的关系视图
---------------------------------------------------------- tvec_t_base_s |----------| | | | | | | | | | | | | | | | | | | timer_list timer_list timer_list |----------| |----------| |----------| |----------| | -vec[i] -|<---->|entry |<---->|entry |<---->|entry | |----------| |expires | |expires | |expires | | | |function()| |function()| |function()| | | |data | |data | |data | | | +--|*base | +--|*base | +--|*base | | | | -|----------| | -|----------| | -|----------| | | | | | | | | | | |----------|<--+-----------------+-----------------+