LV005-内核定时器简介
一、内核时间管理
过 UCOS 或 FreeRTOS 的话应该知道, UCOS 或 FreeRTOS 是需要一个硬件定时器提供系统时钟,一般使用 Systick 作为系统时钟源。同理, Linux 要运行,也是需要一个系统时钟的,至于这个系统时钟是由哪个定时器提供的,就不清楚了,没有去研究过 Linux 内核这部分的逻辑。
但是在 CortexA7 内核中有个通用定时器,在《Cortex-A7 MPCore Technical Reference Manual r0p5》的“9: Generic Timer”章节有简单的讲解,关于这个通用定时器的详细内容,可以参考《ARM Architecture Reference Manual ARMv7-A and ARMv7-R edition》的“chapter B8 The Generic Timer”章节。

这个通用定时器是可选的,猜测 Linux 会将这个通用定时器作为 Linux 系统时钟源(前提是 SOC 得选配这个通用定时器)。具体是怎么做的没有深入研究过,这里仅仅是猜测!不过对于我们 Linux 驱动编写来说,不需要深入研究这些具体的实现,只需要掌握相应的 API 函数即可,除非是内核编写者或者内核爱好者。
二、系统节拍率
Linux 内核中有大量的函数需要时间管理,比如周期性的调度程序、延时程序、对于驱动编写者来说最常用的定时器。硬件定时器提供时钟源,时钟源的频率可以设置, 设置好以后就周期性的产生定时中断,系统使用定时中断来计时。中断周期性产生的频率就是系统频率,也叫做节拍率(tick rate)(有的资料也叫系统频率),比如 1000Hz, 100Hz 等等说的就是系统节拍率。系统节拍率是可以设置的,单位是 Hz,我们在编译 Linux 内核的时候可以通过图形化界面设置系统节拍率,我们执行:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
这里是 4.19.91 版本的内核,其他版本也一样的,按照如下路径打开配置界面:
Kernel Features --->
Timer frequency (100Hz) --->
可以看出,可选的系统节拍率为 100Hz、 200Hz、 250Hz、 300Hz、 500Hz 和 1000Hz,默认情况下选择 100Hz。 设置好以后打开 Linux 内核源码根目录下的.config 文件,在此文件中有如下定义:

CONFIG_HZ 为 100, Linux 内核会使用 CONFIG_HZ 来设置自己的系统时钟。我们打开 param.h - include/asm-generic/param.h 文件
# undef HZ
# define HZ CONFIG_HZ /* Internal kernel timer frequency */
# define USER_HZ 100 /* some user interfaces are */
# define CLOCKS_PER_SEC (USER_HZ) /* in "ticks" like times() */这里定义了一个宏 HZ,宏 HZ 就是 CONFIG_HZ,因此 HZ = 100,我们后面编写 Linux 驱动的时候会常常用到 HZ,因为 HZ 表示一秒的节拍数,也就是频率。
系统节拍率默认为 100Hz,怎么这么小? 100Hz 是可选的节拍率里面最小的。为什么不选择大一点的呢?这里就引出了一个问题:高节拍率和低节拍率的优缺点:
(1)高节拍率会提高系统时间精度,如果采用 100Hz 的节拍率,时间精度就是 10ms,采用 1000Hz 的话时间精度就是 1ms,精度提高了 10 倍。高精度时钟的好处有很多,对于那些对时间要求严格的函数来说,能够以更高的精度运行,时间测量也更加准确。
(2)高节拍率会导致中断的产生更加频繁,频繁的中断会加剧系统的负担, 1000Hz 和 100Hz 的系统节拍率相比,系统要花费 10 倍的“精力”去处理中断。中断服务函数占用处理器的时间增加,但是现在的处理器性能都很强大,所以采用 1000Hz 的系统节拍率并不会增加太大的负载压力。根据自己的实际情况,选择合适的系统节拍率,一般我们还是采用默认的 100Hz 系统节拍率。
三、 jiffies
1. jiffies 是什么?
Linux 内核使用全局变量 jiffies 来记录系统从启动以来的系统节拍数,系统启动的时候会将 jiffies 初始化为 0, jiffies 定义在文件 jiffies.h - include/linux/jiffies.h (timer.h 文件中会包含这个头文件,不需要重复引用 )中:
/*
* The 64-bit value is not atomic - you MUST NOT read it
* without sampling the sequence number in jiffies_lock.
* get_jiffies_64() will do this for you as appropriate.
*/
extern u64 __cacheline_aligned_in_smp jiffies_64;
extern unsigned long volatile __cacheline_aligned_in_smp __jiffy_arch_data jiffies;第 6 行:定义了一个 64 位的 jiffies_64。
第 7 行:定义了一个 unsigned long 类型的 32 位的 jiffies。
jiffies_64 和 jiffies 其实是同一个东西, jiffies_64 用于 64 位系统,而 jiffies 用于 32 位系统。为了兼容不同的硬件, jiffies 其实就是 jiffies_64 的低 32 位, jiffies_64 和 jiffies 的结构如图

