LV10-05-内核定时器-01-基础知识
本文主要是内核定时器基础知识的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
Windows | windows11 |
Ubuntu | Ubuntu16.04的64位版本 |
VMware® Workstation 16 Pro | 16.2.3 build-19376536 |
SecureCRT | Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日 |
Linux开发板 | 华清远见 底板: FS4412_DEV_V5 核心板: FS4412 V2 |
u-boot | 2013.01 |
点击查看本文参考资料
参考方向 | 参考原文 |
--- | --- |
点击查看相关文件下载
文件 | 下载链接 |
--- | --- |
一、内核时间管理
1. 节拍率是什么?
Linux 内核中有大量的函数需要时间管理,比如周期性的调度程序、延时程序、对于我们驱动编写者来说最常用的定时器。硬件定时器提供时钟源,时钟源的频率可以设置, 设置好以后就周期性的产生定时中断,系统使用定时中断来计时。中断周期性产生的频率就是系统频率,也叫做节拍率(tick rate)(有的地方也叫系统频率),比如 1000Hz, 100Hz 等等说的就是系统节拍率。系统节拍率是可以设置的,单位是 Hz,我们在编译 Linux 内核的时候可以通过图形化界面设置系统节拍率,按照如下路径打开配置界面:
1 | Kernel Features ---> |
找不到怎么办?
反正我用的linux内核刚开始是找不到这个选项的,后来经过一番查探,发现,这种选项依赖于某些变量,我们直接在图形界面搜索 CONFIG_HZ 这个关键词(按下 / ,然后输入名字即可搜索),会看到有么一个结果:
1 | 这个是表示 HZ的默认值是200 |
发现明明是有这个选项的,但是就是看不到,我们直接来到这个文件,打开:
1 | vim arch/arm/Kconfig +1658 |
发现配置文件中有这么几行(前边是在文件中的行号):
1 | 1644 source kernel/Kconfig.preempt |
从这里得知,要保证 HZ_FIXED = 0,才会有这些选项,所以后边知道怎么办啦,直接把 config HZ_FIXED下边的 default删掉,只留一个0,然后重新打开图形配置界面,就会发现,这个选项出来了。
【注意事项】这里仅限于查看,看完记得改回来,我并不能保证改了之后会对内核编译的时候造成什么影响。
在Linux 内核会使用 CONFIG_HZ 来设置自己的系统时钟。我们打开 Linux 内核源码的这个文件:
1 | include/asm-generic/param.h |
会看到有如下宏定义:
1 |
宏 HZ 就是 CONFIG_HZ,我们后面编写 Linux驱动的时候会常常用到 HZ,因为 HZ 表示一秒的节拍数,也就是频率。
2. 节拍率的高低影响
高节拍率会提高系统时间精度,如果采用 100Hz 的节拍率,时间精度就是 10ms,采用1000Hz 的话时间精度就是 1ms,精度提高了10 倍。高精度时钟的好处有很多,对于那些对时间要求严格的函数来说,能够以更高的精度运行,时间测量也更加准确。
高节拍率会导致中断的产生更加频繁,频繁的中断会加剧系统的负担, 1000Hz 和 100Hz的系统节拍率相比,系统要花费 10 倍的“精力”去处理中断。中断服务函数占用处理器的时间增加,但是现在的处理器性能都很强大,所以采用 1000Hz 的系统节拍率并不会增加太大的负载压力。根据自己的实际情况,选择合适的系统节拍率。
3. jiffies
Linux 内核使用全局变量 jiffies 来记录系统从启动以来的系统节拍数,系统启动的时候会将 jiffies 初始化为 0,这个变量定义在 linux内核源码的这个文件中:
1 | include/linux/jiffies.h |
我们打开这个文件,可以看到如下定义:
1 | /* |
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。
前面说 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 函数来处理绕回,这些函数定义在linux内核源码的这个文件中:
1 | include/linux/jiffies.h |
我们打开文件,会看到有如下相关函数:
1 | /* |
如果 a 超过 b 的话, time_after 函数返回真,否则返回假。如果 a 没有超过 b 的话 time_before 函数返回真,否则返回假。time_after_eq 函数和 time_after 函数类似,只是多了判断等于这个条件。同理, time_before_eq 函数和 time_before 函数也类似。
4. 代码执行超时判断
上边了解了jiffies
,我们可以用它来判断某段程序有没有执行超时:
1 | unsigned long timeout; |
二、延时机制
在linux中,根据延时时间长短可以有短延时和长延时,按照延时方式,可以分为阻塞延时和非阻塞延时(忙等待)。
1. 忙等待的短延迟
1 | 1. void ndelay(unsigned long nsecs) |
实际上是在函数内不进行循环,不会睡眠。
2. 忙等待的长延时
使用 jiffies 相关比较宏来实现,它也是在函数里边进行循环,不会睡眠:
1 | time_after(a,b) // a > b |
3. 睡眠延时
这个属于阻塞类延时,开始延时后,进程阻塞,让出CPU。
1 | void msleep(unsigned int msecs); |
4. 延时机制的选择
4.1 执行流
执行流:有开始有结束总体顺序执行的一段独立代码,又被称为代码上下文 。计算机系统中的执行流的分类:
- 任务流——任务上下文(都参与CPU时间片轮转,都有任务的五种状态:就绪态 运行态 睡眠态 僵死态 暂停态)
(1)进程
(2)线程
内核线程:内核创建的线程。
应用线程:应用进程创建的线程。
- 异常流——异常上下文
(1)中断
(2)其他异常
应用编程可能涉及到的执行流:
(1)进程
(2)线程
内核编程可能涉及到的执行流:
(1)应用程序自身代码运行在用户空间,处于用户态—— 用户态 app
(2)应用程序正在调用系统调用函数,运行在内核空间,处于内核态,即代码是内核代码但处于应用执行流(即属于一个应用进程或应用线程)——内核态 app
(3)一直运行于内核空间,处于内核态,属于内核内的任务上下文—— 内核线程
(4)一直运行于内核空间,处于内核态,专门用来处理各种异常—— 异常上下文
4.2 延时的选择
延时机制的选择原则:
任务上下文短延迟采用忙等待类,长延迟采用阻塞类
异常上下文中只能采用忙等待类
三、内核定时器
1. 内核定时器简介
定时器是一个很常用的功能,需要周期性处理的工作都要用到定时器, Linux 内核采用系统时钟来实现了一个定时器。 Linux 内核定时器使用很简单,只需要提供超时时间(相当于定时值)和定时处理函数即可,当超时时间到了以后设置的定时处理函数就会执行,和我们使用硬件定时器的套路一样,只是使用内核定时器不需要做一大堆的寄存器初始化工作。
在使用内核定时器的时候要注意一点,内核定时器并不是周期性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函数中重新开启定时器。
2. 一个结构体
Linux 内核使用 timer_list 结构体表示内核定时器 ,这个结构体定义在linux内核源码的这个文件中:
1 | include/linux/timer.h |
我们打开这个文件,会看到结构体定义如下:
1 | struct timer_list { |
【成员介绍】
- expires:unsigned long 类型,定时器的超时时间,单位是节拍数,也就是 jiffies + x * HZ,其中 x 就表示我们要定时的秒数。
- function:函数指针,时间到达后,执行的回调函数,这是一个软中断异常上下文。
- data:unsigned long 类型,表示要传递给回调函数的参数。
3. 相关函数
3.1 init_timer()
我们使用以下命令查询一下函数所在头文件:
1 | grep init_timer -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数其实是一个宏,用于初始化一个定时器,实际上就是初始化定义好了的 timer_list 变量 。
【函数参数】
- timer:struct timer_list *类型,表示要初始化的定时器。
【返回值】none
【使用格式】none
【注意事项】none
3.2 add_timer()
我们使用以下命令查询一下函数所在头文件:
1 | grep add_timer -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于向内核注册一个定时器。
【函数参数】
- timer:struct timer_list *类型,表示要注册的定时器。
【返回值】none
【使用格式】none
【注意事项】none
3.3 del_timer()
我们使用以下命令查询一下函数所在头文件:
1 | grep del_timer -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于删除一个定时器,不管定时器有没有被激活,都可以使用此函数删除。
【函数参数】
- timer:struct timer_list *类型,表示要删除的定时器。
【返回值】int 类型,返回0表示定时器还没被激活,返回 1表示定时器已经激活。
【使用格式】none
【注意事项】在多处理器系统上,定时器可能会在其他的处理器上运行,因此在调用 del_timer 函数删除定时器之前要先等待其他处理器的定时处理器函数退出。
3.4 mod_timer()
我们使用以下命令查询一下函数所在头文件:
1 | grep mod_timer -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于修改定时器的值,如果定时器还没有激活的话, mod_timer 函数会激活定时器 。
【函数参数】
- timer:struct timer_list * 类型,表示要修改的定时器。
- expires:unsigned long 类型,修改后的超时时间,单位为节拍数,格式为 jiffies + x * HZ。
【返回值】int 类型,返回0表示调用 mod_timer 函数前定时器未被激活,返回1表示调用 mod_timer 函数前定时器已被激活。
【使用格式】none
【注意事项】none
3.5 del_timer_sync()
我们使用以下命令查询一下函数所在头文件:
1 | grep del_timer_sync -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于删除一个定时器,会等待其他处理器使用完定时器再删除。
【函数参数】
- timer:struct timer_list * 类型,表示要删除的定时器。
【返回值】int 类型,返回 0 表示定时器未被激活,返回 1 表示定时器已被激活。
【使用格式】none
【注意事项】del_timer_sync 不能使用在中断上下文中。