Linux内核中提供有timer软件定时器,可配置精度为1/HZ。同时还提供有高精度软件定时器hrtimer。本文记录低精度软件定时器timer的使用及原理。

本文内容参考内核版本 3.10.0-862.el7.x86_64

timer 例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Hu Yu <hyuuhit@gmail.com>");
MODULE_DESCRIPTION("lkm test");
MODULE_VERSION("0.1");


static struct timer_list test_timer;

static void timer_handler(unsigned long data) {
static int timer_first = 1;
pr_info("timer handler cpu: %d\n", smp_processor_id());

if (timer_first) {
pr_info("in_interrupt %lu\n", in_interrupt());
pr_info("in_irq %lu\n", in_irq());
pr_info("in_softirq %lu\n", in_softirq());
pr_info("in_serving_softirq %lu\n", in_serving_softirq());
pr_info("in_atomic %d\n", in_atomic());

dump_stack();
}

timer_first = 0;
mod_timer(&test_timer, jiffies + HZ * 1.5);
}

static int __init my_init(void)
{
pr_info("my init.\n");
pr_info("smp_processor_id %d\n", smp_processor_id());

init_timer(&test_timer);

test_timer.function = timer_handler;
// 2.5秒后到期
test_timer.expires = jiffies + HZ * 2.5;

pr_info("expires before add: %lu\n", test_timer.expires);
add_timer(&test_timer);
pr_info("expires after add : %lu\n", test_timer.expires);

return 0;
}
static void __exit my_exit(void)
{
pr_info("smp_processor_id %d\n", smp_processor_id());
del_timer_sync(&test_timer);
pr_info("my exit.\n");
}

module_init(my_init);
module_exit(my_exit);

更多api参考include/linux/timer.h

timer 原理

linux内核中timer激活过程如下:

  1. 每个CPU上发生本地定时器中断
  2. 中断服务例程调用per cpu变量时钟事件设备的event_handler函数
  3. event_handler中会激活TIMER_SOFTIRQ软中断
  4. 软中断处理函数检查并调用到期定时器

先看一下系统启动时的配置是如何将这个过程联系起来的。

