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

这个通用定时器是可选的,猜测 Linux 会将这个通用定时器作为 Linux 系统时钟源(前提是 SOC 得选配这个通用定时器)。具体是怎么做的没有深入研究过,这里仅仅是猜测!不过对于我们 Linux 驱动编写来说,不需要深入研究这些具体的实现,只需要掌握相应的 API 函数即可,除非是内核编写者或者内核爱好者。
2. 系统节拍率
Linux 内核中有大量的函数需要时间管理,比如周期性的调度程序、延时程序、对于驱动编写者来说最常用的定时器。硬件定时器提供时钟源,时钟源的频率可以设置, 设置好以后就周期性的产生定时中断,系统使用定时中断来计时。中断周期性产生的频率就是系统频率,也叫做节拍率(tick rate)(有的资料也叫系统频率),比如 1000Hz, 100Hz 等等说的就是系统节拍率。系统节拍率是可以设置的,单位是 Hz,我们在编译 Linux 内核的时候可以通过图形化界面设置系统节拍率,我们执行:
1 | make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig |

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

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

CONFIG_HZ 为 100, Linux 内核会使用 CONFIG_HZ 来设置自己的系统时钟。我们打开 param.h - include/asm-generic/param.h 文件
1 |
这里定义了一个宏 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 | /* |
第 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。
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 函数来处理绕回。
- 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() 函数也类似。比如我们要判断某段代码执行时间有没有超时,此时就可以使用如下所示代码:
1 | unsigned long timeout; |
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 | struct timer_list { |
要使用内核定时器首先要先定义一个 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 |
_name 为定义的结构体名称, _function 为定时处理函数,可以使用以下代码对定时器和相应的定时处理函数进行定义:
1 | static void timer_handler(struct timer_list *t)//定义 function_test 定时功能函数 |
4.2.2 add_timer()
add_timer() 用于向 Linux 内核注册定时器,使用 add_timer() 函数向内核注册定时器以后,定时器就会开始运行
1 | void add_timer(struct timer_list *timer) |
函数参数:
- timer:要注册的定时器。
返回值:无
4.2.3 del_timer()
del_timer() 函数用于删除一个定时器,不管定时器有没有被激活,都可以使用此函数删除。在多处理器系统上,定时器可能会在其他的处理器上运行,因此在调用 del_timer() 函数删除定时器之前要先等待其他处理器的定时处理器函数退出。
1 | int del_timer(struct timer_list *timer) |
函数参数:
- timer:要删除的定时器。
返回值:0,定时器还没被激活; 1,定时器已经激活。
4.2.4 del_timer_sync()
del_timer_sync() 函数是 del_timer() 函数的同步版,会等待其他处理器使用完定时器再删除, del_timer_sync() 不能使用在中断上下文中。
1 |
|
函数参数:
- timer:要删除的定时器。
返回值:0,定时器还没被激活; 1,定时器已经激活。
4.2.5 mod_timer()
mod_timer() 函数用于修改定时值,如果定时器还没有激活的话, mod_timer 函数会激活定时器
1 | int mod_timer(struct timer_list *timer, unsigned long expires) |
函数参数:
- timer:要修改超时时间(定时值)的定时器。
- expires:修改后的超时时间。
返回值:0,调用 mod_timer 函数前定时器未被激活; 1,调用 mod_timer 函数前定时器已被激活。
Tips:
若是最开始定时器的超时时间为5s,现在在第1s,这个时候修改了定时器的超时时间为2s,此时,定时器会根据当前时间(第1秒)重新计算触发时间。新的触发时间为当前时间(第1秒)加上新的超时时间(2秒),即第3秒。所以定时器将在第4秒时触发,执行预设的回调函数。
4.3 简单示例
内核定时器一般的使用流程如下所示:
1 | static void timer_handler(struct timer_list *t); |
二、demo实现
1. 定时器demo
1.1 demo源码
09_kernel_timer/01_timer_basic · 苏木/imx6ull-driver-demo - 码云 - 开源中国
1.2 开发板验证
这个基本的定时器测试demo就是在驱动中直接打开的,我们使用以下命令加载驱动就会启动定时器:
1 | insmod sdriver_demo.ko |
然后会看到以下打印信息:

可以看到超时时间是5s,基本上看内核日志的时间戳就可以知道每次打印间隔就是5s。
2. app控制定时器
app怎么控制定时器?我们可以通过ioctl方法来实现,我们可以定义三个命令:
1 |
2.1 demo源码
09_kernel_timer/02_timer_ctl · 苏木/imx6ull-driver-demo - 码云 - 开源中国
2.2 开发板验证
我们执行以下命令,来验证效果:
1 | ./app_demo.out /dev/sdevchr 4 |
然后们可以看到以下打印信息:

可以看到打开定时器后,默认超时时间为5s,之后改为2秒超时,每次打印间隔变成了2s。
3. 秒设备
我们来通过定时器定义一个秒设备。这个直接看demo吧:
09_kernel_timer/03_timer_dev · 苏木/imx6ull-driver-demo - 码云 - 开源中国
其实就是定义一个变量,再定义一个定时器,每秒让变量自增,应用再来读取。