LV06-08-linux定时器-01-内核定时器

什么是内核定时器?怎么使用?若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04
linux内核 linux-4.19.71(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
nxp-imx/linux-imx/releases/tag/v4.19.71 NXP linux内核仓库tags中的v4.19.71
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
Source Code https://elixir.bootlin.com/linux/latest/source linux kernel源码
kernel/git/stable/linux.git - Linux kernel stable tree linux kernel源码(官网,tag 4.19.71)
https://elixir.bootlin.com/u-boot/latest/source uboot源码

一、内核定时器?

1. 内核时间管理

过 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 函数即可,除非是内核编写者或者内核爱好者。

2. 系统节拍率

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

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

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

1
2
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 文件

1
2
3
4
# 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 系统节拍率。

3. jiffies

3.1 jiffies是什么?

Linux 内核使用全局变量 jiffies 来记录系统从启动以来的系统节拍数,系统启动的时候会将 jiffies 初始化为 0, jiffies 定义在文件 jiffies.h - include/linux/jiffies.h (timer.h 文件中会包含这个头文件,不需要重复引用 )中:

1
2
3
4
5
6
7
/*
* 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。

3.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() 函数也类似。比如我们要判断某段代码执行时间有没有超时,此时就可以使用如下所示代码:

1
2
3
4
5
6
7
8
9
10
11
12
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,如果小于的话就表示没有超时。

3.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)

4. 内核定时器简介

硬件为内核提供了一个系统定时器来计算流逝的时间(即基于未来时间点的计时方式, 以当前时刻为计时开始的起点, 以未来的某一时刻为计时的终点) ,内核只有在系统定时器的帮助下才能计算和管理时间, 但是内核定时器的精度并不高, 所以不能作为高精度定时器使用。并且

内核定时器的运行没有周期性, 到达计时终点后会自动关闭。 如果要实现周期性定时, 就要在定时处理函数中重新开启定时器。

4.1 struct timer_list

内核中 struct timer_list 结构用于表示一个内核定时器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 就是定时器超时以后的定时处理函数,我们要做的工作就放到这个函数里面,需要我们编写这个定时处理函数。

4.2 相关API

4.2.1 DEFINE_TIMER()

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

1
2
3
4
5
6
7
8
9
10
11
12
#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 为定时处理函数,可以使用以下代码对定时器和相应的定时处理函数进行定义:

1
2
3
4
5
static void timer_handler(struct timer_list *t)//定义 function_test 定时功能函数
{
//......
}
DEFINE_TIMER(timer_test, timer_handler);//定义一个定时器

4.2.2 add_timer()

add_timer() 用于向 Linux 内核注册定时器,使用 add_timer() 函数向内核注册定时器以后,定时器就会开始运行

1
2
3
4
5
6
void add_timer(struct timer_list *timer)
{
BUG_ON(timer_pending(timer));
mod_timer(timer, timer->expires);
}
EXPORT_SYMBOL(add_timer);

函数参数:

  • timer:要注册的定时器。

返回值:

4.2.3 del_timer()

del_timer() 函数用于删除一个定时器,不管定时器有没有被激活,都可以使用此函数删除。在多处理器系统上,定时器可能会在其他的处理器上运行,因此在调用 del_timer() 函数删除定时器之前要先等待其他处理器的定时处理器函数退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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,定时器已经激活。

4.2.4 del_timer_sync()

del_timer_sync() 函数是 del_timer() 函数的同步版,会等待其他处理器使用完定时器再删除, del_timer_sync() 不能使用在中断上下文中。

1
2
3
4
5
#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,定时器已经激活。

4.2.5 mod_timer()

mod_timer() 函数用于修改定时值,如果定时器还没有激活的话, mod_timer 函数会激活定时器

1
2
3
4
5
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秒时触发,执行预设的回调函数。

4.3 简单示例

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

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

二、demo实现

1. 定时器demo

1.1 demo源码

09_kernel_timer/01_timer_basic · 苏木/imx6ull-driver-demo - 码云 - 开源中国

1.2 开发板验证

这个基本的定时器测试demo就是在驱动中直接打开的,我们使用以下命令加载驱动就会启动定时器:

1
insmod sdriver_demo.ko

然后会看到以下打印信息:

image-20250208095416610

可以看到超时时间是5s,基本上看内核日志的时间戳就可以知道每次打印间隔就是5s。

2. app控制定时器

app怎么控制定时器?我们可以通过ioctl方法来实现,我们可以定义三个命令:

1
2
3
#define CMD_TIMER_SETPERIOD   (_IOW('T', 100, int)) /* 设置定时器周期命令 */
#define CMD_TIMER_OPEN (_IO('T', 101)) /* 打开内核定时器 */
#define CMD_TIMER_CLOSE (_IO('T', 102)) /* 关闭内核定时器 */

2.1 demo源码

09_kernel_timer/02_timer_ctl · 苏木/imx6ull-driver-demo - 码云 - 开源中国

2.2 开发板验证

我们执行以下命令,来验证效果:

1
./app_demo.out /dev/sdevchr 4

然后们可以看到以下打印信息:

image-20250208101539520

可以看到打开定时器后,默认超时时间为5s,之后改为2秒超时,每次打印间隔变成了2s。

3. 秒设备

我们来通过定时器定义一个秒设备。这个直接看demo吧:

09_kernel_timer/03_timer_dev · 苏木/imx6ull-driver-demo - 码云 - 开源中国

其实就是定义一个变量,再定义一个定时器,每秒让变量自增,应用再来读取。