Skip to content

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”章节。

image-20250207105342816

这个通用定时器是可选的,猜测 Linux 会将这个通用定时器作为 Linux 系统时钟源(前提是 SOC 得选配这个通用定时器)。具体是怎么做的没有深入研究过,这里仅仅是猜测!不过对于我们 Linux 驱动编写来说,不需要深入研究这些具体的实现,只需要掌握相应的 API 函数即可,除非是内核编写者或者内核爱好者。

二、系统节拍率

Linux 内核中有大量的函数需要时间管理,比如周期性的调度程序、延时程序、对于驱动编写者来说最常用的定时器。硬件定时器提供时钟源,时钟源的频率可以设置, 设置好以后就周期性的产生定时中断,系统使用定时中断来计时。中断周期性产生的频率就是系统频率,也叫做节拍率(tick rate)(有的资料也叫系统频率),比如 1000Hz, 100Hz 等等说的就是系统节拍率。系统节拍率是可以设置的,单位是 Hz,我们在编译 Linux 内核的时候可以通过图形化界面设置系统节拍率,我们执行:

shell
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
image-20250207104530662

这里是 4.19.91 版本的内核,其他版本也一样的,按照如下路径打开配置界面:

shell
	Kernel Features --->
		Timer frequency (100Hz) --->
image-20250207104808736

可以看出,可选的系统节拍率为 100Hz、 200Hz、 250Hz、 300Hz、 500Hz 和 1000Hz,默认情况下选择 100Hz。 设置好以后打开 Linux 内核源码根目录下的.config 文件,在此文件中有如下定义:

image-20250207105124318

CONFIG_HZ 为 100, Linux 内核会使用 CONFIG_HZ 来设置自己的系统时钟。我们打开 param.h - include/asm-generic/param.h 文件

c
# 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 文件中会包含这个头文件,不需要重复引用 )中:

c
/*
 * 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 的结构如图

image-20250207112432711

当我们访问 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 函数来处理绕回。

unkown 通常为 jiffies, known 通常是需要对比的值。如果 unkown 超过 known 的话,time_after() 函数返回真,否则返回假。如果 unkown 没有超过 known 的话 time_before() 函数返回真,否则返回假。 time_after_eq() 函数和 time_after() 函数类似,只是多了判断等于这个条件。同理, time_before_eq() 函数和 time_before() 函数也类似。比如我们要判断某段代码执行时间有没有超时,此时就可以使用如下所示代码:

c
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 结构用于表示一个内核定时器:

c
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() 宏可用于定义一个定时器结构体变量,它可以直接指定超时处理函数,但是只是定义了变量,后续还是需要调用一系列的函数对定时器进行初始化。

c
#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 为定时处理函数,可以使用以下代码对定时器和相应的定时处理函数进行定义:

c
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() 函数向内核注册定时器以后,定时器就会开始运行

c
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() 函数删除定时器之前要先等待其他处理器的定时处理器函数退出。

c
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() 不能使用在中断上下文中。

c
#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 函数会激活定时器

c
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. 简单示例

内核定时器一般的使用流程如下所示:

c
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);
}