当我们访问 jiffies 的时候其实访问的是 jiffies_64 的低 32 位,使用 get_jiffies_64() 这个函数可以获取 jiffies_64 的值。在 32 位的系统上读取 jiffies 的值,在 64 位的系统上 jiffes 和 jiffies_64 表示同一个变量,因此也可以直接读取 jiffies 的值。所以不管是 32 位的系统还是 64 位系统,都可以使用 jiffies。
1.2 溢出了怎么办?
前面说了 HZ 表示每秒的节拍数, jiffies 表示系统运行的 jiffies 节拍数,所以 jiffies/HZ 就是系统运行时间,单位为秒。不管是 32 位还是 64 位的 jiffies,都有溢出的风险,溢出以后会重新从 0 开始计数,相当于绕回来了,因此有些资料也将这个现象也叫做绕回。
假如 HZ 为最大值 1000 的时候, 32 位的 jiffies 只需要 49.7 天就发生了绕回,对于 64 位的 jiffies 来说大概需要 5.8 亿年才能绕回,因此 jiffies_64 的绕回忽略不计。处理 32 位 jiffies 的绕回显得尤为重要 ,Linux 内核提供了下面所示的几个 API 函数来处理绕回。
- time_after(unkown, known)
- time_before(unkown, known)
- time_after_eq(unkown, known)
- time_before_eq(unkown, known)
unkown 通常为 jiffies, known 通常是需要对比的值。如果 unkown 超过 known 的话,time_after() 函数返回真,否则返回假。如果 unkown 没有超过 known 的话 time_before() 函数返回真,否则返回假。 time_after_eq() 函数和 time_after() 函数类似,只是多了判断等于这个条件。同理, time_before_eq() 函数和 time_before() 函数也类似。比如我们要判断某段代码执行时间有没有超时,此时就可以使用如下所示代码:
unsigned long timeout;
timeout = jiffies + (2 * HZ); /* 超时的时间点 */
/* 判断有没有超时 */
if(time_before(jiffies, timeout))
{
/* 超时未发生 */
}
else
{
/* 超时发生 */
}timeout 就是超时时间点,比如我们要判断代码执行时间是不是超过了 2 秒,那么超时时间点就是 jiffies+(2*HZ),如果 jiffies 大于 timeout 那就表示超时了,否则就是没有超时。第 5 行通过函数 time_before() 来判断 jiffies 是否小于 timeout,如果小于的话就表示没有超时。
1.3 jiffies 转 ms、us、ns
为了方便开发, Linux 内核提供了几个 jiffies 和 ms、 us、 ns 之间的转换函数 :
| int jiffies_to_msecs(const unsigned long j) | 将 jiffies 类型的参数 j 分别转换为对应的毫秒、微秒、纳秒。 |
| int jiffies_to_usecs(const unsigned long j) | |
| u64 jiffies_to_nsecs(const unsigned long j) | |
| long msecs_to_jiffies(const unsigned int m) | 将毫秒、微秒、纳秒转换为 jiffies 类型。 |
| long usecs_to_jiffies(const unsigned int u) | |
| unsigned long nsecs_to_jiffies(u64 n) |
四、内核定时器简介
硬件为内核提供了一个系统定时器来计算流逝的时间(即基于未来时间点的计时方式, 以当前时刻为计时开始的起点, 以未来的某一时刻为计时的终点) ,内核只有在系统定时器的帮助下才能计算和管理时间, 但是内核定时器的精度并不高, 所以不能作为高精度定时器使用。并且
内核定时器的运行没有周期性, 到达计时终点后会自动关闭。 如果要实现周期性定时, 就要在定时处理函数中重新开启定时器。
1. struct timer_list
内核中 struct timer_list 结构用于表示一个内核定时器:
struct timer_list {
/*
* All fields that change during normal runtime grouped to the
* same cacheline
*/
struct hlist_node entry;
unsigned long expires; /* 定时器超时时间, 单位是节拍数 */
void (*function)(struct timer_list *);/* 定时处理函数 */
u32 flags;
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};要使用内核定时器首先要先定义一个 timer_list 变量,表示定时器。
- tiemr_list 结构体的 expires 成员变量表示超时时间,单位为节拍数。
比如我们现在需要定义一个周期为 2 秒的定时器,那么这个定时器的超时时间就是 jiffies+(2*HZ),因此 expires = jiffies+(2*HZ)。 或者可以通过 msecs_to_jiffies() 函数计算,expires = jiffies+msecs_to_jiffies(2000) 也可以实现 2 秒的定时。
- function 就是定时器超时以后的定时处理函数,我们要做的工作就放到这个函数里面,需要我们编写这个定时处理函数。
2. 相关 API
2.1 DEFINE_TIMER()
DEFINE_TIMER() 宏可用于定义一个定时器结构体变量,它可以直接指定超时处理函数,但是只是定义了变量,后续还是需要调用一系列的函数对定时器进行初始化。
#define __TIMER_INITIALIZER(_function, _flags) { \
.entry = { .next = TIMER_ENTRY_STATIC }, \
.function = (_function), \
.flags = (_flags), \
__TIMER_LOCKDEP_MAP_INITIALIZER( \
__FILE__ ":" __stringify(__LINE__)) \
}
#define DEFINE_TIMER(_name, _function) \
struct timer_list _name = \
__TIMER_INITIALIZER(_function, 0)_name 为定义的结构体名称,_function 为定时处理函数,可以使用以下代码对定时器和相应的定时处理函数进行定义:
static void timer_handler(struct timer_list *t)//定义 function_test 定时功能函数
{
//......
}
DEFINE_TIMER(timer_test, timer_handler);//定义一个定时器这里可以看到其实就是定义了一个全局变量,然后进行参数初始化,我们也可以自己去初始化这些成员。
2.2 add_timer()
add_timer() 用于向 Linux 内核注册定时器,使用 add_timer() 函数向内核注册定时器以后,定时器就会开始运行
void add_timer(struct timer_list *timer)
{
BUG_ON(timer_pending(timer));
mod_timer(timer, timer->expires);
}
EXPORT_SYMBOL(add_timer);【参数说明】
- timer:要注册的定时器。
【返回值】 无
2.3 del_timer()
del_timer() 函数用于删除一个定时器,不管定时器有没有被激活,都可以使用此函数删除。在多处理器系统上,定时器可能会在其他的处理器上运行,因此在调用 del_timer() 函数删除定时器之前要先等待其他处理器的定时处理器函数退出。
int del_timer(struct timer_list *timer)
{
struct timer_base *base;
unsigned long flags;
int ret = 0;
debug_assert_init(timer);
if (timer_pending(timer)) {
base = lock_timer_base(timer, &flags);
ret = detach_if_pending(timer, base, true);
raw_spin_unlock_irqrestore(&base->lock, flags);
}
return ret;
}
EXPORT_SYMBOL(del_timer);【参数说明】
- timer:要删除的定时器。
【返回值】 0,定时器还没被激活; 1,定时器已经激活。
2.3.1 del_timer_sync()
del_timer_sync() 函数是 del_timer() 函数的同步版,会等待其他处理器使用完定时器再删除, del_timer_sync() 不能使用在中断上下文中。
#ifdef CONFIG_SMP
extern int del_timer_sync(struct timer_list *timer);
#else
# define del_timer_sync(t) del_timer(t)
#endif【参数说明】
- timer:要删除的定时器。
【返回值】 0,定时器还没被激活; 1,定时器已经激活。
2.3.2 mod_timer()
mod_timer() 函数用于修改定时值,如果定时器还没有激活的话, mod_timer 函数会激活定时器
int mod_timer(struct timer_list *timer, unsigned long expires)
{
return __mod_timer(timer, expires, 0);
}
EXPORT_SYMBOL(mod_timer);【参数说明】
- timer:要修改超时时间(定时值)的定时器。
- expires:修改后的超时时间。
【返回值】 0,调用 mod_timer 函数前定时器未被激活; 1,调用 mod_timer 函数前定时器已被激活。
Tips:
若是最开始定时器的超时时间为 5s,现在在第 1s,这个时候修改了定时器的超时时间为 2s,此时,定时器会根据当前时间(第 1 秒)重新计算触发时间。新的触发时间为当前时间(第 1 秒)加上新的超时时间(2 秒),即第 3 秒。所以定时器将在第 4 秒时触发,执行预设的回调函数。
3. 简单示例
内核定时器一般的使用流程如下所示:
static void timer_handler(struct timer_list *t);
DEFINE_TIMER(timer_test, timer_handler);//定义一个定时器变量并指定超时处理函数
static void timer_handler(struct timer_list *t)//定义 function_test 定时功能函数
{
/* 定时器处理代码 */
/* 如果需要定时器周期性运行的话就使用 mod_timer 函数重新设置超时值并且启动定时器。*/
mod_timer(&timer_test, jiffies + msecs_to_jiffies(2000));
}
/* 初始化/打开一个定时器 */
void timer_init(void)
{
timer_test.expires = jffies + msecs_to_jiffies(2000); /* 超时时间 2 秒 */
add_timer(&timer_test); /* 启动定时器 */
}
/* 关闭/删除定时器 */
void timer_destory(void)
{
del_timer(&timer_test); /* 删除定时器 */
/* 或者使用 */
del_timer_sync(&timer_test);
}