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
2
Kernel Features  --->
Timer frequency (100 Hz) --->
找不到怎么办?

反正我用的linux内核刚开始是找不到这个选项的,后来经过一番查探,发现,这种选项依赖于某些变量,我们直接在图形界面搜索 CONFIG_HZ 这个关键词(按下 / ,然后输入名字即可搜索),会看到有么一个结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 这个是表示 HZ的默认值是200
│ Symbol: HZ [=200] │
│ Type : integer │
│ Defined at arch/arm/Kconfig:1678 |


# 后边还有几个与下边这类似的
│ Symbol: HZ_100 [=n] │
│ Type : boolean │
│ Prompt: 100 Hz │
│ Location: │
│ (2) -> Kernel Features │
│ -> Timer frequency (<choice> [=n]) │
│ Defined at arch/arm/Kconfig:1658 │
│ Depends on: <choice> │

发现明明是有这个选项的,但是就是看不到,我们直接来到这个文件,打开:

1
vim arch/arm/Kconfig +1658

发现配置文件中有这么几行(前边是在文件中的行号):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1644 source kernel/Kconfig.preempt
1645
1646 config HZ_FIXED
1647 int
1648 default 200 if ARCH_EBSA110 || ARCH_S3C24XX || ARCH_S5P64X0 || \
1649 ARCH_S5PV210 || ARCH_EXYNOS4
1650 default AT91_TIMER_HZ if ARCH_AT91
1651 default SHMOBILE_TIMER_HZ if ARCH_SHMOBILE_LEGACY
1652 default 0
1653
1654 choice
1655 depends on HZ_FIXED = 0
1656 prompt "Timer frequency"
1657
1658 config HZ_100
1659 bool "100 Hz"

从这里得知,要保证 HZ_FIXED = 0,才会有这些选项,所以后边知道怎么办啦,直接把 config HZ_FIXED下边的 default删掉,只留一个0,然后重新打开图形配置界面,就会发现,这个选项出来了。

【注意事项】这里仅限于查看,看完记得改回来,我并不能保证改了之后会对内核编译的时候造成什么影响。

在Linux 内核会使用 CONFIG_HZ 来设置自己的系统时钟。我们打开 Linux 内核源码的这个文件:

1
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 就是 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
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 __jiffy_data jiffies_64;
extern unsigned long volatile __jiffy_data jiffies;

jiffies_64 和 jiffies 其实是同一个东西, jiffies_64 用于 64 位系统,而 jiffies 用于 32 位系统。为了兼容不同的硬件, jiffies 其实就是 jiffies_64 的低 32 位, jiffies_64 和 jiffies 的结构如下图:

image-20220918154908163