硬件定时器初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
+------------+
|start_kernel|
+-----+------+
|
|
+--> blabla...
|
| +----------+
+---+setup_arch|
| +----------+
| 这里与时钟相关的是调用register_refined_jiffies注册了一个时钟源refined_jiffies。
|
+--> blabla...
|
| +--------------+
+---+tick_nohz_init|
| +--------------+
| 为nohz模式做初始化配置,比如分配nohz模式和housekeeping模式的cpu位图,
| 配置cpu donw的回调函数(比如nohz模式下负责定时器的cpu不能down)。
|
+--> blabla...
|
| +--------+
+---+init_IRQ|
| +-+------+
| |
| +--> blabla...
| |
| | +-------------------------+
| +---+ x86_init.irqs.intr_init |
| | |实际调用为native_init_IRQ|
| | +--+----------------------+
| | |
| | | blabla...
| | |
| | | +--------------+
| | |---+apic_intr_init|
| | | +--------------+
| | | 为LOCAL_TIMER_VECTOR中断服务例程为apic_timer_interrupt
| | | 实际为smp_apic_timer_interrupt
| | | 这个是cpu本地定时器中断,TIMER_SOFTIRQ由这个中断激活。
| | |
| | | blabla...
| |<-----+
|<----+
|
| +---------+
+---+tick_init|
| +---------+
| 调用tick_broadcast_init分配了6个tick broadcast相关的位图。
|
|
| +-------------+
| | |
+---+ init_timers |
| | |
| +-------------+
| timer软件定时器数据结构的初始化,后面详细介绍。
|
|
+--> blabla...
|
| +---------+
+---+time_init|
| +---------+
| 体系结构相关,x86下只是把late_time_init函数指针设置为x86_late_time_init。
|
+--> blabla...
|
| +--------------+
+---+late_time_init|
| +--+-----------+
| | 实际调用函数为x86_late_time_init
| |
| | +--------------------------+
| +----+x86_init.timers.timer_init|
| | +--------------------------+
| | x86_init定义位于arch/x86/kernel/x86_init.c
| | 实际调用函数为hpet_time_init
| | 启用hpet作为时钟事件设备,并作为时钟源(global_clock_event指针设置为hpet_clockevent)。
| | 如果失败的话启用精度较低的pit作为时钟事件设备。
| | 启用hpet过程中hpet_clockevent->event_handler设置为tick_handle_periodic。
| | 设置0号中断(定时器中断)的处理函数为timer_interrupt。
| |
| | +--------+
| +----+tsc_init|
| | +--------+
| | 启用tsc时钟源。
| |
|<-----+
|
+--> blabla...
|
| +---------+
+---+rest_init|
| +--+------+
| |
| | +-----------------------+
| +---|kernel_init 1号内核线程|
| | +-----------------------+
| | |
| | | +--------------------+
| | +---+kernel_init_freeable|
| | | +--+-----------------+
| | | |
| | | | blabla...
| | | |
| | | | +---------------------------------------------+
| | | | |smp_prepare_cpu包装了smp_ops.smp_prepare_cpus|
| | | | |实际调用为native_smp_prepare_cpus |
| | | | +--+------------------------------------------+
| | | | |
| | | | | blabla...
| | | | |
| | | | | +------------------------------------+
| | | | +---+x86_init.timers.setup_percpu_clockev|
| | | | | | 实际调用为setup_boot_APIC_clock |
| | | | | +--+---------------------------------+
| | | | | |
| | | | | | blabla...
| | | | | |
| | | | | | +------------------+
| | | | | | | |
| | | | | | | setup_APIC_timer |
| | | | | | | |
| | | | | | +------------------+
| | | | | | 设置了当前cpu的per cpu变量lapic_events内容为lapic_clockevent
| | | | | | 并注册该时钟事件设备,这里会设置event_handler为tick_handle_periodic
| | | | | |
| | | | | | blabla...
| | | | | |
| | | | |<-----+
| | | |<-----+
| | | |
| | | | blabla...
| | | |
| | | | +--------+
| | | +---+smp_init|
| | | | +--------+
| | | | 这个流程比较长,直接列主要的调用栈吧。
| | | | smp_init->cpu_up(for each present cpu)->_cpu_up->__cpu_up->
| | | | ->smp_ops.cpu_up(实际为native_cpu_up)->do_boot_cpu->
| | | | ->wakeup_cpu_via_init_nmi(实际这里有个分支判断,暂且就当是这个吧)->
| | | | ->wakeup_cpu_via_init->通过startup_ipi_hook调用start_secondary(没看懂,就当是吧)->
| | | | ->x86_cpuinit.setup_percpu_clockev(实际为setup_secondary_APIC_clock)->
| | | |
| | | | ->setup_APIC_timer
| | | | 又到这个函数了,解释见上文
| | | |
| | |<-----+
| | |
| | | blabla...
| | |
|<-----+ |
| |
| |
END 1号线程

LOCAL_TIMER_VECTOR(这是cpu的本地定时器中断,每个cpu都会接收)中断服务例程实际为smp_apic_timer_interrupt,这里我们关心的调用栈为:

smp_apic_timer_interrupt -> local_apic_timer_interrupt -> per cpu变量lapic_events成员event_handler(我们知道这是tick_handle_periodic)
tick_handle_periodic -> tick_periodic -> update_process_times -> run_local_timers

这里激活了TIMER_SOFTIRQ软中断。

timer 初始化

继续看init_timers函数,包含了定时器相关结构的配置和软中断处理函数设置。

init_timers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static struct notifier_block timers_nb = {
.notifier_call = timer_cpu_notify,
};


void __init init_timers(void)
{
int err;

/* ensure there are enough low bits for flags in timer->base pointer */
BUILD_BUG_ON(__alignof__(struct tvec_base) & TIMER_FLAG_MASK);

err = timer_cpu_notify(&timers_nb, (unsigned long)CPU_UP_PREPARE,
(void *)(long)smp_processor_id());
init_timer_stats();

BUG_ON(err != NOTIFY_OK);
register_cpu_notifier(&timers_nb);
open_softirq(TIMER_SOFTIRQ, run_timer_softirq);
}

timer_cpu_notify函数第一次运行会调用timer_cpu_notify,并在cpu通知链上注册回调函数(依然是timer_cpu_notify),最后注册了TIMER_SOFTIRQ定时器软中断的处理函数run_timer_softirq。其中timer相关结构配置在timer_cpu_notify中,timer的运行在run_timer_softirq中。

