Linux 中的定时器
在 Linux 内核中主要有两种类型的定时器。一类称为 timeout 类型,另一类称为 timer 类型。timeout 类型的定时器通常用于检测各种错误条件,例如用于检测网卡收发数据包是否会超时的定时器,IO 设备的读写是否会超时的定时器等等。通常情况下这些错误很少发生,因此,使用 timeout 类型的定时器一般在超时之前就会被移除,从而很少产生真正的函数调用和系统开销。总的来说,使用 timeout 类型的定时器产生的系统开销很小,它是下文提及的 timer wheel 通常使用的环境。此外,在使用 timeout 类型定时器的地方往往并不关心超时处理,因此超时精确与否,早 0.01 秒或者晚 0.01 秒并不十分重要,这在下文论述 deferrable timers 时会进一步介绍。timer 类型的定时器与 timeout 类型的定时器正相反,使用 timer 类型的定时器往往要求在精确的时钟条件下完成特定的事件,通常是周期性的并且依赖超时机制进行处理。例如设备驱动通常会定时读写设备来进行数据交互。如何高效的管理 timer 类型的定时器对提高系统的处理效率十分重要,下文在介绍 hrtimer 时会有更加详细的论述。
内核需要进行时钟管理,离不开底层的硬件支持。在早期是通过 8253 芯片提供的 PIT(Programmable Interval Timer)来提供时钟,但是 PIT 的频率很低,只能提供最高 1ms 的时钟精度,由于 PIT 触发的中断速度太慢,会导致很大的时延,对于像音视频这类对时间精度要求更高的应用并不足够,会极大的影响用户体验。随着硬件平台的不断发展变化,陆续出现了 TSC(Time Stamp Counter),HPET(High Precision Event Timer),ACPI PM Timer(ACPI Power Management Timer),CPU Local APIC Timer 等精度更高的时钟。这些时钟陆续被 Linux 的时钟子系统所采纳,从而不断的提高 Linux 时钟子系统的性能和灵活性。这些不同的时钟会在下文不同的章节中分别进行介绍。
Timer wheel
在 Linux 2.6.16 之前,内核一直使用一种称为 timer wheel 的机制来管理时钟。这就是熟知的 kernel 一直采用的基于 HZ 的 timer 机制。Timer wheel 的核心数据结构如清单 1 所示:
清单 1. Timer wheel 的核心数据结构
#define TVN_BITS (CONFIG_BASE_SMALL ? 4 : 6)
#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)
struct tvec {
struct list_head vec[TVN_SIZE];
};
struct tvec_root {
struct list_head vec[TVR_SIZE];
};
struct tvec_base {
spinlock_t lock;
struct timer_list *running_timer;
unsigned long timer_jiffies;
unsigned long next_timer;
struct tvec_root tv1;
struct tvec tv2;
struct tvec tv3;
struct tvec tv4;
struct tvec tv5;
} ____cacheline_aligned;
以 CONFIG_BASE_SMALL 定义为 0 为例,TVR_SIZE = 256,TVN_SIZE = 64,这样
可以得到如图 1 所示的 timer wheel 的结构。
图 1. Timer wheel 的逻辑结构
在 timer wheel 的框架下,所有系统正在使用的 timer 并不是顺序存放在一个平坦的链表中,因为这样做会使得查找,插入,删除等操作效率低下。Timer wheel 提供了 5 个 timer 数组,数组之间存在着类似时分秒的进位关系。TV1 为第一个 timer 数组,其中存放着从 timer_jiffies(当前到期的 jiffies)到 timer_jiffies + 255 共 256 个 tick 对应的 timer list。因为在一个 tick 上可能同时有多个 timer 等待超时处理,timer wheel 使用 list_head 将所有 timer 串成一个链表,以便在超时时顺序处理。TV2 有 64 个单元,每个单元都对应着 256 个 tick,因此 TV2 所表示的超时时间范围从 timer_jiffies + 256 到 timer_jiffies + 256 * 64 – 1。依次类推 TV3,TV4,TV5。以 HZ=1000 为例,每 1ms 产生一次中断,TV1 就会被访问一次,但是 TV2 要每 256ms 才会被访问一次,TV3 要 16s,TV4 要 17 分钟,TV5 甚至要 19 小时才有机会检查一次。最终,timer wheel 可以管理的最大超时值为 2^32。一共使用了 512 个 list_head(256+64+64+64+64)。如果 CONFIG_BASE_SMALL 定义为 1,则最终使用的 list_head 个数为 128 个(64+16+16+16+16),占用的内存更少,更适合嵌入式系统使用。Timer wheel 的处理逻辑如清单 2 所示:
清单 2. timer wheel 的核心处理函数
static inline void __run_timers(struct tvec_base *base)
{
struct timer_list *timer;
spin_lock_irq(&base-》lock);
while (time_after_eq(jiffies, base-》timer_jiffies)) {
struct list_head work_list;
struct list_head *head = &work_list;
int index = base-》timer_jiffies & TVR_MASK;
/*
* Cascade timers:
*/
if (!index &&
(!cascade(base, &base-》tv2, INDEX(0))) &&
(!cascade(base, &base-》tv3, INDEX(1))) &&
!cascade(base, &base-》tv4, INDEX(2)))
cascade(base, &base-》tv5, INDEX(3));
++base-》timer_jiffies;
list_replace_init(base-》tv1.vec + index, &work_list);
while (!list_empty(head)) {
void (*fn)(unsigned long);
unsigned long data;
timer = list_first_entry(head, struct timer_list,entry);
fn = timer-》function;
data = timer-》data;
。 . 。 .
fn(data);
。 . 。 .
}
base-》timer_jiffies 用来记录在 TV1 中最接近超时的 tick 的位置。index 是用来遍历 TV1 的索引。每一次循环 index 会定位一个当前待处理的 tick,并处理这个 tick 下所有超时的 timer。base-》timer_jiffies 会在每次循环后增加一个 jiffy,index 也会随之向前移动。当 index 变为 0 时表示 TV1 完成了一次完整的遍历,此时所有在 TV1 中的 timer 都被处理了,因此需要通过 cascade 将后面 TV2,TV3 等 timer list 中的 timer 向前移动,类似于进位。这种层叠的 timer list 实现机制可以大大降低每次检查超时 timer 的时间,每次中断只需要针对 TV1 进行检查,只有必要时才进行 cascade。即便如此,timer wheel 的实现机制仍然存在很大弊端。一个弊端就是 cascade 开销过大。在极端的条件下,同时会有多个 TV 需要进行 cascade 处理,会产生很大的时延。这也是为什么说 timeout 类型的定时器是 timer wheel 的主要应用环境,或者说 timer wheel 是为 timeout 类型的定时器优化的。因为 timeout 类型的定时器的应用场景多是错误条件的检测,这类错误发生的机率很小,通常不到超时就被删除了,因此不会产生 cascade 的开销。另一方面,由于 timer wheel 是建立在 HZ 的基础上的,因此其计时精度无法进一步提高。毕竟一味的通过提高 HZ 值来提高计时精度并无意义,结果只能是产生大量的定时中断,增加额外的系统开销。因此,有必要将高精度的 timer 与低精度的 timer 分开,这样既可以确保低精度的 timeout 类型的定时器应用,也便于高精度的 timer 类型定时器的应用。还有一个重要的因素是 timer wheel 的实现与 jiffies 的耦合性太强,非常不便于扩展。因此,自从 2.6.16 开始,一个新的 timer 子系统 hrtimer 被加入到内核中。
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