Linux内核驱动定时器

一、基本简介

         Linux要运行,就需要一个系统时钟(也叫滴答时钟),系统利用这个时钟实现进程的轮询调度、进程定时休眠等工作。系统时钟是一个软件时钟,它通常基于ARM内核的systick作为硬件基础时钟源而来;硬件定时器提供时钟源,时钟源的频率可以设置, 设置好以后就周期性的产生定时中断,系统使用定时中断来计时。中断周期性产生的频率就是系统频率,也叫做节拍率(tick rate)(有的资料也叫系统频率)。

         Linux的系统节拍率是可以配置的,在编译内核时通过图形化配置界面可以设置,目前最大支持1000Hz,默认为100Hz。配置路径如下:(Kernel Features->Timer frequency)

在Linux内核源码当中(include/asm-generic/param.h),Linux会使用配置的目标频率作为系统内核的基础时钟节拍率:

注意:从截图的代码中来看,第8行中有一个独立的"USER_HZ"宏定义,经过相关资料的查阅,这个是LINUX 2.6之后的版本才有的,目的是为了兼容旧版的一些应用程序,保证应用程序的可移植性。通常不建议修改它,应保持其默认值。

        大多数初学者看到系统节拍率默认为 100Hz 的时候都会有疑问,怎么这么小?100Hz 是可选的节拍率里面最小的。为什么不选择大一点的呢?这里就引出了一个问题,高节拍率和低节拍率的优缺点

① 高节拍率会提高系统时间精度,如果采用 100Hz 的节拍率,时间精度就是 10ms,采用1000Hz 的话时间精度就是 1ms,精度提高了 10 倍。高精度时钟的好处有很多,对于那些对时间要求严格的函数来说,能够以更高的精度运行,时间测量也更加准确。

② 高节拍率会导致中断的产生更加频繁,频繁的中断会加剧系统的负担,1000Hz 和 100Hz的系统节拍率相比,系统要花费 10 倍的“精力”去处理中断。中断服务函数占用处理器的时间增加,但是现在的处理器性能都很强大,所以采用 1000Hz 的系统节拍率并不会增加太大的负载压力。

/

二、Linux系统时钟(系统节拍器

1、基本介绍

       Linux内核使用全局变量 jiffies 来记录系统从启动以来的系统节拍数,系统启动的时候会将 jiffies 初始化为0,jiffies定义在文件 include/linux/jiffies.h中:

       jiffies_64和jiffies 其实是同一个东西。jiffies_64用于64位系统里,而jiffies则在32位系统内被使用;jiffies其实就是jiffies_64的低32位。

      关于jiffies值溢出的问题。不管是 32 位还是 64 位的 jiffies,都有溢出的风险,溢出以后会重新从 0 开始计数,相当于绕回来了,因此有些资料也将这个现象也叫做绕回。假如 HZ 为最大值 1000 的时候,32 位的 jiffies 只需要 49.7 天就发生了绕回,对于 64 位的 jiffies 来说大概需要5.8 亿年才能绕回,因此 jiffies_64 的绕回忽略不计。如何处理节拍器的计数绕回:

2、Linux内核提供节拍值对比的API函数

#include <linux/jiffies.h>

① time_after(unkown,known);           //如果unkown大于known,则返回true

② time_before(unkown,known);        //如果unkown小于known,则返回true

③ time_after_eq(unkown,known);     //如果unkown大于等于known,则返回true

④ time_before_eq(unkown,known);  //如果unkown小于等于known,则返回true

3、Linux内核提供的jiffies值与标准时间单位值之间的转换API函数

#include <linux/jiffies.h>

① unsigned int jiffies_to_msecs(const unsigned long j);     //将j值转换为毫秒单位

② unsigned int jiffies_to_usecs(const unsigned long j);      //将j值转换为微秒单位

③ u64 jiffies_to_nsecs(const unsigned long j);                   //将j值转换为纳秒单位

④ unsigned long msecs_to_jiffies(const unsigned int m);  //将毫秒单位转换为jiffies值

⑤ unsigned long usecs_to_jiffies(const unsigned int u);    //将微妙单位转换为jiffies值

⑥ unsigned long nsecs_to_jiffies(u64 n);                          //将纳秒单位转换为jiffies值

 三、内核驱动定时器

1、基本的介绍

        定时器是一个很常用的功能,需要周期性处理的工作都要用到定时器。Linux内核的定时器基于系统节拍来实现,并不是裸机概念中的硬件定时器。定时器的使用很简单,你只需要执行一些初始化工作,设置一个超时时间,指定超时发生后的执行函数,然后激活定时器就可以了;指定的函数将在定时器到期时自动执行。注意定时器并非周期运行,它在超时后自动撤销,可以不断地创建和撤销,而且它的执行次数不受限制,故被称为动态定时器。

        Linux内核在2.4版本以后,去掉了老版本内核中的静态定时器机制,而只留下动态定时器。相应地在timer_bh()函数中也不再通过run_old_timers()函数来运行老式的静态定时器。动态定时器与静态定时器这二个概念是相对于Linux内核定时器机制的可扩展功能而言的,动态定时器是指内核的定时器队列是可以动态变化的,然而就定时器本身而言,二者并无本质的区别。考虑到静态定时器机制的能力有限,因此Linux内核2.4版中完全去掉了以前的静态定时器机制。

        Linux内核使用结构体timer_list来描述一个动态定时器。值得注意的是,Timer是Linux内核中的一种软中断,被调用的函数是异步执行的;由于是软中断, 所以函数被执行的时候是处于非进程的上下文中,所以有以下规则需要遵守:

① 不允许访问用户空间

② 不能执行会引起休眠的函数或调用

③ 注意对并发访问数据的保护

④ 目前的系统时钟最高精度是1000HZ,与系统节拍率配置有关

⑤  内核添加printk后,由于打印函数耗时会导致执行时间延长,一般为ms级

        

/

2、Linux内核动态定时器描述结构体timer_list

#include <linux/timer.h>

结构体原型:

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

};