timer_cpu_notify
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static int timer_cpu_notify(struct notifier_block *self,
unsigned long action, void *hcpu)
{
long cpu = (long)hcpu;
int err;

switch(action) {
case CPU_UP_PREPARE:
case CPU_UP_PREPARE_FROZEN:
err = init_timers_cpu(cpu);
if (err < 0)
return notifier_from_errno(err);
break;
#ifdef CONFIG_HOTPLUG_CPU
case CPU_DEAD:
case CPU_DEAD_FROZEN:
migrate_timers(cpu);
break;
#endif
default:
break;
}
return NOTIFY_OK;
}

timer_cpu_notify中,对cpu拔除通知调用函数migrate_timers(这个函数的作用是将拔除的cpu上的timer迁移到当前cpu上),对cpu up事件调用函数init_timers_cpu

init_timers_cpu
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
static int init_timers_cpu(int cpu)
{
int j;
struct tvec_base *base;
static char tvec_base_done[NR_CPUS];

if (!tvec_base_done[cpu]) {
static char boot_done;

if (boot_done) {
/*
* The APs use this path later in boot
*/
base = kmalloc_node(sizeof(*base),
GFP_KERNEL | __GFP_ZERO,
cpu_to_node(cpu));
if (!base)
return -ENOMEM;

/* Make sure that tvec_base is 2 byte aligned */
if (tbase_get_deferrable(base)) {
WARN_ON(1);
kfree(base);
return -ENOMEM;
}
per_cpu(tvec_bases, cpu) = base;
} else {
/*
* This is for the boot CPU - we use compile-time
* static initialisation because per-cpu memory isn't
* ready yet and because the memory allocators are not
* initialised either.
*/
boot_done = 1;
base = &boot_tvec_bases;
}
spin_lock_init(&base->lock);
tvec_base_done[cpu] = 1;
} else {
base = per_cpu(tvec_bases, cpu);
}

for (j = 0; j < TVN_SIZE; j++) {
INIT_LIST_HEAD(base->tv5.vec + j);
INIT_LIST_HEAD(base->tv4.vec + j);
INIT_LIST_HEAD(base->tv3.vec + j);
INIT_LIST_HEAD(base->tv2.vec + j);
}
for (j = 0; j < TVR_SIZE; j++)
INIT_LIST_HEAD(base->tv1.vec + j);

base->timer_jiffies = jiffies;
base->next_timer = base->timer_jiffies;
base->active_timers = 0;
base->all_timers = 0;
return 0;
}

上面代码中涉及两个变量

1
2
3
struct tvec_base boot_tvec_bases;
EXPORT_SYMBOL(boot_tvec_bases);
static DEFINE_PER_CPU(struct tvec_base *, tvec_bases) = &boot_tvec_bases;

这里的struct tvec_base结构是timer定时器的挂载点,timer的生效必须要挂载到一个struct tvec_base上,这个结构是每个cpu独立分配一个的,这种方式避免了smp下多cpu竞争timer的情况。这个结构涉及两个变量:

  • boot_tvec_bases
    struct tvec_base类型的结构体,其地址也是tvec_bases指向的默认地址。
  • tvec_bases
    struct tvec_base指针类型,这是一个per cpu变量,上面挂载了该cpu上所有等待的timer_list。由于其静态初始化为boot_tvec_bases的地址,因此在未重新赋值前,每个cpu对应的值都为该地址。简单的说,per cpu的变量自身有一个地址,但是对应多个cpu实际有多块内存区域,每块区域对应一个cpu,通过cpu id查找到对应区域的偏移量,来确定该cpu对应的变量内存,因此重要的是该变量的地址,per cpu变量自身的值只在静态初始化时有意义,一旦每个cpu的值重新修改过,该变量自身的值不再有意义。

init_timers_cpu主要作用的对传入的cpu id对应的特定cpu的tvec_bases变量做初始化。在做初始化之前首先需要确定该cpu的tvec_bases对应的地址。对于启动阶段的cpu,直接使用boot_tvec_bases的地址,其内存是在编译时静态分配的,也被静态赋值给了tvec_bases,因为启动阶段per cpu的内存并没有准备好,而且内存分配器也没有初始化完成。对于后续通知链通知的可以使用的cpu,动态分配同一numa node上的内存,并赋值给该cpu对应的tvec_bases。对于已经分配过内存的cpu,拔除cpu再上线的情况,由于使用了静态标识数组对每一个cpu的初始化做记录,只会重新初始化结构体成员,不会多次分配内存。

