Linux 内核定时器实验

Linux 内核定时器实验Linux时间管理和内核定时器简介50.1.1内核时间管理简介学习过UCOS或FreeRTOS的同学应该知道,UCOS或FreeRTOS是需要一个硬件定时器提供系统时钟,一般使用Systick作为系统时钟源。同理,Linux要运行,也是需要一个系统时钟的,至于这个系统时钟是由哪个定时器提供的,笔者没有去研究过Linux内核。Linux内核中有大量的函数需要时间管理,比如周期性的调度程序、延时程序、对于我们驱动编写者来说最常用的定时器。硬件定时器提

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

Linux 时间管理和内核定时器简介

50.1.1 内核时间管理简介
    学习过 UCOS 或 FreeRTOS 的同学应该知道, UCOS 或 FreeRTOS 是需要一个硬件定时器
提供系统时钟,一般使用 Systick 作为系统时钟源。同理, Linux 要运行,也是需要一个系统时
钟的,至于这个系统时钟是由哪个定时器提供的,笔者没有去研究过 Linux 内核。
    Linux 内核中有大量的函数需要时间管理,比如周期性的调度程序、延时程序、对于我们驱
动编写者来说最常用的定时器。硬件定时器提供时钟源,时钟源的频率可以设置, 设置好以后
就周期性的产生定时中断,系统使用定时中断来计时。中断周期性产生的频率就是系统频率,
也叫做节拍率(tick rate)(有的资料也叫系统频率),比如 1000Hz, 100Hz 等等说的就是系统节拍
率。系统节拍率是可以设置的,单位是 Hz,我们在编译 Linux 内核的时候可以通过图形化界面
设置系统节拍率,按照如下路径打开配置界面:
-> Kernel Features
-> Timer frequency (<choice> [=y])
选中“ Timer frequency”
可选的系统节拍率为 100Hz、 200Hz、 250Hz、 300Hz、 500Hz 和1000Hz,默认情况下选择 100Hz。设置好以后打开 Linux 内核源码根目录下的.config 文件,在此文件中 CONFIG_HZ 为 100, Linux 内核会使用 CONFIG_HZ 来设置自己的系统时
钟。打开文件 include/asm-generic/param.h,有如下内容:
示例代码 50.1.1.1 include/asm-generic/param.h 文件代码段
6 # undef HZ
7 # define HZ CONFIG_HZ
8 # define USER_HZ 100
9 # define CLOCKS_PER_SEC (USER_HZ)7 行定义了一个宏 HZ,宏 HZ 就是 CONFIG_HZ,因此 HZ=100,我们后面编写 Linux
驱动的时候会常常用到 HZ,因为 HZ 表示一秒的节拍数,也就是频率。
    大多数初学者看到系统节拍率默认为 100Hz 的时候都会有疑问,怎么这么小? 100Hz 是可
选的节拍率里面最小的。为什么不选择大一点的呢?这里就引出了一个问题:高节拍率和低节
拍率的优缺点:
①、高节拍率会提高系统时间精度,如果采用 100Hz 的节拍率,时间精度就是 10ms,采用
1000Hz 的话时间精度就是 1ms,精度提高了 10 倍。高精度时钟的好处有很多,对于那些对时
间要求严格的函数来说,能够以更高的精度运行,时间测量也更加准确。
②、高节拍率会导致中断的产生更加频繁,频繁的中断会加剧系统的负担, 1000Hz 和 100Hz
的系统节拍率相比,系统要花费 10 倍的“精力”去处理中断。中断服务函数占用处理器的时间
增加,但是现在的处理器性能都很强大,所以采用 1000Hz 的系统节拍率并不会增加太大的负
载压力。根据自己的实际情况,选择合适的系统节拍率,本教程我们全部采用默认的 100Hz 系
统节拍率。
    Linux 内核使用全局变量 jiffies 来记录系统从启动以来的系统节拍数,系统启动的时候会