/

3、Linux内核提供的定时器相关API接口

#include <linux/timer.h>

① void init_timer(struct timer_list *timer);                                       //初始化定义好的定时器,就是将定时器加入内核定时器管理队列

② void add_timer(struct timer_list *timer);                                      //启动动态定时器,开始计时;其实是调用了mod_timer

③ int mod_timer(struct timer_list *timer,unsigned long expires);     //修改动态定时器的超时时间并启动定时器,返回1=定时器正在运行,不能修改,0=操作成功

④ int del_timer(struct timer_list *timer);                                          //删除动态定时器,不管定时器是否正在运行;返回值1=激活态的定时器被释放,0=释放了关闭态的定时器

⑤ int del_timer_sync(struct timer_list *timer);                                 //同步等待其他CPU处理完定时器再删除;返回值1=定时器已经被激活 0=定时器未被激活

/

4、Linux内核动态定时器实现的原理

         Linux是怎样为其内核定时器机制提供动态扩展能力的呢? 其关键就在于“定时器向量”的概念。所谓“定时器向量”就是指这样一条双向循环定时器队列(对列中的每一个元素都是一个timer_list结构):对列中的所有定时器都在同一个时刻到期,也即对列中的每一个timer_list结构都具有相同的expires值。显然,可以用一个timer_list结构类型的指针来表示一个定时器向量。

        显然,定时器expires成员的值与jiffies变量的差值决定了一个定时器将在多长时间后到期。在32位系统中,这个时间差值的最大值应该是0xffffffff。因此如果是基于“定时器向量”基本定义,内核将至少要维护0xffffffff个timer_list结构类型的指针,这显然是不现实的。 另一方面,从内核本身这个角度看,它所关心的定时器显然不是那些已经过期而被执行过的定时器(这些定时器完全可以被丢弃),也不是那些要经过很长时间才会到期的定时器,而是那些当前已经到期或者马上就要到期的定时器(注意!时间间隔是以滴答次数为计数单位的)。

        基于上述考虑,并假定一个定时器要经过interval个时钟滴答后才到期(interval=expires-jiffies),则Linux采用了下列思想来实现其动态内核定时器机制:对于那些0≤interval≤255的定时器,Linux严格按照定时器向量的基本语义来组织这些定时器,也即Linux内核最关心那些在接下来的255个时钟节拍内就要到期的定时器,因此将它们按照各自不同的expires值组织成256个定时器向量。而对于那些256≤interval≤0xffffffff的定时器,由于他们离到期还有一段时间,因此内核并不关心他们,而是将它们以一种扩展的定时器向量语义(或称为“松散的定时器向量语义”)进行组织。所谓“松散的定时器向量语义”就是指:各定时器的expires值可以互不相同的一个定时器队列。

         具体的组织方案可以分为两大部分:

       (1)对于内核最关心的、interval值在[0,255]之间的前256个定时器向量,内核是这样组织它们的:这256个定时器向量被组织在一起组成一个定时器向量数组,并作为数据结构timer_vec_root的一部分,该数据结构定义在kernel/timer.c文件中,如下述代码段所示:

基于数据结构timer_vec_root,Linux定义了一个全局变量tv1,以表示内核所关心的前256个定时器向量。这样内核在处理是否有到期定时器时,它就只从定时器向量数组tv1.vec[256]中的某个定时器向量内进行扫描。而tv1的index字段则指定当前正在扫描定时器向量数组tv1.vec[256]中的哪一个定时器向量,也即该数组的索引,其初值为0,最大值为255(以256为模)。每个时钟节拍时index字段都会加1。显然,index字段所指定的定时器向量tv1.vec[index]中包含了当前时钟节拍内已经到期的所有动态定时器。而定时器向量tv1.vec[index+k]则包含了接下来第k个时钟节拍时刻将到期的所有动态定时器。当index值又重新变为0时,就意味着内核已经扫描了tv1变量中的所有256个定时器向量。在这种情况下就必须将那些以松散定时器向量语义来组织的定时器向量补充到tv1中来。

         (2)而对于内核不关心的、interval值在[0xff,0xffffffff]之间的定时器,它们的到期紧迫程度也随其interval值的不同而不同。显然interval值越小,定时器紧迫程度也越高。因此在将它们以松散定时器向量进行组织时也应该区别对待。通常,定时器的interval值越小,它所处的定时器向量的松散度也就越低(也即向量中的各定时器的expires值相差越小);而interval值越大,它所处的定时器向量的松散度也就越大(也即向量中的各定时器的expires值相差越大)。

内核规定,对于那些满足条件:0x100≤interval≤0x3fff的定时器,只要表达式(interval>>8)具有相同值的定时器都将被组织在同一个松散定时器向量中。因此,为组织所有满足条件0x100≤interval≤0x3fff的定时器,就需要26=64个松散定时器向量。同样地,为方便起见,这64个松散定时器向量也放在一起形成数组,并作为数据结构timer_vec的一部分。基于数据结构timer_vec,Linux定义了全局变量tv2,来表示这64条松散定时器向量。如上述代码段所示。

对于那些满足条件0x4000≤interval≤0xfffff的定时器,只要表达式(interval>>8+6)的值相同的定时器都将被放在同一个松散定时器向量中。同样,要组织所有满足条件0x4000≤interval≤0xfffff的定时器,也需要26=64个松散定时器向量。类似地,这64个松散定时器向量也可以用一个timer_vec结构来描述,相应地Linux定义了tv3全局变量来表示这64个松散定时器向量。

对于那些满足条件0x100000≤interval≤0x3ffffff的定时器,只要表达式(interval>>8+6+6)的值相同的定时器都将被放在同一个松散定时器向量中。同样,要组织所有满足条件0x100000≤interval≤0x3ffffff的定时器,也需要26=64个松散定时器向量。类似地,这64个松散定时器向量也可以用一个timer_vec结构来描述,相应地Linux定义了tv4全局变量来表示这64个松散定时器向量。

对于那些满足条件0x4000000≤interval≤0xffffffff的定时器,只要表达式(interval>>8+6+6+6)的值相同的定时器都将被放在同一个松散定时器向量中。同样,要组织所有满足条件0x4000000≤interval≤0xffffffff的定时器,也需要26=64个松散定时器向量。类似地,这64个松散定时器向量也可以用一个timer_vec结构来描述,相应地Linux定义了tv5全局变量来表示这64个松散定时器向量。

最后,为了引用方便,Linux定义了一个指针数组tvecs[],来分别指向tv1、tv2、…、tv5结构变量。如上述代码所示。

/

 /

四、Linux内核提供各种延时函数

1、内核短延时函数

        内核提供的短延时实际上是忙等待,也就是说会一直占用CPU直到超时。这种阻塞延时的精度比较高,但比较消耗CPU资源,不适用于较长时间的延时。常用的内核短延时函数:

include <linux/delay.h>

① void ndelay(unsigned long nsecs);      //阻塞延时纳秒时间

② void udelay(unsigned long usecs);      //阻塞延时微秒时间

③ void mdelay(unsigned long msecs);    //阻塞延时毫秒时间

//

2、内核睡眠长延时函数

        调用这些延时函数的进程将进入休眠状态,允许CPU去执行其他任务;延时时间到达后,将产生中断信号唤醒进程。休眠延时函数的精度是有限的。常用的睡眠延时函数如下:

#include <linux/timer.h>

#include <linux/delay.h>

① void msleep(unsigned int msecs);                                             //进程睡眠延时指定毫秒时间,睡眠过程不能被信号打断

② unsigned long msleep_interruptible(unsigned int msecs);         //进程睡眠延时指定毫秒时间,睡眠可以被信号打断

③ void ssleep(unsigned int seconds);                                           //进程睡眠延时指定秒单位时间,睡眠过程不能被信号打断

//

3、等待队列上睡眠的延时函数:(未明确

          函数可以将当前进程添加到等待队列中,从而在等待队列上睡眠。当超时发生时,进程将被唤醒。函数接口如下:

① 

//

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值