初始化部分到这里就完成了。

timer 结构体

timer定时器中,主要涉及两个结构体

  • struct tvec_base
  • struct timer_list

tvec_base

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*
* per-CPU timer vector definitions:
*/
#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)
#define MAX_TVAL ((unsigned long)((1ULL << (TVR_BITS + 4*TVN_BITS)) - 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;
// 下次激活需要检查的tv1槽位由该值确定,每检查一个槽位增一,直到追上全局jiffies。
// 这样可以适应timer中断间跳过多个tick的情况。
unsigned long timer_jiffies;
// 下面两个成员与TIMER_DEFERRABLE有关,应该与cpu睡眠省电有关,暂不关注。
unsigned long next_timer;
unsigned long active_timers;
// 下面5个不同层级的定时器轮。
struct tvec_root tv1;
struct tvec tv2;
struct tvec tv3;
struct tvec tv4;
struct tvec tv5;
RH_KABI_EXTEND(unsigned long all_timers)
} ____cacheline_aligned;

struct tvec_base boot_tvec_bases;
EXPORT_SYMBOL(boot_tvec_bases);
static DEFINE_PER_CPU(struct tvec_base *, tvec_bases) = &boot_tvec_bases;

前面说过这个结构体用于per cpu变量,每个cpu使用该结构体挂载关联的timer_list。此定时器的设计称为定时器轮(timer wheel),最近的内核中已经更改了该设计,但是本文参考的内核版本依然使用该定时器轮。

定时器轮在默认编译配置下对32位tick时间做分割,分为最低的8位和依次向上的4个6位,在结构体上体现就是256个哈希槽的tv1和64个哈希槽的tv2~tv5,tv1存储到期时间距离当前时间在256个tick以内的timer,tv2存储超越了tv1时间但在高6位tick时间以内到期的timer,tv3~tv5依次递增,tv5特殊一点在于如果到期时间距今超出了MAX_TVAL则依然存储在tv5中。这里tick时间体现为全局变量jiffies,这个变量记录了系统硬件定时器的tick变化,由特定的cpu负责在本地定时器中断服务例程中被更新,更新先于软件定时器timer的检查激活。

插入时使用到期时间距离当前时间的tick差值选择定时器轮中具体哪个轮,之后使用到期时间tick值在该轮对应位的值选择使用该轮的哪个槽位。比如

  • 假设当前tick为1000,timer到期时间为1200。距离超时还有200tick,因此选择tv1轮。1200在低8位的值为176,因此选择槽位176。
  • 假设当前tick为1000,timer到期时间为1300。距离超时还有300tick,因此选择tv2轮。1300在tv2对应的6位的值为5(这里右移tv1的8位,然后用6位做掩码),因此选择槽位5。
  • 假设当前tick为1000,timer到期时间为32768。距离超时还有31768tick,因此选择tv3轮。32768在tv3对应的6位的值为2(这里右移tv1的8位,再右移tv2的6位,然后用6位做掩码),因此选择槽位2。
  • 有一个特殊情况是插入timer的到期时间比当前时间小,则插入到tv1中,使用成员timer_jiffies选择槽位,这样可以在最近的中断后被激活。
  • 另一个特殊情况是插入timer的到期时间距离当前时间超过了MAX_TVAL,插入tv5,槽位做了一些处理,不管了。

在运行时,使用成员timer_jiffies与TVR_MASK(也就是tv1对应的低8位)选择tv1的槽位,只激活tv1中对应槽位的timer。tv1中的256个槽位轮转一圈后,从tv2中选择一个槽位的timer做重新插入,这将使这些timer重新插入到tv1相应槽位中。tv2轮转一圈后,从tv3中用同样的方式选择一个槽位的timer重新插入tv1中。从上一层级级联下降插入的时机为当前层级轮转到0号槽位。这个运行模式也就是定时器轮的命名原因。

timer_list

include/linux/timer.h中部分代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 这个暂不关注。
#define TIMER_DEFERRABLE 0x1LU