当我们访问 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* These inlines deal with timer wrapping correctly. You are
* strongly encouraged to use them
* 1. Because people otherwise forget
* 2. Because if the timer wrap changes in future you won't have to
* alter your driver code.
*
* time_after(a,b) returns true if the time a is after time b.
*
* Do this with "<0" and ">=0" to only test the sign of the result. A
* good compiler would generate better code (and a really good compiler
* wouldn't care). Gcc is currently neither.
*/
#define time_after(a,b) \
(typecheck(unsigned long, a) && \
typecheck(unsigned long, b) && \
((long)((b) - (a)) < 0))
#define time_before(a,b) time_after(b,a)

#define time_after_eq(a,b) \
(typecheck(unsigned long, a) && \
typecheck(unsigned long, b) && \
((long)((a) - (b)) >= 0))
#define time_before_eq(a,b) time_after_eq(b,a)

如果 a 超过 b 的话, time_after 函数返回真,否则返回假。如果 a 没有超过 b 的话 time_before 函数返回真,否则返回假。time_after_eq 函数和 time_after 函数类似,只是多了判断等于这个条件。同理, time_before_eq 函数和 time_before 函数也类似。

4. 代码执行超时判断

上边了解了jiffies,我们可以用它来判断某段程序有没有执行超时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
unsigned long timeout;
timeout = jiffies + (2 * HZ); /* 超时的时间点,这里是2s */

/*************************************
* 具体的代码
*************************************/

/* 判断有没有超时 */
if(time_before(jiffies, timeout))
{
/* 超时未发生 */
}
else
{
/* 超时发生 */
}

二、延时机制

在linux中,根据延时时间长短可以有短延时和长延时,按照延时方式,可以分为阻塞延时和非阻塞延时(忙等待)。

1. 忙等待的短延迟

1
2
3
1. void ndelay(unsigned long nsecs)
2. void udelay(unsigned long usecs)
3. void mdelay(unsigned long msecs)

实际上是在函数内不进行循环,不会睡眠。

2. 忙等待的长延时

使用 jiffies 相关比较宏来实现,它也是在函数里边进行循环,不会睡眠:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
time_after(a,b)    // a > b
time_before(a,b) // a < b

// 延迟100个 jiffies
unsigned long delay = jiffies + 100;
while(time_before(jiffies,delay))
{
;
}

// 延迟 2s
unsigned long delay = jiffies + 2*HZ;
while(time_before(jiffies,delay))
{
;
}

3. 睡眠延时

这个属于阻塞类延时,开始延时后,进程阻塞,让出CPU。

1
2
3
void msleep(unsigned int msecs);

unsigned long msleep_interruptible(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct timer_list {
/*
* All fields that change during normal runtime grouped to the
* same cacheline
*/
struct list_head entry;
unsigned long expires;
struct tvec_base *base;

void (*function)(unsigned long);
unsigned long data;

int slack;

#ifdef CONFIG_TIMER_STATS
int start_pid;
void *start_site;
char start_comm[16];
#endif
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};

【成员介绍】

  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 需包含的头文件 */
#include <linux/timer.h>

/* 函数声明 */
#define init_timer(timer) \
__init_timer((timer), 0)
// __init_timer的定义
#ifdef CONFIG_LOCKDEP
#define __init_timer(_timer, _flags) \
do { \
static struct lock_class_key __key; \
init_timer_key((_timer), (_flags), #_timer, &__key); \
} while (0)
// ...
#else
#define __init_timer(_timer, _flags) \
init_timer_key((_timer), (_flags), NULL, NULL)
// ...
#endif
// init_timer_key的定义
void init_timer_key(struct timer_list *timer, unsigned int flags,
const char *name, struct lock_class_key *key);

【函数说明】该函数其实是一个宏,用于初始化一个定时器,实际上就是初始化定义好了的 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
2
3
4
5
/* 需包含的头文件 */
#include <linux/timer.h>

/* 函数声明 */
extern void add_timer(struct timer_list *timer);

【函数说明】该函数用于向内核注册一个定时器。

【函数参数】

  • timer:struct timer_list *类型,表示要注册的定时器。

【返回值】none

【使用格式】none

【注意事项】none

3.3 del_timer()

我们使用以下命令查询一下函数所在头文件:

1
grep del_timer -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
/* 需包含的头文件 */
#include <linux/timer.h>

/* 函数声明 */
extern int del_timer(struct timer_list * timer);

【函数说明】该函数用于删除一个定时器,不管定时器有没有被激活,都可以使用此函数删除。

【函数参数】

  • 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
2
3
4
5
/* 需包含的头文件 */
#include <linux/timer.h>

/* 函数声明 */
extern int mod_timer(struct timer_list *timer, unsigned long expires);

【函数说明】该函数用于修改定时器的值,如果定时器还没有激活的话, 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
2
3
4
5
/* 需包含的头文件 */
#include <linux/timer.h>

/* 函数声明 */
extern int del_timer_sync(struct timer_list *timer);

【函数说明】该函数用于删除一个定时器,会等待其他处理器使用完定时器再删除。

【函数参数】

  • timer:struct timer_list * 类型,表示要删除的定时器。

【返回值】int 类型,返回 0 表示定时器未被激活,返回 1 表示定时器已被激活。

【使用格式】none

【注意事项】del_timer_sync 不能使用在中断上下文中。