高精度定时器Hrtimer
工欲善其事必先利其器,在开始讲之前,我们先利一下器:
相关的用到的几个源代码文件以及其路径如下:
Hrtimers.txt (linux-3.2.12\documentation\timers)
Hrtimer.c (linux-3.2.12\kernel)
Hrtimer.h (linux-3.2.12\include\linux)
单纯的在高精度定时器模式下操作高精度定时器,整个操作框架如下:
初始化hrtimer_init,通过hetimer结构体设置相关数据,比如定时时长等->开启定时器hrtimer_start->运行高精度定时器hrtimer_run_queues->触发中断,调用中断回调函数,hrtimer_interrupt->移除高精度定时器 remove_hrtimer.
读者现在脑子里有一个框架,具体驱动细节下文将一一阐述。
先概述一下,可能会有些生僻的东西在里面难理解,不要紧,后面会有相关的代码和例子来解释。
高精度定时器按照时间在一棵红黑树上排序。
他们独立于周期时钟,采用纳秒时间戳而非jiffies的时间规格。
先把Linux代码里面的高精度定时器相关的文档拿出来,看看他的介绍,然后我再解释一下,文档路径:Hrtimers.txt (linux-3.2.12\documentation\timers)
文档内容……说实话文档有点长,而且Linux的文档维护度不是很高,我从里面找了几句话翻译出来,然后解释一下:
This patch introduces a new subsystem for high-resolution kernel timers.这句话里的patch这个单词有点意思,他是说高精度定时器作为一个补丁包被安装到系统里的,在2.6.16之前是没有这个概念的。
第二点,英文太长就不贴了,是说为什么要用高精度定时器,因为每个系统都存在定时器,当然精度不高,相比较而言就称之为低精度定时器。说白了就是需要高精度。
第三点,高精度定时器还有个特点,就是他的框架在编译的时候是在内核里,但是如果没有配置高精度定时器,那么高精度定时器是按照普通的定时器来运行。
最后一点,高精度定时器是采用红黑树算法实现的,而普通的定时器是采用时间轮循算法实现的.
另外,文档中还解释了很多比如时钟源、数据结构、红黑树等等问题,这些问题在下面分开阐述。
一、相关的数据结构
高分辨率定时器所涉及的数据结构,我们从这几大方面考虑:
关于来源:就是说这个时钟是怎么来的,在hrtimer.h中定义了一个结构体,代码如下:
struct hrtimer_clock_base {
struct hrtimer_cpu_base *cpu_base;
int index; //用于区分时钟的属性(一共有两种,下面将会提及)
clockid_t clockid; //每个CPU所支持的时钟的ID
struct timerqueue_head active; //正在启用的定时器的红黑树根节点
ktime_t resolution; //时钟的分辨率,纳秒
ktime_t (*get_time)(void); //用来恢复当前时钟
ktime_t softirq_time; //在软中断中运行高精度定时器队列的时间
ktime_t offset; //修改定时器时钟的额偏移量
};
关于上面的几个元素,,有些东西解释一下。
高分辨率定时器可以基于两种时钟(时钟基础,clock base):一种是单调时钟(CLOCK_MONOTONIC),在系统启动时,从0开始;另一种时钟(CLOCK_REALTIME)表示系统的实际时间。
上文的结构体struct hrtimer_clock_base中,index元素就是用来区分是CLOCK_MONOTONIC还是CLOCK_REALTIME时钟的。对于系统的每一个CPU都提供了一个包含这两种时钟基础的数据结构,每一个时候总时钟基础都有一个红黑树,来排序所有待决的高精度定时器,而每个CPU都提供两个时钟基础(单调时钟和实际时间),所有定时器都按过期时间在红黑树上排序,如果定时器已经到期但其处理程序回调函数尚未执行,则从红黑树迁移到一个链表中。在调整实时时钟的时候,会造成存储在CLOCK_REALTIME时钟基础上的定时器的过期时间值与当前实际时间之间的偏差。offset字段有助于修正这种情况,他表示定时器需要校正的偏移量。由于这只是一种临时效应,很少发生。
在认识时钟源之前,我们其实还应该认识一个结构体struct hrtimer,代码如下:
struct hrtimer {
struct timerqueue_node node; //定时器队列节点, 同时还管理 node.expires,高精度定时器的绝对失效时间在其内部的算法这个时间和定时器基于的时钟有关(就是上文说的那两种时基).
ktime_t _softexpires; //绝对的最早的到期时间
enum hrtimer_restart (*)(struct hrtimer *); //定时器到期回调函数
struct hrtimer_clock_base *base; //指向时基的指针(每个CPU,每个时钟)
unsigned long state; //状态信息,用来看位值
#ifdef CONFIG_TIMER_STATS
int start_pid; //定时器统计区域存储的开始计时的任务的pid
void *start_site; //定时器存放当前定时的开始值
char start_comm[16]; //定时器统计区域名称开始计时的存储过程
#endif
};
以上这个结构体, 对于用户来说,只需关心三点,第一是字段,这个是定时器失效后的回调函数,第二是expires,这个表示到期时间,第三是最后一句话,高精度定时器结构体的使用必须经过 hrtimer_init()函数的初始化,hrtimer_init()函数属于应用接口,所以放在后面说。这里还有一个问题,也是高精度定时器的核心的问题,就是红黑树在高精度定时器的应用问题,当然现在讲这个有点早,但是先让读者心里有这么个底,Linux的传统定时器通过时间轮算法实现(timer.c),但hrtimer通过红黑树算法实现。在struct hrtimer里面有一个node域,类型为struct timerqueue_node,这个域代表了hrtimer在红黑树中的位置,注意一下,我参考的源代码是3.2.12版本,在2.6.X版本中这个域的格式为struct rb_node。这里先跟读者打声招呼,有这么回事,等在将具体用法时,我们在说说是如何实现的。
两个重要的结构体说完了,由于要兼容多核处理器,因此会涉及到每个CPU的时基,结构体struct hrtimer_cpu_base就是用来定义每个CPU的时钟的,目前每个CPU只对应于单调时钟和实时时钟,结构体如下:
struct hrtimer_cpu_base { //单个CPU时基结构体
raw_spinlock_t lock; //锁定相关的时基和定时器,自旋锁
unsigned long active_bases; //用活动的定时器标记基础的位字段
#ifdef CONFIG_HIGH_RES_TIMERS
ktime_t expires_next; //将要到期的下一个时间的绝对时间
int hres_active; //高分辨率模式的状态,布尔变量
int hang_detected; //最新的被发现的挂起的高精度定时器中断
unsigned long nr_events; //高精度定时器中断总数
unsigned long nr_retries; //高精度定时器中断重试总数
unsigned long nr_hangs; //高精度定时器中断挂起总数
ktime_t max_hang_time; //在高精度定时器中断触发最长时间
#endif
struct hrtimer_clock_base clock_base[HRTIMER_MAX_CLOCK_BASES]; //此CPU时基指针
};
上面的三个结构体应该是最基础的,定义了高精度定时器相关的功能和元素,并且每一个CPU都全套一个定义的结构体,然后初始化hrtimers。
基本的结构体讲完了,下面开始讲API接口。
首先是配置并初始化hrtimers的API。在开始的时候我们讲struct hrtimer的时候,提到要使用struct hrtimer要先初始化,函数声明代码如下:
void hrtimer_init(struct hrtimer *timer, clockid_t clock_id,enum hrtimer_mode mode) //给定时钟初始化定时器
{
debug_init(timer, clock_id, mode);
__hrtimer_init(timer, clock_id, mode);
}
以上函数实现了一个高精度定时器的初始化,下面是相关元素的解释:
/**
* hrtimer_init – 给定时钟初始化定时器
* @timer: 将要被初始化的定时器
* @clock_id: 将要被用到的时钟
* @mode: 定时器模式 abs/rel
*/
mode可以使用五个常数,如下:
enum hrtimer_mode {
HRTIMER_MODE_ABS = 0x0, /* 时间是绝对的 */
HRTIMER_MODE_REL = 0x1, /*时间是相对的 */
HRTIMER_MODE_PINNED = 0x02, /* 定时器被绑定到CPU */
HRTIMER_MODE_ABS_PINNED = 0x02,
HRTIMER_MODE_REL_PINNED = 0x03,
};
hrtimer_init()函数里面调用了__hrtimer_init()函数,下面是该函数的原型:
static void __hrtimer_init(struct hrtimer *timer, clockid_t clock_id,enum hrtimer_mode mode)
{
struct hrtimer_cpu_base *cpu_base;
int base;
memsettimer, 0, sizeof(struct hrtimer));
cpu_base = &__raw_get_cpu_var(hrtimer_bases);
if (clock_id == CLOCK_REALTIME && mode != HRTIMER_MODE_ABS)
clock_id = CLOCK_MONOTONIC;
base = hrtimer_clockid_to_base(clock_id);
timer->base = &cpu_base->clock_base[base];
timerqueue_init(&timer->node);
#ifdef CONFIG_TIMER_STATS
timer->start_site = NULL;
timer->start_pid = -1;
memset(timer->start_comm, 0, TASK_COMM_LEN);
#endif
}
__hrtimer_init()函数调用了struct hrtimer_cpu_base结构体对CPU进行相关的初始化,并使用memset()函数。
在这里注意一下,还是前面说到的一个问题,我用的源代码是3.2.12的,而2.6.X的源代码里所提供的,其实只有两个常数
二、相关的接口代码
定时器初始化之后,进行设定定时器的到期时间,并启动定时器,
函数声明代码hrtimer_start(struct hrtimer *timer, ktime_t tim, const enum hrtimer_mode mode),
timer代表将要被添加的定时器,tim代表到期时间,mode代表定时器模式。如果启动成功,则返回0,否则返回1。
如果要 取消一个设置好的定时器,可以使用int hrtimer_cancel(struct hrtimer *timer)和int hrtimer_try_to_cancel(struct hrtimer *timer),这两个函数的区别是,后者提供了额外的返回值-1,如果定时器当前正在执行因而无法停止,则返回-1.在这种情况下,hrtimer_cancel会一直等处理程序执行完毕。另外,如果定时器处于未激活状态,两个函数的返回值都是0,如果处于激活状态(即状态为HRTIMER_STATE_INACTIVE或者HRTIMER_STATE_ENQUEUED),二者都返回1(读者务必注意,在2.6.X版本的源代码中,处于激活状态的两个常数是HRTIMER_STATE_PENDING或者HRTIMER_STATE_ENQUEUED,无非改了模样了,这个注意一下就好)。
如果要重启一个取消的定时器,可以使用static inline int hrtimer_restart(struct hrtimer *timer)。
上面讲的几个函数应该是最基本的,也没什么大的讲头,下面的函数才是高精度定时器的精彩之处,也就四高精度定时器的到期机制和回调函数的运行方式,运用红黑树的方法既节省资源又提高效率。
在讲精彩之处之前,首先要跟读者说一下几个补充知识,其实前面我们也已经提到了,无非这里重复和补充一下:
(1)高精度定时器按照时间在一棵红黑树上排序。
(2)他们独立于周期时钟, 采用纳秒时间戳而非jiffies的时间规格。
This patch introduces a new subsystem for high-resolution kernel timers.这句话里的patch这个单词有点意思,他是说高精度定时器作为一个补丁包被安装到系统里的,在2.6.16之前是没有这个概念的。
(3)高精度定时器的框架在编译的时候是在内核里,但是 如果没有配置高精度定时器,那么高精度定时器是按照普通的定时器来运行。
(4)最后一点,高精度定时器是采用红黑树算法实现的,而普通的定时器是采用时间轮循算法实现的.
根据上面的几点,我们可以知道高精度定时器框架总是有一部分会编译到内核中去的,即使禁止了对高分辨率定时器的支持。在这种情况下,高分辨率定时器的到期是有一个低分辨率时钟驱动的。这避免了代码复制,因为高分辨率定时器的用户,在没有高分辨率计时能力的系统上,无需对时间相关代码提供一个额外的版本。这种情况下,仍然会采用高分辨率框架,但只是以低分辨率运行。即使高分辨率定时器支持已经编译到内核中,但在启动时只提供了低分辨率计时功能,这与上述情况是相同的。因此我们在这里讲的高分辨率定时器,要分成两种情况,一种是高分辨率模式下的高分辨率定时器,另一种是高分辨率模式下的低分辨率定时器。
前面我们讲了定时器的初始化和打开,下面讲定时器中断相关,这里我们只讲在高分辨率模式下的hrtimer的工作情况。
首先我们假定一个高分辨率时钟已经设置好而且正在运行,而向高分辨率模式的迁移已经完全完成,那么接下来,一般会发生以下情形:负责高分辨率定时器的时钟事件设备引发中断,调用hrtimer_interrupt(struct clock_event_device *dev)作为事件处理程序,该函数负责选中所有到期的定时器,然后直接执行中断程序,当然也可能将其转移到过期链表中(如果他们可以在软中断上下文执行的话——这一段接下来会说)。执行中断程序后,接下来重新对硬件进行编程,说白了有点类似于单片机的清除中断标志,当然这里还做了很多的事情,不光是清楚中断标志,还包括对硬件的重新编程和初始化,使得在下一个待决定时器到期时可以引发中断,然后将引发static void run_hrtimer_softirq(struct softirq_action *h)处理到期链表上所有定时器的中断函数。下面讨论一下相关的代码的特性和机制。首先考虑的是hrtimer_interrupt函数,下面贴上它的源代码(务必注意这里的代码版本是以Linux3.2.12为准):
void hrtimer_interrupt(struct clock_event_device *dev)
{
struct hrtimer_cpu_base *cpu_base = &__get_cpu_var(hrtimer_bases);
ktime_t expires_next, now, entry_time, delta;
int i, retries = 0;
BUG_ON(!cpu_base->hres_active);
cpu_base->nr_events++;
dev->next_event.tv64 = KTIME_MAX;
entry_time = now = ktime_get();
retry:
expires_next.tv64 = KTIME_MAX;
raw_spin_lock(&cpu_base->lock);
cpu_base->expires_next.tv64 = KTIME_MAX;
for (i = 0; i < HRTIMER_MAX_CLOCK_BASES; i++) {
struct hrtimer_clock_base *base;
struct timerqueue_node *node;
ktime_t basenow;
if (!(cpu_base->active_bases & (1 << i)))
continue;
base = cpu_base->clock_base + i;
basenow = ktime_add(now, base->offset);
while ((node = timerqueue_getnext(&base->active))) {
struct hrtimer *timer;
timer = container_of(node, struct hrtimer, node);
if (basenow.tv64 < hrtimer_get_softexpires_tv64(timer)) {
ktime_t expires;
expires = ktime_sub(hrtimer_get_expires(timer),
base->offset);
if (expires.tv64 < expires_next.tv64)
expires_next = expires;
break;
}
__run_hrtimer(timer, &basenow);
}
}
cpu_base->expires_next = expires_next;
raw_spin_unlock(&cpu_base->lock);
if (expires_next.tv64 == KTIME_MAX ||
!tick_program_event(expires_next, 0)) {
cpu_base->hang_detected = 0;
return;
}
now = ktime_get();
cpu_base->nr_retries++;
if (++retries < 3)
goto retry;
cpu_base->nr_hangs++;
cpu_base->hang_detected = 1;
delta = ktime_sub(now, entry_time);
if (delta.tv64 > cpu_base->max_hang_time.tv64)
cpu_base->max_hang_time = delta;
if (delta.tv64 > 100 * NSEC_PER_MSEC)
expires_next = ktime_add_ns(now, 100 * NSEC_PER_MSEC);
else
expires_next = ktime_add(now, delta);
tick_program_event(expires_next, 1);
printk_once(KERN_WARNING "hrtimer: interrupt took %llu ns\n",
ktime_to_ns(delta));
}
话说有点长啊……把它分成几部分来看,首先进行的是一些必要的初始化配置:
struct hrtimer_cpu_base *cpu_base = &__get_cpu_var(hrtimer_bases);
ktime_t expires_next, now, entry_time, delta;
int i, retries = 0;
BUG_ON(!cpu_base->hres_active);
cpu_base->nr_events++;
dev->next_event.tv64 = KTIME_MAX;
entry_time = now = ktime_get();
retry:
expires_next.tv64 = KTIME_MAX;
raw_spin_lock(&cpu_base->lock);
以上几行程序就是用来初始化的,接下来,其到期时间保存在expires_next中。最初将该变量设置为KTIME_MAX,是表明没有下一个定时器。函数的主要工作是遍历所有的时基(两种时钟都遍历),代码如下:
for (i = 0; i < HRTIMER_MAX_CLOCK_BASES; i++) {
struck hrtimer_clock_base *base;
struct timerqueue_node *node;
ktime_t basenow;
if (!(cpu_base->active_bases & (1 << i)))
continue;
base = cpu_base->clock_base + i;
basenow = ktime_add(now, base->offset);
上面那段代码没什么亮点吧,最后一句basenow就是表示当前时间,base->offset仅在已经重新调整了实时时钟的时候是非零,其他的时候不会影响到单调时钟基础。从base->first开始,即可以获得红黑树中到期的结点,
代码如下:
while ((node = timerqueue_getnext(&base->active))) {struct hrtimer *timer;
timer = container_of(node, struct hrtimer, node);
if (basenow.tv64 < hrtimer_get_softexpires_tv64(timer)) {
ktime_t expires;
expires = ktime_sub(hrtimer_get_expires(timer),
base->offset);
if (expires.tv64 < expires_next.tv64)
expires_next = expires;
break;
}
__run_hrtimer(timer, &basenow);
}
这里面涉及到另一个东西——红黑树,其实在之前我们已经说了,高精度定时器之所以精度高,就是因为定时器引入了二叉树算法,确切点说是红黑树算法,红黑树算饭的算法的源代码文件及位置如下:
Rbtree.c (linux-3.2.12\lib)
Rbtree.h (linux-3.2.12\include\linux)
Rbtree.txt (linux-3.2.12\documentation)
在这里不细细讲解红黑树,只是简单的提一下相关的性质吧:
l 每个结点或是红的或是黑的;
l 根节点是黑的;
l 每个叶子节点(NIL)是黑的;
l 如果一个结点是红的,则他的两个儿子都是黑的;
l 对于每个结点,从该结点到其子孙节点的结点的所有路径上包含相同数目的黑结点。
一个二叉查找树,如果符合上述特点,那么他就是红黑树。说的太玄乎了……其实引入红黑树的目的就是让计时更加精确而已,这里我们只关心以下几个结构体:
struct rb_root
{
struct rb_node *rb_node;
};
struct rb_node
{
unsigned long rb_parent_color;
#define RB_RED 0
#define RB_BLACK 1
struct rb_node *rb_right;
struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));
可能有人会诧异,就这俩?!我的答案是:嗯,就这俩。struct rb_root定义树的根,struct rb_node定义树的结点,然后再就是几个常用的API函数,这里就不细细说了,毕竟红黑树涉及的东西太多了。
高精度定时器之所以引入红黑树,就是为了更加精确,这个说起来可能很难理解,我们先说说二叉查找树,二叉查找树有个性质,左孩子的关键字≤父结点≤右孩子的关键字,这样的规定就大大加快了查找的效率,降低了算法的复杂度,同样的,在二叉查找树上再加上一些规定,可以将查找效率更大的提高,这样,可以更加精确的计时,这是高精度定时器引入红黑树的核心思想。
关于红黑树,就说这么些,继续说高精度定时器。
如果下一个定时器到期时间在未来,那么可以停止处理,离开while循环。但需要记住该到期时间,以便在稍后对时钟事件设备重新编程。
如果当前的定时器已经到期,那么在允许在软中断上下文执行处理程序的情况下,会将该定时器移动到回调链表。continue确保了处理代码转向下一个到期的候选定时器;如果不允许在软中断上下文执行定时器的处理程序,那么将直接在硬件中断上下文中执行定时器回调函数。
回调处理程序通过timer->(timer)来执行(原型在linux-3.2.12\include\linux\ Hrtimer.h中,注意这个大的for循环里面有struct hrtimer *timer;这一句)。如果处理函数返回HRTIMER_RESTART,请求重启定时器,那么通过enqueue_hrtimer来完成该请求。在处理程序已经执行后,可以清除HRTIMER_STATE_CALLBACK标志。在已经选择了所有时钟基础的待决定时器之后,内核需要对时钟事件设备重新编程,以便在下一个定时器到期时依法中断。
另外,我们前面说的,中断引发多了,不能同时处理的,就挂到连边上等待,处理完这个,再从链表上弄些一个来处理,直到链表为空。
最后还需要一个步骤,就是引发软中断执行待决定时器的回调函数,该软中断的处理程序是:
static void run_hrtimer_softirq(struct softirq_action *h)
{
hrtimer_peek_ahead_timers();
}
从本质上来看,该函数将遍历所有待决定的链表,hrtimer_peek_ahead_timers();代码如下:
void hrtimer_peek_ahead_timers(void)
{
unsigned long flags;
local_irq_save(flags);
__hrtimer_peek_ahead_timers();
local_irq_restore(flags);
}
在hrtimer_peek_ahead_timers()函数的注释里写的很清楚,英文翻译如下:
hrtimer_peek_ahead_timers——run soft-expired timers now现在运行软中断执行待决定时期
hrtimer_peek_ahead_timers会监视目前的cpu的定时器队列,检查是否有任何过期的软中断的定时器。如有此类定时器存在,他们立即运行,然后从定时器队列移除。
移除定时器的函数为remove_hrtimer(struct hrtimer *timer, struct hrtimer_clock_base *base)。
大致上,到此已经讲完了了高精度定时器在高精度模式下的整个使用过程
===================================================================================================================
内核的开发者考察了多种数据结构,例如基数树、哈希表等等,最终他们选择了红黑树(rbtree)来组织hrtimer,红黑树已经以库的形式存在于内核中,并被成功地使用在内存管理子系统和文件系统中,随着系统的运行,
hrtimer不停地被创建和销毁,新的hrtimer按顺序被插入到红黑树中,树的最左边的节点就是最快到期的定时器,内核用一个hrtimer结构来表示一个高精度定时器:
struct hrtimer {
struct timerqueue_node node;
ktime_t _softexpires;
enum hrtimer_restart (*function)(struct hrtimer *);
struct hrtimer_clock_base *base;
unsigned long state;
......
};
定时器的到期时间用ktime_t来表示,_softexpires字段记录了时间,定时器一旦到期,function字段指定的回调函数会被调用,该函数的返回值为一个枚举值,它决定了该hrtimer是否需要被重新激活:
enum hrtimer_restart {
HRTIMER_NORESTART, /* Timer is not restarted */
HRTIMER_RESTART, /* Timer must be restarted */
};
state字段用于表示hrtimer当前的状态,有几下几种位组合:
#define HRTIMER_STATE_INACTIVE 0x00 // 定时器未激活
#define HRTIMER_STATE_ENQUEUED 0x01 // 定时器已经被排入红黑树中
#define HRTIMER_STATE_CALLBACK 0x02 // 定时器的回调函数正在被调用
#define HRTIMER_STATE_MIGRATE 0x04 // 定时器正在CPU之间做迁移
hrtimer的到期时间可以基于以下几种时间基准系统:
enum hrtimer_base_type {
HRTIMER_BASE_MONOTONIC, // 单调递增的monotonic时间,不包含休眠时间
HRTIMER_BASE_REALTIME, // 平常使用的墙上真实时间
HRTIMER_BASE_BOOTTIME, // 单调递增的boottime,包含休眠时间
HRTIMER_MAX_CLOCK_BASES, // 用于后续数组的定义
};
和低分辨率定时器一样,处于效率和上锁的考虑,每个cpu单独管理属于自己的hrtimer,为此,专门定义了一个结构hrtimer_cpu_base:
struct hrtimer_cpu_base {
......
struct hrtimer_clock_base clock_base[HRTIMER_MAX_CLOCK_BASES];
};
其中,clock_base数组为每种时间基准系统都定义了一个hrtimer_clock_base结构,它的定义如下:
struct hrtimer_clock_base {
struct hrtimer_cpu_base *cpu_base; // 指向所属cpu的hrtimer_cpu_base结构
......
struct timerqueue_head active; // 红黑树,包含了所有使用该时间基准系统的hrtimer
ktime_t resolution; // 时间基准系统的分辨率
ktime_t (*get_time)(void); // 获取该基准系统的时间函数
ktime_t softirq_time;// 当用jiffies
ktime_t offset; //
};
active字段是一个timerqueue_head结构,它实际上是对rbtree的进一步封装:
struct timerqueue_node {
struct rb_node node; // 红黑树的节点
ktime_t expires; // 该节点代表队hrtimer的到期时间,与hrtimer结构中的_softexpires稍有不同
};
struct timerqueue_head {
struct rb_root head; // 红黑树的根节点
struct timerqueue_node *next; // 该红黑树中最早到期的节点,也就是最左下的节点
};
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进行唤醒。
===================================================================================================================
hrtimer如何运转
hrtimer的实现需要一定的硬件基础,它的实现依赖于我们前几章介绍的timekeeper和clock_event_device,如果你对timekeeper和clock_event_device不了解请参考以下文章:Linux时间子系统之三:时间的维护者:
timekeeper,Linux时间子系统之四:定时器的引擎: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函数对它进行初始化,它的原型如下:
void hrtimer_init(struct hrtimer *timer, clockid_t which_clock, enum hrtimer_mode mode);
which_clock可以是CLOCK_REALTIME、CLOCK_MONOTONIC、CLOCK_BOOTTIME中的一种,mode则可以是相对时间HRTIMER_MODE_REL,也可以是绝对时间HRTIMER_MODE_ABS。设定回调函数:
timer.function = hr_callback;
如果定时器无需指定一个到期范围,可以在设定回调函数后直接使用hrtimer_start激活该定时器:
int hrtimer_start(struct hrtimer *timer, ktime_t tim, const enum hrtimer_mode mode);
如果需要指定到期范围,则可以使用hrtimer_start_range_ns激活定时器:
hrtimer_start_range_ns(struct hrtimer *timer, ktime_t tim, unsigned long range_ns, const enum hrtimer_mode mode);
要取消一个hrtimer,使用hrtimer_cancel:
int hrtimer_cancel(struct hrtimer *timer);
extern u64
hrtimer_forward(struct hrtimer *timer, ktime_t now, ktime_t interval);
/* Forward a hrtimer so it expires after the hrtimer's current now */
static inline u64 hrtimer_forward_now(struct hrtimer *timer,
ktime_t interval)
{
return hrtimer_forward(timer, timer->base->get_time(), interval);
}
以下几个函数用于 获取定时器的当前状态:
static inline int hrtimer_active(const struct hrtimer *timer)
{
return timer->state != HRTIMER_STATE_INACTIVE;
}
static inline int hrtimer_is_queued(struct hrtimer *timer)
{
return timer->state & HRTIMER_STATE_ENQUEUED;
}
static inline int hrtimer_callback_running(struct hrtimer *timer)
{
return timer->state & HRTIMER_STATE_CALLBACK;
}
hrtimer_init最终会进入__hrtimer_init函数,该函数的主要目的是初始化hrtimer的base字段,同时初始化作为红黑树的节点的node字段:
static void __hrtimer_init(struct hrtimer *timer, clockid_t clock_id,
enum hrtimer_mode mode)
{
struct hrtimer_cpu_base *cpu_base;
int base;
memset(timer, 0, sizeof(struct hrtimer));
cpu_base = &__raw_get_cpu_var(hrtimer_bases);
if (clock_id == CLOCK_REALTIME && mode != HRTIMER_MODE_ABS)
clock_id = CLOCK_MONOTONIC;
base = hrtimer_clockid_to_base(clock_id);
timer->base = &cpu_base->clock_base[base];
timerqueue_init(&timer->node);
......
}
hrtimer_start和hrtimer_start_range_ns最终会把实际的工作交由__hrtimer_start_range_ns来完成:
int __hrtimer_start_range_ns(struct hrtimer *timer, ktime_t tim,
unsigned long delta_ns, const enum hrtimer_mode mode,
int wakeup)
{
......
/* 取得hrtimer_clock_base指针 */
base = lock_hrtimer_base(timer, &flags);
/* 如果已经在红黑树中,先移除它: */
ret = remove_hrtimer(timer, base); ......
/* 如果是相对时间,则需要加上当前时间,因为内部是使用绝对时间 */
if (mode & HRTIMER_MODE_REL) {
tim = ktime_add_safe(tim, new_base->get_time());
......
}
/* 设置到期的时间范围 */
hrtimer_set_expires_range_ns(timer, tim, delta_ns);
......
/* 把hrtime按到期时间排序,加入到对应时间基准系统的红黑树中 */
/* 如果该定时器的是最早到期的,将会返回true */
leftmost = enqueue_hrtimer(timer, new_base);
/*
* Only allow reprogramming if the new base is on this CPU.
* (it might still be on another CPU if the timer was pending)
*
* XXX send_remote_softirq() ?
* 定时器比之前的到期时间要早,所以需要重新对tick_device进行编程,重新设定的的到期时间
*/
if (leftmost && new_base->cpu_base == &__get_cpu_var(hrtimer_bases))
hrtimer_enqueue_reprogram(timer, new_base, wakeup);
unlock_hrtimer_base(timer, &flags);
return ret;
}
2.2 hrtimer的到期处理
高精度定时器系统有3个入口可以对到期定时器进行处理,它们分别是:
没有切换到高精度模式时,在每个jiffie的tick事件中断中进行查询和处理;
在HRTIMER_SOFTIRQ软中断中进行查询和处理;
切换到高精度模式后,在每个clock_event_device的到期事件中断中进行查询和处理;
低精度模式 因为系统并不是一开始就会支持高精度模式,而是在系统启动后的某个阶段,等待所有的条件都满足后,才会切换到高精度模式,当系统还没有切换到高精度模式时,所有的高精度定时器运行在低精度模式下,
在每个jiffie的tick事件中断中进行到期定时器的查询和处理,显然这时候的精度和低分辨率定时器是一样的(HZ级别)。
低精度模式下,每个tick事件中断中,hrtimer_run_queues函数会被调用,由它完成定时器的到期处理。
hrtimer_run_queues首先判断目前高精度模式是否已经启用,如果已经切换到了高精度模式,什么也不做,直接返回:
void hrtimer_run_queues(void)
{
if (hrtimer_hres_active())
return;
如果hrtimer_hres_active返回false,说明目前处于低精度模式下,则继续处理,它用一个for循环遍历各个时间基准系统,查询每个hrtimer_clock_base对应红黑树的左下节点,判断它的时间是否到期,如果到期,通过
__run_hrtimer函数,对到期定时器进行处理,包括:调用定时器的回调函数、从红黑树中移除该定时器、根据回调函数的返回值决定是否重新启动该定时器等等:
for (index = 0; index < HRTIMER_MAX_CLOCK_BASES; index++) {
base = &cpu_base->clock_base[index];
if (!timerqueue_getnext(&base->active))
continue;
if (gettime) {
hrtimer_get_softirq_time(cpu_base);
gettime = 0;
}
raw_spin_lock(&cpu_base->lock);
while ((node = timerqueue_getnext(&base->active))) {
struct hrtimer *timer;
timer = container_of(node, struct hrtimer, node);
if (base->softirq_time.tv64 <=
hrtimer_get_expires_tv64(timer))
break;
__run_hrtimer(timer, &base->softirq_time);
}
raw_spin_unlock(&cpu_base->lock);
}
上面的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中:
void hrtimer_interrupt(struct clock_event_device *dev)
{
......
for (i = 0; i < HRTIMER_MAX_CLOCK_BASES; i++) {
......
while ((node = timerqueue_getnext(&base->active))) {
......
if (basenow.tv64 < hrtimer_get_softexpires_tv64(timer)) {
ktime_t expires;
expires = ktime_sub(hrtimer_get_expires(timer),
base->offset);
if (expires.tv64 < expires_next.tv64)
expires_next = expires;
break;
}
__run_hrtimer(timer, &basenow);
}
}
/*
* Store the new expiry value so the migration code can verify
* against it.
*/
cpu_base->expires_next = expires_next;
raw_spin_unlock(&cpu_base->lock);
/* Reprogramming necessary ? */
if (expires_next.tv64 == KTIME_MAX ||
!tick_program_event(expires_next, 0)) {
cpu_base->hang_detected = 0;
return;
}
如果这时的tick_program_event返回了非0值,表示过期时间已经在当前时间的前面,这通常由以下原因造成:
系统正在被调试跟踪,导致时间在走,程序不走;
定时器的回调函数花了太长的时间;
系统运行在虚拟机中,而虚拟机被调度导致停止运行;
为了避免这些情况的发生,接下来系统提供3次机会,重新执行前面的循环,处理到期的定时器:
raw_spin_lock(&cpu_base->lock);
now = hrtimer_update_base(cpu_base);
cpu_base->nr_retries++;
if (++retries < 3)
goto retry;
如果3次循环后还无法完成到期处理,系统不再循环,转为计算本次总循环的时间,然后把tick_device的到期时间强制设置为当前时间加上本次的总循环时间,不过推后的时间被限制在100ms以内:
delta = ktime_sub(now, entry_time);
if (delta.tv64 > cpu_base->max_hang_time.tv64)
cpu_base->max_hang_time = delta;
/*
* Limit it to a sensible value as we enforce a longer
* delay. Give the CPU at least 100ms to catch up.
*/
if (delta.tv64 > 100 * NSEC_PER_MSEC)
expires_next = ktime_add_ns(now, 100 * NSEC_PER_MSEC);
else
expires_next = ktime_add(now, delta);
tick_program_event(expires_next, 1);
printk_once(KERN_WARNING "hrtimer: interrupt took %llu ns\n",
ktime_to_ns(delta));
}
3. 切换到高精度模式
上面提到,尽管内核配置成支持高精度定时器,但并不是一开始就工作于高精度模式,系统在启动的开始阶段,还是按照传统的模式在运行:tick_device按HZ频率定期地产生tick事件,这时的hrtimer工作在低分辨率模式,
到期事件在每个tick事件中断中由hrtimer_run_queues函数处理,同时,在低分辨率定时器(时间轮)的软件中断TIMER_SOFTIRQ中,hrtimer_run_pending会被调用,系统在这个函数中判断系统的条件是否满足切换到高精度模式,如果条件满足,则会切换至高分辨率模式,另外提一下,NO_HZ模式也是在该函数中判断并切换。
void hrtimer_run_pending(void)
{
if (hrtimer_hres_active())
return;
......
if (tick_check_oneshot_change(!hrtimer_is_hres_enabled()))
hrtimer_switch_to_hres();
}
因为不管系统是否工作于高精度模式,每个TIMER_SOFTIRQ期间,该函数都会被调用,所以函数一开始先用hrtimer_hres_active判断目前高精度模式是否已经激活,如果已经激活,则说明之前的调用中已经切换了工作模式,
不必再次切换,直接返回。hrtimer_hres_active很简单:
DEFINE_PER_CPU(struct hrtimer_cpu_base, hrtimer_bases) = {
......
}
static inline int hrtimer_hres_active(void)
{
return __this_cpu_read(hrtimer_bases.hres_active);
}
hrtimer_run_pending函数接着通过tick_check_oneshot_change判断系统是否可以切换到高精度模式,
int tick_check_oneshot_change(int allow_nohz)
{
struct tick_sched *ts = &__get_cpu_var(tick_cpu_sched);
if (!test_and_clear_bit(0, &ts->check_clocks))
return 0;
if (ts->nohz_mode != NOHZ_MODE_INACTIVE)
return 0;
if (!timekeeping_valid_for_hres() || !tick_is_oneshot_available())
return 0;
if (!allow_nohz)
return 1;
tick_nohz_switch_to_nohz();
return 0;
}
函数的一开始先判断check_clock标志的第0位是否被置位,如果没有置位,说明系统中没有注册符合要求的时钟事件设备,函数直接返回,check_clock标志由clocksource和clock_event_device系统的notify系统置位,当系统中有更高精度的clocksource被注册和选择后,或者有更精确的支持CLOCK_EVT_MODE_ONESHOT模式的clock_event_device被注册时,通过它们的notify函数,check_clock标志的第0为会置位。
如果tick_sched结构中的nohz_mode字段不是NOHZ_MODE_INACTIVE,表明系统已经切换到其它模式,直接返回。nohz_mode的取值有3种:
NOHZ_MODE_INACTIVE // 未启用NO_HZ模式
NOHZ_MODE_LOWRES // 启用NO_HZ模式,hrtimer工作于低精度模式下
NOHZ_MODE_HIGHRES // 启用NO_HZ模式,hrtimer工作于高精度模式下
接下来的timerkeeping_valid_for_hres判断timekeeper系统是否支持高精度模式,tick_is_oneshot_available判断tick_device是否支持CLOCK_EVT_MODE_ONESHOT模式。如果都满足要求,则继续往下判断。allow_nohz是函数的参数,为true表明可以切换到NOHZ_MODE_LOWRES 模式,函数将进入tick_nohz_switch_to_nohz,切换至NOHZ_MODE_LOWRES 模式,这里我们传入的allow_nohz是表达式:
(!hrtimer_is_hres_enabled())
所以当系统不允许高精度模式时,将会在tick_check_oneshot_change函数内,通过tick_nohz_switch_to_nohz切换至NOHZ_MODE_LOWRES 模式,如果系统允许高精度模式,传入的allow_nohz参数为false,tick_check_oneshot_change函数返回1,回到上面的hrtimer_run_pending函数,hrtimer_switch_to_hres函数将会被调用,已完成切换到NOHZ_MODE_HIGHRES高精度模式。好啦,真正的切换函数找到了,我们看一看它如何切换:
首先,它通过hrtimer_cpu_base中的hres_active字段判断该cpu是否已经切换至高精度模式,如果是则直接返回:
static int hrtimer_switch_to_hres(void)
{
int i, cpu = smp_processor_id();
struct hrtimer_cpu_base *base = &per_cpu(hrtimer_bases, cpu);
unsigned long flags;
if (base->hres_active)
return 1;
接着,通过tick_init_highres函数接管tick_device关联的clock_event_device:
local_irq_save(flags);
if (tick_init_highres()) {
local_irq_restore(flags);
printk(KERN_WARNING "Could not switch to high resolution "
"mode on CPU %d\n", cpu);
return 0;
}
tick_init_highres函数把tick_device切换到CLOCK_EVT_FEAT_ONESHOT模式,同时把clock_event_device的回调handler设置为hrtimer_interrupt,这样设置以后,tick_device的中断回调将由hrtimer_interrupt接管,
hrtimer_interrupt在上面已经讨论过,它将完成高精度定时器的调度和到期处理。
接着,设置hres_active标志,以表明高精度模式已经切换,然后把3个时间基准系统的resolution字段设为KTIME_HIGH_RES:
base->hres_active = 1;
for (i = 0; i < HRTIMER_MAX_CLOCK_BASES; i++)
base->clock_base[i].resolution = KTIME_HIGH_RES;
最后,因为tick_device被高精度定时器接管,它将不会再提供原有的tick事件机制,所以需要由高精度定时器系统模拟一个tick事件设备,继续为系统提供tick事件能力,这个工作由tick_setup_sched_timer函数完成。因为
刚刚完成切换,tick_device的到期时间并没有被正确地设置为下一个到期定时器的时间,这里使用retrigger_next_event函数,传入参数NULL,使得tick_device立刻产生到期中断,hrtimer_interrupt被调用一次,然后下一
个到期的定时器的时间会编程到tick_device中,从而完成了到高精度模式的切换:
tick_setup_sched_timer();
/* "Retrigger" the interrupt to get things going */
retrigger_next_event(NULL);
local_irq_restore(flags);
return 1;
===============================================================================================================
1.hrtimers - 为高分辨率kernel定时器,可作为超时或周期性定时器使用
1). hrtimer_init初始化定时器工作模式。
hrtimer_init(&vibe_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
vibe_timer.function = vibrator_timer_func;
/* 设置定时器的回调函数,定时器到时该函数将被调用 */
static enum hrtimer_restart vibrator_timer_func(struct hrtimer *timer)
注:该回调函数为原子操作不能被中断
2). hrtimer_start的第二个参数用于设置超时参数。
hrtimer_start(&vibe_timer,
ktime_set(value / 1000, (value % 1000) * 1000000),HRTIMER_MODE_REL);
3). INIT_WORK初始化工作队列。
INIT_WORK(&vibe_work, vibe_work_func);
static void vibe_work_func(struct work_struct *work)
4). schedule_work调用工作队列。
schedule_work(&vibe_work)
hrtimer高精度定时器的interval由ktime_set(const long secs, const unsigned long nsecs)决定,可做到ns级