/*
* 这个标识记录定时器是否可以在中断中被修改或删除。
* 如果这个标记存在于定时器中,表示这个定时器可能在中断中被修改或删除,
* 这样在定时器函数被回调时,将保持中断关闭状态。
* 如果没有这个标记,表示定时器不会再中断中被操作,这样定时器函数被回调时将会开启中断,
* 这种情况如果错误的在中断中操作了定时器,可能会有引起死锁。
* */
#define TIMER_IRQSAFE 0x2LU

// 由于前两个标记是借用了tvec_base地址的低二位,因此要取得正确的地址,需要将低二位掩掉。
#define TIMER_FLAG_MASK 0x3LU
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct timer_list {
/*
* All fields that change during normal runtime grouped to the
* same cacheline
*/
// 用于插入到tvec_base。
struct list_head entry;
// 定时器到期tick时间。
unsigned long expires;
// 标识归属的tvec_base,也就是对应了cpu。
// 由于tvec_base做了4字节对齐,因此可以使用低2位做标记
// TIMER_DEFERRABLE 和 TIMER_IRQSAFE
struct tvec_base *base;

void (*function)(unsigned long);
unsigned long data;

// 记录该定时器可以允许的宽限延迟tick数,如果为-1则系统会自动按比例设置允许的延迟范围做聚集。
int slack;

#ifdef CONFIG_TIMER_STATS
int start_pid;
void *start_site;
char start_comm[16];
#endif
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};

timer 插入

add_timer为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* add_timer - start a timer
* @timer: the timer to be added
*
* The kernel will do a ->function(->data) callback from the
* timer interrupt at the ->expires point in the future. The
* current time is 'jiffies'.
*
* The timer's ->expires, ->function (and if the handler uses it, ->data)
* fields must be set prior calling this function.
*
* Timers with an ->expires field in the past will be executed in the next
* timer tick.
*/
void add_timer(struct timer_list *timer)
{
BUG_ON(timer_pending(timer));
mod_timer(timer, timer->expires);
}
EXPORT_SYMBOL(add_timer);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* mod_timer - modify a timer's timeout
* @timer: the timer to be modified
* @expires: new timeout in jiffies
*
* mod_timer() is a more efficient way to update the expire field of an
* active timer (if the timer is inactive it will be activated)
*
* mod_timer(timer, expires) is equivalent to:
*
* del_timer(timer); timer->expires = expires; add_timer(timer);
*
* Note that if there are multiple unserialized concurrent users of the
* same timer, then mod_timer() is the only safe way to modify the timeout,
* since add_timer() cannot modify an already running timer.
*
* The function returns whether it has modified a pending timer or not.
* (ie. mod_timer() of an inactive timer returns 0, mod_timer() of an
* active timer returns 1.)
*/
int mod_timer(struct timer_list *timer, unsigned long expires)
{
expires = apply_slack(timer, 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;

return __mod_timer(timer, expires, false, TIMER_NOT_PINNED);
}
EXPORT_SYMBOL(mod_timer);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
* 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
*/
static inline
unsigned long apply_slack(struct timer_list *timer, unsigned long expires)
{
unsigned long expires_limit, mask;
int bit;

if (timer->slack >= 0) {
expires_limit = expires + timer->slack;
} else {
long delta = expires - jiffies;

if (delta < 256)
return expires;

expires_limit = expires + delta / 256;
}
mask = expires ^ expires_limit;
if (mask == 0)
return expires;

bit = find_last_bit(&mask, BITS_PER_LONG);

mask = (1UL << bit) - 1;

expires_limit = expires_limit & ~(mask);

return expires_limit;
}

可以看到mod_timer时,调用了apply_slack对定时器的到期时间做了一定程度的延迟聚集。这个延迟聚集只有在apply_timermod_timer这两个函数中发生,其他的设置定时器的函数并不会做此操作。修改过到期时间后,其他的插入操作参考源码就很容易理解了,不做更多说明。

timer 激活

timer在TIMER_SOFTIRQ软中断处理函数run_timer_softirq中被检查并激活,仅处理当前CPU上的timer,运行逻辑在tvec_base结构中有说明。

1
2
3
4
5
6
7
8
9
10
/*
* This function runs timers and the timer-tq in bottom half context.
*/
static void run_timer_softirq(struct softirq_action *h)
{
struct tvec_base *base = __this_cpu_read(tvec_bases);

if (time_after_eq(jiffies, base->timer_jiffies))
__run_timers(base);
}