将 jiffies 初始化为 0, jiffies 定义在文件 include/linux/jiffies.h 中,定义如下:
示例代码 50.1.1.2 include/jiffies.h 文件代码段
76 extern u64 __jiffy_data jiffies_64;
77 extern unsigned long volatile __jiffy_data jiffies;76 行,定义了一个 64 位的 jiffies_64。
第 77 行,定义了一个 unsigned long 类型的 32 位的 jiffies。
jiffies_64 和 jiffies 其实是同一个东西, jiffies_64 用于 64 位系统,而 jiffies 用于 32 位系统。
为了兼容不同的硬件, jiffies 其实就是 jiffies_64 的低 32.
    当我们访问 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 内核提供了如表 50.1.1.1 所示的几个 API 函数来处理绕回。
函数                                       描述
time_after(unkown, known)
time_before(unkown, known) unkown 通常为 jiffies, known 通常是需要对比的值。
time_after_eq(unkown, known)
time_before_eq(unkown, known)50.1.1.1 处理绕回的 API 函数
    如果 unkown 超过 known 的话, time_after 函数返回真,否则返回假。如果 unkown 没有超
过 known 的话 time_before 函数返回真,否则返回假。time_after_eq 函数和 time_after 函数类似,
只是多了判断等于这个条件。同理, time_before_eq 函数和 time_before 函数也类似。比如我们
要判断某段代码执行时间有没有超时,此时就可以使用如下所示代码:
示例代码 50.1.1.3 使用 jiffies 判断超时
1 unsigned long timeout;
2 timeout = jiffies + (2 * HZ); /* 超时的时间点 */
4 /************************************* 5 具体的代码 6 ************************************/
/* 判断有没有超时 */
9 if(time_before(jiffies, timeout)) { 
   
1   0 /* 超时未发生 */
11 } else { 
   
    12 /* 超时发生 */
13 }
    timeout 就是超时时间点,比如我们要判断代码执行时间是不是超过了 2 秒,那么超时时间
点就是 jiffies+(2*HZ),如果 jiffies 大于 timeout 那就表示超时了,否则就是没有超时。
    为了方便开发, Linux 内核提供了几个 jiffies 和 ms、 us、 ns 之间的转换函数,如表 50.1.1.2
所示:
函数                                        描述
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)50.1.1.2 jiffies 和 ms、 us、 ns 之间的转换函数

50.1.2 内核定时器简介
    定时器是一个很常用的功能,需要周期性处理的工作都要用到定时器。 Linux 内核定时器
采用系统时钟来实现,并不是我们在裸机篇中讲解的 PIT 等硬件定时器。 Linux 内核定时器使
用很简单,只需要提供超时时间(相当于定时值)和定时处理函数即可,当超时时间到了以后设
置的定时处理函数就会执行,和我们使用硬件定时器的套路一样,只是使用内核定时器不需要
做一大堆的寄存器初始化工作。在使用内核定时器的时候要注意一点,内核定时器并不是周期
性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函
数中重新开启定时器。 Linux 内核使用 timer_list 结构体表示内核定时器, timer_list 定义在文件
include/linux/timer.h 中,定义如下(省略掉条件编译):
示例代码 50.1.2.1 timer_list 结构体
struct timer_list { 
   
    struct list_head entry;
    unsigned long expires; /* 定时器超时时间,单位是节拍数 */
    struct tvec_base *base;
    void (*function)(unsigned long); /* 定时处理函数 */
    unsigned long data; /* 要传递给 function 函数的参数 */
    int slack;
};
    要使用内核定时器首先要先定义一个 timer_list 变量,表示定时器, tiemr_list 结构体的
expires 成员变量表示超时时间,单位为节拍数。比如我们现在需要定义一个周期为 2 秒的定时
器,那么这个定时器的超时时间就是 jiffies+(2*HZ),因此 expires=jiffies+(2*HZ)。 function 就是
定时器超时以后的定时处理函数,我们要做的工作就放到这个函数里面,需要我们编写这个定
时处理函数。
    定义好定时器以后还需要通过一系列的 API 函数来初始化此定时器,这些函数如下:
1、 init_timer 函数
init_timer 函数负责初始化 timer_list 类型变量,当我们定义了一个 timer_list 变量以后一定
要先用 init_timer 初始化一下。 init_timer 函数原型如下:
void init_timer(struct timer_list *timer)
函数参数和返回值含义如下:
timer:要初始化定时器。
返回值: 没有返回值。
2、 add_timer 函数
add_timer 函数用于向 Linux 内核注册定时器,使用 add_timer 函数向内核注册定时器以后,
定时器就会开始运行,函数原型如下:
void add_timer(struct timer_list *timer)
函数参数和返回值含义如下:
timer:要注册的定时器。
返回值: 没有返回值。
3、 del_timer 函数
del_timer 函数用于删除一个定时器,不管定时器有没有被激活,都可以使用此函数删除。
在多处理器系统上,定时器可能会在其他的处理器上运行,因此在调用 del_timer 函数删除定时
器之前要先等待其他处理器的定时处理器函数退出。 del_timer 函数原型如下:
int del_timer(struct timer_list * timer)
函数参数和返回值含义如下:
timer:要删除的定时器。
返回值: 0,定时器还没被激活; 1,定时器已经激活。
4、 del_timer_sync 函数
del_timer_sync 函数是 del_timer 函数的同步版,会等待其他处理器使用完定时器再删除,
del_timer_sync 不能使用在中断上下文中。 del_timer_sync 函数原型如下所示:
int del_timer_sync(struct timer_list *timer)
函数参数和返回值含义如下:
timer:要删除的定时器。
返回值: 0,定时器还没被激活; 1,定时器已经激活。
5、 mod_timer 函数
mod_timer 函数用于修改定时值,如果定时器还没有激活的话, mod_timer 函数会激活定时
器!函数原型如下:
int mod_timer(struct timer_list *timer, unsigned long expires)
函数参数和返回值含义如下:
timer:要修改超时时间(定时值)的定时器。
expires:修改后的超时时间。
返回值: 0,调用 mod_timer 函数前定时器未被激活; 1,调用 mod_timer 函数前定时器已
被激活。
关于内核定时器常用的 API 函数就讲这些,内核定时器一般的使用流程如下所示:
示例代码 50.1.2.2 内核定时器使用方法演示
1 struct timer_list timer; /* 定义定时器 */
2 3
/* 定时器回调函数 */
4 void function(unsigned long arg)
5 { 
   
    6 /* 7 * 定时器处理代码 8 */
    9
    10 /* 如果需要定时器周期性运行的话就使用 mod_timer 11 * 函数重新设置超时值并且启动定时器。 12 */
    13 mod_timer(&dev->timertest, jiffies + msecs_to_jiffies(2000));
14 }
15
16 /* 初始化函数 */
17 void init(void)
18 { 
   
    19 init_timer(&timer); /* 初始化定时器 */
    20
    21 timer.function = function; /* 设置定时处理函数 */
    22 timer.expires=jffies + msecs_to_jiffies(2000);/* 超时时间 2 秒 */
    23 timer.data = (unsigned long)&dev; /* 将设备结构体作为参数 */
    24
    25 add_timer(&timer); /* 启动定时器 */
26 }
27
28 /* 退出函数 */
29 void exit(void)
30 { 
   
    31 del_timer(&timer); /* 删除定时器 */
    32 /* 或者使用 */
    33 del_timer_sync(&timer);
34 }

50.1.3 Linux 内核短延时函数
有时候我们需要在内核中实现短延时,尤其是在 Linux 驱动中。 Linux 内核提供了毫秒、微
秒和纳秒延时函数,这三个函数如表 50.1.3.1 所示:
函数 描述
void ndelay(unsigned long nsecs)
void udelay(unsigned long usecs) 纳秒、微秒和毫秒延时函数。
void mdelay(unsigned long mseces)50.1.3.1 内核短延时函数

50.3 实验程序编写

