一、hrtimer 概述
在Linux内核中已经存在了一个管理定时器的通用框架。不过它也有很多不足,最大的问题是其精度不是很高。哪怕底层的定时事件设备精度再高,定时器层的分辨率只能达到Tick级别,按照内核配置选项的不同,在100Hz到1000Hz之间。但是,原有的定时器层由于实现教早,应用广泛,如果完全替换掉会引入大量代码改动。因此,Linux内核又独立设计出了一个叫高精度定时器层(High Resolution Timer)的框架,可以为我们提供纳秒级的定时精度,以满足对精确时间有迫切需求的应用程序或内核驱动程序。
高分辨率定时器是建立在每CPU私有独占的本地时钟事件设备上的,对于一个多处理器系统,如果只有全局的时钟事件设备,高分辨率定时器是无法工作的。因为如果没有每CPU私有独占的时钟事件设备,当到期中断发生时系统必须产生夸处理器中断来通知其它CPU完成相应的工作,而过多的夸处理器中断会带来很大的系统开销,这样会令使用高分辨率定时器的代价大大增加,还不如不用。为了让内核支持高分辨率定时器,必须要在编译的时候打开编译选项CONFIG_HIGH_RES_TIMERS。
高分辨率定时器层有两种工作模式:低精度模式与高精度模式。虽然高分辨率定时器子系统是为高精度定时器准备的,但是系统可能在运行过程中动态切换到不同精度和模式的定时事件设备,因此高精度定时器层必须能够在低精度模式与高精度模式下自由切换。
高分辨率定时器层使用红黑树来组织各个高分辨率定时器。随着系统的运行,高分辨率定时器不停地被创建和销毁,新的高分辨率定时器按顺序被插入到红黑树中,树的最左边的节点就是最快到期的定时器。
二、相关数据结构
2.1 高分辨率定时器由hrtimer结构体表示(代码位于include/linux/hrtimer.h中):
struct hrtimer {struct timerqueue_node node;ktime_t _softexpires;enum hrtimer_restart (*function)(struct hrtimer *);struct hrtimer_clock_base *base;u8 state;u8 is_rel;u8 is_soft;u8 is_hard; };
node:是一个timerqueue_node结构体变量。这个结构体中有两个成员,node是红黑树的节点,expires:表示该定时器的硬超时时间:
struct timerqueue_node {struct rb_node node;ktime_t expires;
};
_softexpires:表示该定时器的软超时时间。高精度定时器一般都有一个到期的时间范围,而不像(低精度)定时器那样就是一个时间点。这个时间范围的前时间点就是软超时时间,而后一个时间点就是硬超时时间。达到软超时时间后,还可以再拖一会再调用超时回调函数,而到达硬超时时间后就不能再拖了。
function:定时器到期后的回调函数。
base:指向包含该高分辨率定时器的的hrtimer_clock_base结构体。
state:用来表示该高分辨率定时器当前所处的状态,目前共有两种状态:
/* 表示定时器还未激活 */
#define HRTIMER_STATE_INACTIVE 0x00
/* 表示定时器已激活(入列) */
#define HRTIMER_STATE_ENQUEUED 0x01
is_rel:表示该定时器的到期时间是否是相对时间。
is_soft:表示该定时器是否是“软”定时器。
is_hard:表示该定时器是否是“硬”定时器。
2.2 hrtimer_clock_base
struct hrtimer_clock_base {struct hrtimer_cpu_base *cpu_base;unsigned int index;clockid_t clockid;seqcount_t seq;struct hrtimer *running;struct timerqueue_head active;ktime_t (*get_time)(void);ktime_t offset;
} __hrtimer_clock_base_align;
cpu_base:指向所属CPU的hrtimer_cpu_base结构体。
index:表示该结构体在当前CPU的hrtimer_cpu_base结构体中clock_base数组中所处的下标。
clockid:表示当前时钟类型的ID值。
seq:顺序锁,在处理到期定时器的函数__run_hrtimer中会用到。
running:指向当前正在处理的那个定时器。
active:红黑树,包含了所有使用该时间类型的定时器。
get_time:是一个函数指针,指定了如何获取该时间类型的当前时间的函数。由于不同类型的时间在Linux中都是由时间维护层来统一管理的,因此这些函数都是在时间维护层里面定义好的。
offset:表示当前时间类型和单调时间之间的差值。
2.3 hrtimer_cpu_base
每个CPU单独管理属于自己的高分辨率定时器,为了方便管理,专门定义了一个结构体hrtimer_cpu_base:
struct hrtimer_cpu_base {raw_spinlock_t lock;unsigned int cpu;unsigned int active_bases;unsigned int clock_was_set_seq;unsigned int hres_active : 1,in_hrtirq : 1,hang_detected : 1,softirq_activated : 1;
#ifdef CONFIG_HIGH_RES_TIMERSunsigned int nr_events;unsigned short nr_retries;unsigned short nr_hangs;unsigned int max_hang_time;
#endif
#ifdef CONFIG_PREEMPT_RT......
#endifktime_t expires_next;struct hrtimer *next_timer;ktime_t softirq_expires_next;struct hrtimer *softirq_next_timer;struct hrtimer_clock_base clock_base[HRTIMER_MAX_CLOCK_BASES];
} ____cacheline_aligned;
lock:用来保护该结构体的自旋锁。
cpu:绑定到的CPU编号。
active_bases:表示clock_base数组中哪些元素下的红黑树中含有定时器。
clock_was_set_seq:表示时钟被设置的序数。
hres_active:表示是否已经处在了高精度模式下。
in_hrtirq:是否正在执行hrtimer_interrupt中断处理程序中。
hang_detected:表明在前一次执行hrtimer_interrupt中断处理程序的时候发生了错误。
softirq_activated:是否正在执行hrtimer_run_softirq软中断处理程序。
nr_events:表明一共执行了多少次hrtimer_interrupt中断处理程序。
nr_retries:表明在执行hrtimer_interrupt中断处理程序的时候对定时事件设备编程错误后重试的次数。
nr_hangs:表明在执行hrtimer_interrupt中断处理程序的时候发生错误的次数。
max_hang_time:表明在碰到错误后,在hrtimer_interrupt中断处理程序中停留的最长时间。
expires_next:该CPU上即将要到期定时器的到期时间。
next_timer:该CPU上即将要到期的定时器。
softirq_expires_next:该CPU上即将要到期的“软”定时器的到期时间。
softirq_next_timer:该CPU上即将要到期的“软”定时器。
clock_base:高分辨率定时器的到期时间可以基于以下几种时间类型,clock_base数组为每种时间基准系统都定义了一个hrtimer_clock_base结构体:
enum hrtimer_base_type {HRTIMER_BASE_MONOTONIC,HRTIMER_BASE_REALTIME,HRTIMER_BASE_BOOTTIME,HRTIMER_BASE_TAI,HRTIMER_BASE_MONOTONIC_SOFT,HRTIMER_BASE_REALTIME_SOFT,HRTIMER_BASE_BOOTTIME_SOFT,HRTIMER_BASE_TAI_SOFT,HRTIMER_MAX_CLOCK_BASES,
};
没有加 SOFT 后缀的,表示是“硬”定时器,将直接在中断处理程序中处理;而加了SOFT后缀的,表示是“软”定时器,将在软中断(HRTIMER_SOFTIRQ)中处理。而且前面的一半都定义成“硬”定时器类型,后面一半都定义成“软”定时器类型,硬的一半和软的一半申明的次序也是对应的。这样设计就方便根据“硬”“软”特性和时间类型很快查出对应 hrtimer_clock_base 结构体的下标。
所以,综上所述,高分辨率定时器层的组织相对来说还是比较简单的,甚至比(低分辨率)定时器层还要简单。每个CPU对应有一个 hrtimer_cpu_base 的 Per CPU 结构体变量,其定义如下:
DEFINE_PER_CPU(struct hrtimer_cpu_base, hrtimer_bases) =
{.lock = __RAW_SPIN_LOCK_UNLOCKED(hrtimer_bases.lock),.clock_base ={{.index = HRTIMER_BASE_MONOTONIC,.clockid = CLOCK_MONOTONIC,.get_time = &ktime_get,},{.index = HRTIMER_BASE_REALTIME,.clockid = CLOCK_REALTIME,.get_time = &ktime_get_real,},{.index = HRTIMER_BASE_BOOTTIME,.clockid = CLOCK_BOOTTIME,.get_time = &ktime_get_boottime,},{.index = HRTIMER_BASE_TAI,.clockid = CLOCK_TAI,.get_time = &ktime_get_clocktai,},{.index = HRTIMER_BASE_MONOTONIC_SOFT,.clockid = CLOCK_MONOTONIC,.get_time = &ktime_get,},{.index = HRTIMER_BASE_REALTIME_SOFT,.clockid = CLOCK_REALTIME,.get_time = &ktime_get_real,},{.index = HRTIMER_BASE_BOOTTIME_SOFT,.clockid = CLOCK_BOOTTIME,.get_time = &ktime_get_boottime,},{.index = HRTIMER_BASE_TAI_SOFT,.clockid = CLOCK_TAI,.get_time = &ktime_get_clocktai,},}
};
每个 hrtimer_cpu_base 结构体中有一个 hrtimer_clock_base 类型的数组变量 clock_base,目前数组元素是8个,分别用来存放8种到期时间类型的高分辨率定时器。而每种到期时间类型下,又是以红黑数来组织所有的高分辨率定时器。因此,高分辨率定时器层的数据结构如下图所示:
三、高精度定时器相关API
3.1 高精度定时器层初始化 hrtimers_init
在linux内核启动初始化阶段,start_kernel 会调用 hrtimers_init 函数对高精度定时器层初始化:
void __init hrtimers_init(void)
{/* 初始化属于当前CPU的hrtimer_cpu_base结构体 */hrtimers_prepare_cpu(smp_processor_id());/* 打开HRTIMER_SOFTIRQ软中断 */open_softirq(HRTIMER_SOFTIRQ, hrtimer_run_softirq);
}
该函数主要功能就是初始化属于当前 CPU 的 hrtimer_cpu_base 结构体,然后打开HRTIMER_SOFTIRQ软中断。
int hrtimers_prepare_cpu(unsigned int cpu)
{/* 获得属于当前CPU的hrtimer_cpu_base结构体 */struct hrtimer_cpu_base *cpu_base = &per_cpu(hrtimer_bases, cpu);int i;/* 初始化所有的hrtimer_clock_base结构体 */for (i = 0; i < HRTIMER_MAX_CLOCK_BASES; i++) {/* 初始化cpu_base */cpu_base->clock_base[i].cpu_base = cpu_base;/* 初始化红黑树 */timerqueue_init_head(&cpu_base->clock_base[i].active);}cpu_base->cpu = cpu;cpu_base->active_bases = 0;cpu_base->hres_active = 0;cpu_base->hang_detected = 0;cpu_base->next_timer = NULL;cpu_base->softirq_next_timer = NULL;cpu_base->expires_next = KTIME_MAX;cpu_base->softirq_expires_next = KTIME_MAX;hrtimer_cpu_base_init_expiry_lock(cpu_base);return 0;
}
从上面的内核源码中,可以看出在内核初始化时,hrtimers_prepare_cpu 并没有对所有hrtimer_bases 初始化,而仅仅是对hrtimer_bases[0] 的8种hrtimer_clock_base 进行了初始化,红黑树初始化。那么有一个问题,其他CPU 的struct hrtimer_cpu_base 对象,什么时候开始初始化???
留一个问题:那么有一个问题,其他CPU 的struct hrtimer_cpu_base 对象,什么时候开始初始化??? 目前还没有找到源码中,是如何处理的;
3.2 定时器初始化 hrtimer_init
在将一个高分辨率定时器插入并激活之前,首先需要调用hrtimer_init函数对其进行初始化:
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);
}
EXPORT_SYMBOL_GPL(hrtimer_init);
clock_id:
<include/uapi/linux/time.h>
/** The IDs of the various system clocks (for POSIX.1b interval timers):*/
/* REALTIME记录的是从1970.1.1到当前的时间,常说的wall time(墙上时钟) */
#define CLOCK_REALTIME 0
/* MONOTIC记录的是从开机到当前的时间,suspend时不会增加 */
#define CLOCK_MONOTONIC 1
#define CLOCK_PROCESS_CPUTIME_ID 2
#define CLOCK_THREAD_CPUTIME_ID 3
#define CLOCK_MONOTONIC_RAW 4
#define CLOCK_REALTIME_COARSE 5
#define CLOCK_MONOTONIC_COARSE 6
/* BOOTTIME记录的是从开机到当前的时间,suspend时依然会增加 */
#define CLOCK_BOOTTIME 7
#define CLOCK_REALTIME_ALARM 8
#define CLOCK_BOOTTIME_ALARM 9
其直接调用了__hrtimer_init函数:
static void __hrtimer_init(struct hrtimer *timer, clockid_t clock_id,enum hrtimer_mode mode)
{bool softtimer = !!(mode & HRTIMER_MODE_SOFT);struct hrtimer_cpu_base *cpu_base;int base;if (IS_ENABLED(CONFIG_PREEMPT_RT) && !(mode & HRTIMER_MODE_HARD))softtimer = true;/* 将hrtimer结构体清0 */memset(timer, 0, sizeof(struct hrtimer));/* 获得当前CPU的hrtimer_cpu_base结构体变量 */cpu_base = raw_cpu_ptr(&hrtimer_bases);/* 相对的实时时间调整为单调时间 */if (clock_id == CLOCK_REALTIME && mode & HRTIMER_MODE_REL)clock_id = CLOCK_MONOTONIC;/* 根据定时器是不是“软”的找到hrtimer_clock_base的起始下标 */base = softtimer ? HRTIMER_MAX_CLOCK_BASES / 2 : 0;/* 通过clock_id找hrtimer_clock_base的偏移下标 */base += hrtimer_clockid_to_base(clock_id);timer->is_soft = softtimer;timer->is_hard = !softtimer;timer->base = &cpu_base->clock_base[base];/* 初始化定时器的红黑树节点结构体 */timerqueue_init(&timer->node);
}
所以,可以看出任何一个高分辨率定时器在初始化的时候都是默认被分配到当前处理器上的。
一个定时器驱动,在注册的时候,其实只是绑定在一个CPU 上,并不是所有的CPU 上都有。
对于 clock_base 下标的计算,由于“硬”定时器模式和“软”定时器模式各占一半,上下对称。所以,如果是“软”的,那起始下标就是最大值的一半,否则就是0。然后,在根据 clock_id 找到对应模式的偏移下标,两者相加就可以了。
static inline int hrtimer_clockid_to_base(clockid_t clock_id)
{if (likely(clock_id < MAX_CLOCKS)) {/* 将clock_id转换成下标 */int base = hrtimer_clock_to_base_table[clock_id];if (likely(base != HRTIMER_MAX_CLOCK_BASES))return base;}WARN(1, "Invalid clockid %d. Using MONOTONIC\n", clock_id);/* 如果出错默认选择单调时间 */return HRTIMER_BASE_MONOTONIC;
}
static const int hrtimer_clock_to_base_table[MAX_CLOCKS] = {[0 ... MAX_CLOCKS - 1] = HRTIMER_MAX_CLOCK_BASES,[CLOCK_REALTIME] = HRTIMER_BASE_REALTIME,[CLOCK_MONOTONIC] = HRTIMER_BASE_MONOTONIC,[CLOCK_BOOTTIME] = HRTIMER_BASE_BOOTTIME,[CLOCK_TAI] = HRTIMER_BASE_TAI,
};
数组下标是 clock_id,而数组的值是要返回的高分辨率定时器的时间类型。由于实际有意义的只有4项,因此多出来的项全部填上 HRTIMER_MAX_CLOCK_BASES,表示出错了。
高分辨率定时器共有以下几种模式:
enum hrtimer_mode {HRTIMER_MODE_ABS = 0x00,HRTIMER_MODE_REL = 0x01,HRTIMER_MODE_PINNED = 0x02,HRTIMER_MODE_SOFT = 0x04,HRTIMER_MODE_HARD = 0x08,HRTIMER_MODE_ABS_PINNED = HRTIMER_MODE_ABS | HRTIMER_MODE_PINNED,HRTIMER_MODE_REL_PINNED = HRTIMER_MODE_REL | HRTIMER_MODE_PINNED,HRTIMER_MODE_ABS_SOFT = HRTIMER_MODE_ABS | HRTIMER_MODE_SOFT,HRTIMER_MODE_REL_SOFT = HRTIMER_MODE_REL | HRTIMER_MODE_SOFT,HRTIMER_MODE_ABS_PINNED_SOFT = HRTIMER_MODE_ABS_PINNED | HRTIMER_MODE_SOFT,HRTIMER_MODE_REL_PINNED_SOFT = HRTIMER_MODE_REL_PINNED | HRTIMER_MODE_SOFT,HRTIMER_MODE_ABS_HARD = HRTIMER_MODE_ABS | HRTIMER_MODE_HARD,HRTIMER_MODE_REL_HARD = HRTIMER_MODE_REL | HRTIMER_MODE_HARD,HRTIMER_MODE_ABS_PINNED_HARD = HRTIMER_MODE_ABS_PINNED | HRTIMER_MODE_HARD,HRTIMER_MODE_REL_PINNED_HARD = HRTIMER_MODE_REL_PINNED | HRTIMER_MODE_HARD,
};
共有五种基本模式,其它模式都是由这五种组合而成:
- HRTIMER_MODE_ABS:表示定时器到期时间是一个绝对值。
- HRTIMER_MODE_REL:表示定时器到期时间是一个相对于当前时间之后的值。
- HRTIMER_MODE_PINNED:表示定时器是否需要绑定到某个CPU上。
- HRTIMER_MODE_SOFT:表示该定时器是否是“软”的,也就是定时器到期回调函数是在软中断下被执行的。
- HRTIMER_MODE_HARD:表示该定时器是否是“硬”的,也就是定时器到期回调函数是在中断处理程序中被执行的。
3.3 定时器移除 remove_hrtimer
将一个高分辨率定时器从系统中移除是通过 remove_hrtimer 函数实现的:
static inline int
remove_hrtimer(struct hrtimer *timer, struct hrtimer_clock_base *base, bool restart)
{u8 state = timer->state;/* 如果定时器没有激活则直接返回 */if (state & HRTIMER_STATE_ENQUEUED) {int reprogram;debug_deactivate(timer);/* 只有要删除的定时器激活在当前处理器上时才需要重编程 */reprogram = base->cpu_base == this_cpu_ptr(&hrtimer_bases);/* 如果要删除的定时器不需要重新再激活则将其状态改为HRTIMER_STATE_INACTIVE */if (!restart)state = HRTIMER_STATE_INACTIVE;__remove_hrtimer(timer, base, state, reprogram);return 1;}return 0;
}
参数 base 指向的就是那个包含要删除定时器的 hrtimer_clock_base 结构体。参数 restart 表示该定时器删除后是不是还会马上被激活,如果是的话就没必要修改状态为HRTIMER_STATE_INACTIVE(未激活)了。该函数判断一些状态后,主要是调用__remove_hrtimer函数进行删除:
static void __remove_hrtimer(struct hrtimer *timer,struct hrtimer_clock_base *base,u8 newstate, int reprogram)
{struct hrtimer_cpu_base *cpu_base = base->cpu_base;u8 state = timer->state;/* 修改当前定时器的状态为参数指定的状态 */WRITE_ONCE(timer->state, newstate);/* 如果该定时器还没有被添加到任何红黑树中则直接返回 */if (!(state & HRTIMER_STATE_ENQUEUED))return;/* 将要删除的定时器从红黑树中移除 */if (!timerqueue_del(&base->active, &timer->node))/* 如果删除后红黑树为空则清除active_bases中对应的位 */cpu_base->active_bases &= ~(1 << base->index);/* 如果需要重编程并且要删除的定时器是其激活CPU上马上就要到期的定时器则重编程 */if (reprogram && timer == cpu_base->next_timer)hrtimer_force_reprogram(cpu_base, 1);
}
如果要删除的高分辨率定时器是其激活CPU上马上就要到期的那个定时器,则需要对底层的定时事件设备进行重新编程,让其在下一个定时器的到期时间上到期。关于重编程的部分,后面会介绍。
3.4 定时器激活 hrtimer_start_range_ns
hrtimer_start()
/hrtimer_start_range_ns()
开启定时器;如果定时器不需要指定到期范围就使用hrtimer_start()
,如果定时器需要指定到期范围就使用hrtimer_start_range_ns()
要激活一个高分辨率定时器需要调用 hrtimer_start_range_ns 函数:
void hrtimer_start_range_ns(struct hrtimer *timer, ktime_t tim,u64 delta_ns, const enum hrtimer_mode mode)
{struct hrtimer_clock_base *base;unsigned long flags;if (!IS_ENABLED(CONFIG_PREEMPT_RT))WARN_ON_ONCE(!(mode & HRTIMER_MODE_SOFT) ^ !timer->is_soft);elseWARN_ON_ONCE(!(mode & HRTIMER_MODE_HARD) ^ !timer->is_hard);/* 获得定时器对应CPU的hrtimer_cpu_base结构体内的自旋锁 */base = lock_hrtimer_base(timer, &flags);/* 激活定时器 */if (__hrtimer_start_range_ns(timer, tim, delta_ns, mode, base))/* 如果成功则尝试对定时事件设备重编程 */hrtimer_reprogram(timer, true);/* 释放自旋锁 */unlock_hrtimer_base(timer, &flags);
}
EXPORT_SYMBOL_GPL(hrtimer_start_range_ns);
参数 tim 保存了定时器的“软”到期时间。参数 delta_ns 是到期时间的范围,所以硬到期时间就是tim+delta_ns。参数 mode 指定了到期时间的类型。在获得了定时器对应 CPU 的hrtimer_cpu_base 结构体内的自旋锁后,其接着调用了 __hrtimer_start_range_ns 函数:
static int __hrtimer_start_range_ns(struct hrtimer *timer, ktime_t tim,u64 delta_ns, const enum hrtimer_mode mode,struct hrtimer_clock_base *base)
{struct hrtimer_clock_base *new_base;/* 先将该定时器从现有红黑树中移除 */remove_hrtimer(timer, base, true);/* 如果是相对时间则获得当前时间并累加得到绝对时间 */if (mode & HRTIMER_MODE_REL)tim = ktime_add_safe(tim, base->get_time());tim = hrtimer_update_lowres(timer, tim, mode);/* 设置定时器的“软”“硬”到期时间 */hrtimer_set_expires_range_ns(timer, tim, delta_ns);/* 尝试迁移该高分辨率定时器 */new_base = switch_hrtimer_base(timer, base, mode & HRTIMER_MODE_PINNED);/* 将定时器插入红黑树 */return enqueue_hrtimer(timer, new_base, mode);
}
该函数首先调用 remove_hrtimer 函数,如果要激活的定时器已经被激活过了的话,会将其先删除掉。由于后面还会再激活,所以对应的 restart 参数传的是 true。接着调用了hrtimer_set_expires_range_ns 函数,根据参数设置定时器的“软”“硬”到期时间:
static inline void hrtimer_set_expires_range_ns(struct hrtimer *timer, ktime_t time, u64 delta)
{timer->_softexpires = time;timer->node.expires = ktime_add_safe(time, ns_to_ktime(delta));
}
所以,“硬”到期时间就是“软”到期时间加上delta。
switch_hrtimer_base 函数会尝试迁移该要激活的高分辨率定时器,这个后面会分析。如果不需要迁移,则返回的 hrtimer_clock_base 结构体和调用参数是一样的,否则会返回一个新的要迁移到的 hrtimer_clock_base 结构体。
最后,函数调用 enqueue_hrtimer 函数,将定时器插入红黑树中,从而完成定时器的激活:
static int enqueue_hrtimer(struct hrtimer *timer,struct hrtimer_clock_base *base,enum hrtimer_mode mode)
{debug_activate(timer, mode);/* 设置active_bases中对应的位 */base->cpu_base->active_bases |= 1 << base->index;/* 更新定时器的状态为HRTIMER_STATE_ENQUEUED */WRITE_ONCE(timer->state, HRTIMER_STATE_ENQUEUED);/* 将该定时器加入对应hrtimer_clock_base结构体内的红黑树中 */return timerqueue_add(&base->active, &timer->node);
}
四、hrtimer 的到期处理
处理hrtimer的函数是__hrtimer_run_queues()
,看看有谁调用该函数:
因此hrtimer的处理有3个入口:
- 在hrtimer软中断处理程序中,只处理软timer;
- 在低精度模式下,在每个jiffies的tick中断处理函数中执行,只处理硬timer;
- 在高精度模式下,在每个clock_event_device的到期中断处理函数hrtimer_interrupt中执行,只处理硬timer。
4.1 低精度模式
在系统运行早期,hrtimer子系统先运行在低精度模式,这时是在每个jiffies的tick事件中断(tick_handle_periodic()
)中处理到期的hrtimer,因此此时的时间精度和timer子系统是一致的,都是HZ级别的。
函数调用链:tick_handle_periodic()---->tick_periodic()---->update_process_times()---->run_local_timers()---->hrtimer_run_queues()<kernel/time/hrtimer.c>
void hrtimer_run_queues(void)
{struct hrtimer_cpu_base *cpu_base = this_cpu_ptr(&hrtimer_bases);unsigned long flags;ktime_t now;/* 如果当前是高精度模式,则直接退出 */if (__hrtimer_hres_active(cpu_base))return;/** This _is_ ugly: We have to check periodically, whether we* can switch to highres and / or nohz mode. The clocksource* switch happens with xtime_lock held. Notification from* there only sets the check bit in the tick_oneshot code,* otherwise we might deadlock vs. xtime_lock.*//* 检查是否可以切换到高精度模式 */if (tick_check_oneshot_change(!hrtimer_is_hres_enabled())) {hrtimer_switch_to_hres();return;}raw_spin_lock_irqsave(&cpu_base->lock, flags);/* 获得当前时间 */now = hrtimer_update_base(cpu_base);/* 如果当前时间 > 下一个到期的软timer的过期时间,表示有软timer到期了 */if (!ktime_before(now, cpu_base->softirq_expires_next)) {/* 将下个到期的软超时间设置为最大值,这算是默认值,等待hrtimer软中断处理函数重新更新下一个即将超时的软timer的过期时间*/cpu_base->softirq_expires_next = KTIME_MAX;/* 设置softirq_activated表示已经激活了本CPU的hrtimer软中断 */cpu_base->softirq_activated = 1;/* 触发HRTIMER软中断 */raise_softirq_irqoff(HRTIMER_SOFTIRQ);}/* 处理所有到期的硬timer,用户指定的hrtimer->function会在这里执行 */__hrtimer_run_queues(cpu_base, now, flags, HRTIMER_ACTIVE_HARD);raw_spin_unlock_irqrestore(&cpu_base->lock, flags);
}
低精度模式下,每次执行到hrtimer_run_queues
都会检查是否符合切换到高精度的条件:
- 高精度模式没有被禁止(打开了内核选项CONFIG_HIGH_RES_TIMERS);
- 当前存在高分辨率的时钟源设备;
- 当前定时设备是支持单次触发(oneshot)的。
当以上条件满足时,切换到高精度模式:
<kernel/time/hrtimer.c>
/** Switch to high resolution mode*/
static void hrtimer_switch_to_hres(void)
{struct hrtimer_cpu_base *base = this_cpu_ptr(&hrtimer_bases);/* * 将clock_event_device的中断回调函数注册为hrtimer_interrupt(之前是tick_handle_periodic),* 同时时钟中断的触发模式从周期触发变成了oneshot模式*/if (tick_init_highres()) {pr_warn("Could not switch to high resolution mode on CPU %u\n",base->cpu);return;}base->hres_active = 1;hrtimer_resolution = HIGH_RES_NSEC;/* * 因为时钟中断变成了oneshot模式,但是jiffies的更新、进程调度还是依赖于低精度的tick,* 因此专门通过一个hrtimer循环模拟低精度的tick*/tick_setup_sched_timer();/* "Retrigger" the interrupt to get things going */retrigger_next_event(NULL);
}
我们再稍微关心一下模拟tick的hrtimer的回调函数:
<kernel/time/tick-sched.c>
static enum hrtimer_restart tick_sched_timer(struct hrtimer *timer)
{struct tick_sched *ts =container_of(timer, struct tick_sched, sched_timer);struct pt_regs *regs = get_irq_regs();ktime_t now = ktime_get();/* 主要是更新jiffies时间,注意smp系统中只有一个cpu负责更新jiffiies */tick_sched_do_timer(ts, now);/** Do not call, when we are not in irq context and have* no valid regs pointer*/if (regs)tick_sched_handle(ts, regs);elsets->next_tick = 0;/* No need to reprogram if we are in idle or full dynticks mode */if (unlikely(ts->tick_stopped))return HRTIMER_NORESTART;/* 将该hrtimer的到期时间推后tick_period时间 */hrtimer_forward(timer, now, tick_period);/* 返回HRTIMER_RESTART表示该hrtimer需要再次执行,这就实现了每tick_period时间触发一次tick中断了 */return HRTIMER_RESTART;
}
4.2 高精度模式
当系统切换到高精度定时器模式后,某个hrtimer到期后会调用中断处理函数hrtimer_interrupt()
。需要强调的是,即使在开了PREEMPT_RT的系统上,hrtimer_interrupt
也是在硬中断上下文中执行。
<kernel/time/hrtimer.c>
/** High resolution timer interrupt* Called with interrupts disabled*/
void hrtimer_interrupt(struct clock_event_device *dev)
{struct hrtimer_cpu_base *cpu_base = this_cpu_ptr(&hrtimer_bases);ktime_t expires_next, now, entry_time, delta;unsigned long flags;int retries = 0;/* 如果当前不是高精度定时器模式,则触发oops使系统panic */BUG_ON(!cpu_base->hres_active);cpu_base->nr_events++;dev->next_event = KTIME_MAX;raw_spin_lock_irqsave(&cpu_base->lock, flags);/* 获得当前时间 */entry_time = now = hrtimer_update_base(cpu_base);
retry:/* 标识当前处于hrtimer_interrupt函数中 */cpu_base->in_hrtirq = 1;/** We set expires_next to KTIME_MAX here with cpu_base->lock* held to prevent that a timer is enqueued in our queue via* the migration code. This does not affect enqueueing of* timers which run their callback and need to be requeued on* this CPU.*/cpu_base->expires_next = KTIME_MAX;/* 如果当前时间 > 下一个到期的软timer的过期时间,表示有软timer到期了 */if (!ktime_before(now, cpu_base->softirq_expires_next)) {/* 将下个到期的软超时间设置为最大值,这算是默认值,等待hrtimer软中断处理函数重新更新下一个即将超时的软timer的过期时间*/cpu_base->softirq_expires_next = KTIME_MAX;/* 设置softirq_activated表示已经激活了本CPU的hrtimer软中断 */cpu_base->softirq_activated = 1;/* 触发HRTIMER软中断 */raise_softirq_irqoff(HRTIMER_SOFTIRQ);}/* 处理所有到期的硬timer,用户指定的hrtimer->function会在这里执行 */__hrtimer_run_queues(cpu_base, now, flags, HRTIMER_ACTIVE_HARD);/* 更新接下来即将到期的定时器 *//* Reevaluate the clock bases for the next expiry */expires_next = __hrtimer_get_next_event(cpu_base, HRTIMER_ACTIVE_ALL);/** Store the new expiry value so the migration code can verify* against it.*/cpu_base->expires_next = expires_next;cpu_base->in_hrtirq = 0;raw_spin_unlock_irqrestore(&cpu_base->lock, flags);/* 使用最新的超时时间对定时事件设备(clock_event_device)进行重新编程,即指定下一次arch_timer的中断什么时候到来 *//* Reprogramming necessary ? */if (!tick_program_event(expires_next, 0)) {cpu_base->hang_detected = 0;return;}/** The next timer was already expired due to:* - tracing* - long lasting callbacks* - being scheduled away when running in a VM** We need to prevent that we loop forever in the hrtimer* interrupt routine. We give it 3 attempts to avoid* overreacting on some spurious event.** Acquire base lock for updating the offsets and retrieving* the current time.*/raw_spin_lock_irqsave(&cpu_base->lock, flags);/* 重新获取当前时间,retry处理定时器 & 对定时事件设备进行编程 */now = hrtimer_update_base(cpu_base);cpu_base->nr_retries++;if (++retries < 3)goto retry;/** Give the system a chance to do something else than looping* here. We stored the entry time, so we know exactly how long* we spent here. We schedule the next event this amount of* time away.*//* 如果走到这里说明3次对定时事件设备编程都失败了,更新与hang有关的信息统计 */cpu_base->nr_hangs++;cpu_base->hang_detected = 1;raw_spin_unlock_irqrestore(&cpu_base->lock, flags);/* 计算在hrtimer_interrupt中花费了多少时间,更新max_hang_time */delta = ktime_sub(now, entry_time);if ((unsigned int)delta > cpu_base->max_hang_time)cpu_base->max_hang_time = (unsigned int) delta;/** Limit it to a sensible value as we enforce a longer* delay. Give the CPU at least 100ms to catch up.*//* 适当延后下一次到期时间,最多100毫秒 */if (delta > 100 * NSEC_PER_MSEC)expires_next = ktime_add_ns(now, 100 * NSEC_PER_MSEC);elseexpires_next = ktime_add(now, delta);/* 强制对定时事件设备进程重新编程,否则收不到arch_timer中断了 */tick_program_event(expires_next, 1);pr_warn_once("hrtimer: interrupt took %llu ns\n", ktime_to_ns(delta));
}
4.3 软中断中处理软hrtimer
前文在低精度模式/高精度模式中,在硬中断上下文中处理了硬timer们,对于软timer只是raise了HRTIMER的软中断。这里只聊HRTIMER软中断处理函数。
<kernel/time/hrtimer.c>
static __latent_entropy void hrtimer_run_softirq(struct softirq_action *h)
{struct hrtimer_cpu_base *cpu_base = this_cpu_ptr(&hrtimer_bases);unsigned long flags;ktime_t now;hrtimer_cpu_base_lock_expiry(cpu_base);raw_spin_lock_irqsave(&cpu_base->lock, flags);/* 获得当前时间 */now = hrtimer_update_base(cpu_base);/* 处理所有到期的软timer,用户指定的hrtimer->function会在这里执行 */__hrtimer_run_queues(cpu_base, now, flags, HRTIMER_ACTIVE_SOFT);/* 将本cpu的softirq_activated状态位置0,表示没有激活HRTIMER的软中断 */cpu_base->softirq_activated = 0;hrtimer_update_softirq_timer(cpu_base, true);/* * 更新即将到期的软timer,对clock_event_device重新编程,* 同时更新cpu_base->softirq_expires_next,将其从在* hrtimer_interrupt中置的KTIME_MAX更新为下一个到期的软timer的时间*/raw_spin_unlock_irqrestore(&cpu_base->lock, flags);hrtimer_cpu_base_unlock_expiry(cpu_base);
}
结合hrtimer_run_softirq()
与hrtimer_interrupt()
对软中断的处理,可以看出如果某一次HRTIMER的软中断处理函数hrtimer_run_softirq()
因为操作不当没有得到执行,那么所有的软hrtimer将永远不会得到执行,因此需要确保软中断子系统是稳定的。笔者在生产环境中遇到过网络驱动程序在未关中断的情况下调用raise_softirq_irqoff(NETRX_SOFTIRQ)
修改软中断的pending位,此时正好来了arch_timer的中断,从而触发hrtimer_interrupt
调用raise_softirq_irqoff(HRTIMER_SOFTIRQ)
也来修改软中断的pending位;这种对全局变量非原子的操作,会导致变量RMW(Read-Modify-Write)过程中出现竞争,从而丢掉HRTIMER_SOFTIRQ的pending置位,在高频网络收发包情况下会必现软timer超时,且永远得不到处理的情况,因此当前对软timer的处理逻辑不得不说是十分脆弱的。