本实验对应的例程路径为: 开发板光盘-> 2、 Linux 驱动例程-> 12_timer。
本章实验我们使用内核定时器周期性的点亮和熄灭开发板上的 LED 灯, LED 灯的闪烁周
期由内核定时器来设置,测试应用程序可以控制内核定时器周期。
50.3.1 修改设备树文件
本章实验使用到了 LED 灯, LED 灯的设备树节点信息使用 45.4.1 小节创建的即可。

50.3.2 定时器驱动程序编写
新建名为“ 12_timer”的文件夹,然后在 12_timer 文件夹里面创建 vscode 工程,工作区命
名为“ timer”。工程创建好以后新建 timer.c 文件.38~50 行,定时器设备结构体,在 48 行定义了一个定时器成员变量 timer。
第 60~82 行, LED 灯初始化函数,从设备树中获取 LED 灯信息,然后初始化相应的 IO。
第 91~102 行,函数 timer_open,对应应用程序的 open 函数,应用程序调用 open 函数打开
/dev/timer 驱动文件的时候此函数就会执行。此函数设置文件私有数据为 timerdev,并且初始化
定时周期默认为 1 秒,最后调用 led_init 函数初始化 LED 所使用的 IO。
第 111~137 行,函数 timer_unlocked_ioctl,对应应用程序的 ioctl 函数,应用程序调用 ioctl
函数向驱动发送控制信息,此函数响应并执行。此函数有三个参数: filp, cmd 和 arg,其中 filp
是对应的设备文件, cmd 是应用程序发送过来的命令信息, arg 是应用程序发送过来的参数,在
本章例程中 arg 参数表示定时周期。
一共有三种命令 CLOSE_CMD, OPEN_CMD 和 SETPERIOD_CMD,这三个命令分别为关
闭定时器、打开定时器、设置定时周期。这三个命令的左右如下:
CLOSE_CMD: 关闭定时器命令, 调用 del_timer_sync 函数关闭定时器。
OPEN_CMD:打开定时器命令,调用 mod_timer 函数打开定时器,定时周期为 timerdev 的
timeperiod 成员变量,定时周期默认是 1 秒。
SETPERIOD_CMD:设置定时器周期命令,参数 arg 就是新的定时周期,设置 timerdev 的
timeperiod 成员变量为 arg 所表示定时周期指。并且使用 mod_timer 重新打开定时器,使定时器
以新的周期运行。
第 140~144 行,定时器驱动操作函数集 timer_fops。
第 147~162 行,函数 timer_function,定时器服务函数,此函有一个参数 arg,在本例程中
arg 参数就是 timerdev 的地址,这样通过 arg 参数就可以访问到设备结构体。当定时周期到了以
后此函数就会被调用。在此函数中将 LED 灯的状态取反,实现 LED 灯闪烁的效果。因为内核
定时器不是循环的定时器,执行一次以后就结束了,因此在 161 行又调用了 mod_timer 函数重
新开启定时器。
第 169~209 行,函数 timer_init,驱动入口函数。在第 205~207 行初始化定时器,设置定时
器的定时处理函数为 timer_function,另外设置要传递给 timer_function 函数的参数为 timerdev。
在此函数中并没有调用 timer_add 函数来开启定时器,因此定时器默认是关闭的,除非应用程序
发送打开命令。
第 216~231 行,驱动出口函数,在 219 行关闭 LED,也就是卸载驱动以后 LED 处于熄灭
状态。第 220 行调用 del_timer_sync 函数删除定时器,也可以使用 del_timer 函数。

50.3.3 编写测试 APP
测试 APP 我们要实现的内容如下:
①、运行 APP 以后提示我们输入要测试的命令,输入 1 表示关闭定时器、输入 2 表示打开
定时器,输入 3 设置定时器周期。
②、如果要设置定时器周期的话,需要让用户输入要设置的周期值,单位为毫秒。
新建名为 timerApp.c 的文件.22~24 行,命令值。
第 53~73 行, while(1)循环,让用户输入要测试的命令,然后通过第 72 行的 ioctl 函数发送
给驱动程序。如果是设置定时器周期命令 SETPERIOD_CMD,那么 ioctl 函数的 arg 参数就是用
户输入的周期值。

50.4 运行测试

50.4.1 编译驱动程序和测试 APP
1、编译驱动程序
编写 Makefile 文件,本章实验的 Makefile 文件和第四十章实验基本一样,只是将 obj-m 变
量的值改为 timer.o, Makefile 内容如下所示:
示例代码 50.4.1.1 Makefile 文件
1 KERNELDIR := /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imxrel_imx_4.1.15_2.1.0_ga_alientek
......
4 obj-m := timer.o
......
11 clean:
12 $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
第 4 行,设置 obj-m 变量的值为 timer.o。
输入如下命令编译出驱动模块文件:
make -j32
编译成功以后就会生成一个名为“ timer.ko”的驱动模块文件。
2、编译测试 APP
输入如下命令编译测试 timerApp.c 这个测试程序:
arm-linux-gnueabihf-gcc timerApp.c -o timerApp
编译成功以后就会生成 timerApp 这个应用程序。

50.4.2 运行测试
将上一小节编译出来的 timer.ko 和 timerApp 这两个文件拷贝到 rootfs/lib/modules/4.1.15 目
录中,重启开发板,进入到目录 lib/modules/4.1.15 中,输入如下命令加载 timer.ko 驱动模块:
depmod //第一次加载驱动的时候需要运行此命令
modprobe timer.ko //加载驱动
驱动加载成功以后如下命令来测试:
./timerApp /dev/timer
输入上述命令以后终端提示输入命令
输入“ 2”,打开定时器,此时 LED 灯就会以默认的 1 秒周期开始闪烁。在输入“ 3”来设
置定时周期,根据提示输入要设置的周期值.
输入“ 500”,表示设置定时器周期值为 500ms,设置好以后 LED 灯就会以 500ms 为间隔,
开始闪烁。最后可以通过输入“ 1”来关闭定时器,如果要卸载驱动的话输入如下命令即可:
rmmod timer.ko

参考文献

【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.3.pdf

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/158894.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • XPS文件转换成PDF[通俗易懂]

    XPS文件转换成PDF[通俗易懂]作者:iamlaosongXPS是XMLPaperSpecification(XML文件规格书)的简称是一种电子文件格式,它是微软公司开发的一种文档保存与查看的规范。同事有一个XPS文件,希望能够转换成PDF格式。方法一:电脑上安装了AdobeAcrobat8Professional1、双击打开AdobeAcrobat8Professional。2、点击“打开”,选择要转换的XPS文件。3、不出意外的话XPS文件可以正常打开并看到内容。4、另存为PDF文件即可。电脑上有其他版.

  • idea2022.01 激活码【中文破解版】2022.03.07

    (idea2022.01 激活码)JetBrains旗下有多款编译器工具(如:IntelliJ、WebStorm、PyCharm等)在各编程领域几乎都占据了垄断地位。建立在开源IntelliJ平台之上,过去15年以来,JetBrains一直在不断发展和完善这个平台。这个平台可以针对您的开发工作流进行微调并且能够提供…

  • python3回文数

    python3回文数

  • nrm使用报错_重大错报

    nrm使用报错_重大错报nrm使用错误:ERR_INVALID_ARG_TYPE

    2022年10月28日
  • 关联数据入门——RDF

    关联数据入门——RDF本文是语义网的入门级读本,试图描述一些语义网基本知识……

  • Intellij IDEA第一个java applet程序

    Intellij IDEA第一个java applet程序建好项目之后配置configuration目录结构如下game.htmlmyGame.java运行后显示试着编译myGame.java还是没用这里顺带提一下如何编译myGame.javacmd进入src文件夹,执行命令javacmyGame.java即可在同文件夹生成myGame.class文件如果提示javac不是外部或内部指令按下篇文章进行设置https://jingyan.baidu.com/article/08b6a591bdb18314a80922a0.html回

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号