Linux下的延时/定时机制

2020-05-18

关键字:timer_list定时器、jiffies机制


 

1、jiffies

 

Linux内核一般都通过 jiffies 来获取系统的当前时间。

 

jiffies 是一个被定义在 <linux/jiffies.h> 中的 unsigned long 型的变量。这个变量的值由内核自动设定,它表示的是自最近一次系统启动到当前的时间间隔。

 

jiffies 机制是一个完全独立且兼容性极好的计时系统。任何Linux系统都有 jiffies 。因此,在大多数情况下它可以被设备驱动等程序作为时间参考工具,即使它的精度并没有那么高。jiffies 的精度只能到达 ms 级,如果您的设备驱动需要更高精度的计时,那可能得另寻它法了,通常是直接读取相应寄存器来实现。不过这样一来,代码的兼容性就会差很多了。但在绝大多数情况下,jiffies 的精度都足够了。

 

在Linux内核中,最简单粗暴的延时方式就是“忙等待”。

 

它说白了就是设定一个终止时间点,然后一直去检测系统时钟当前又没有到达这个终止时间点来判定是否执行下一步操作。这种方式虽然会浪费掉很多CPU的运算力,但不可否认的是它的编写也相当简单。一种比较常见的方式就是检测jiffies来延时。如下代码所示:

unsigned long end_time = jiffies + HZ * 5; //表示在当前时间点5秒后为终止时间点。
while(time_before(jiffies, end_time)); //jiffies未到end_time的话会一直在这个循环中打转。time_before()函数位于jiffies.h中。

do_anything_you_wanna_do(); //时间到!

对于以上代码需要解释的下的就是第一行的那个 HZ 。HZ是一个被定义在 <asm/param.h> 中的宏。它的值会根据相应平台的不同而不同。总之这个值就表示当前系统在1秒钟时间里可以跳跃的jiffies数量。 总之记住 jiffies + HZ 表示1秒钟以后的 jiffes 变量的值就行了。

 

不过总得来说,上面这种写法是一种相当糟糕的实现方式。严重的情况下它会导致程序出现“假死”的现象。因此,没有特别的需求,不要使用这种方式来延时。

 

2、短延时

 

在内核程序开发中。短期延时通常直接使用内核提供的几个延时函数即可。如:

#include <linux/delay.h>
void ndelay(unsigned long nsecs); //纳秒延时
void udelay(unsgined long usecs); //微秒延时
void mdelay(unsigned long msecs); //毫秒延时

这里得注意一下,一般“纳秒延时”是做不到的。只能说是起个参考作用罢了。其实严格来讲,这三个延时都只能起到个参考作用而已。并且还有个很重要的:这三个延时函数是“忙等待型”。即在等待延时到期的过程中,CPU会在那死等而不会让出调度权。

 

与之相对应的还有几个延时时间更长的,且不属于“忙等待型”的延时函数:

#include <linux/delay.h>
void msleep(unsigned int ms);
void ssleep(unsigned int seconds);
unsigned long msleep_interruptible(unsigned int ms);

前面两个函数的延时是不可中断的。如果希望在延时等待过程中进程能被相应中断唤醒,则可以使用第三个函数。更具体的用法,还得各位同学自行实践掌握了。

 

 

3、定时器

 

老实说,Linux内核里的定时器还真不少。

 

定时器的优点就不用多说了,既准确又可以不阻塞当前进程。这里要讲的定时器是一个基于 jiffies 时间基准的定时器。

 

这个定时器位于 <linux/timer.h> 中,它的原型定义如下所示:

struct timer_list {
    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
};

当然,需要我们关心的成员其实就三个:

1、expires

一个 jiffies 值。表示本次定时将于指定的 jiffies 值时到时。

2、data

一个 unsigned long 型的变量。当定时到时时这个变量将会作为参数传递到回调函数中去。unsigned long 和指针的长度是一致的,由此你肯定能知道这个参数的作用有多大了。

3、function指针函数

定时器到时回调函数。

 

与大多数结构体类型的使用类似,timer_list 定时器的使用无非就如下三个步骤:

1、申请内存;

kmalloc()等可以在内核中分配内存的函数均可搞定。

2、初始化;

<linux/timer.h> 中有个宏定义 "#define init_timer(timer)",将上一步申请到的内存的地址传递进去即可。这个操作相当于 memset() / bzero()。

其次就是设置 expires , data , function 了。

3、启动定时;

<linux/timer.h>  中的函数 add_timer() 即可搞定。

4、处理定时中断;

在第 2 步中注册的回调函数中执行你的逻辑代码。

5、销毁。

del_timer() 函数。

 

话不多说,直接上一个实例感受一下,以下代码的功能是加载了驱动程序后即启动一个定时器,并在定时器到时以后自动重新启动以实现循环定时计数的功能:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/jiffies.h>
#include <linux/timer.h>
#include <linux/slab.h>

static struct timer_list *timer;

void timer_response(unsigned long data)
{
    printk("%s()\n", __FUNCTION__);
    printk("data:%ld\n", data);

    if(timer != NULL)
    {
        /*
            重启方式一
        */
//        timer->data = jiffies;
//        mod_timer(timer, timer->data + HZ * 3); //直接调用这个函数修改掉 expires 值就会自动触发下一轮定时的。

        /*
            重启方式二
        */
        timer->data = jiffies;
        timer->expires = timer->data + HZ * 3; 
        add_timer(timer);
    }
}

static int __init mymd_init()
{
    printk("%s()\n", __FUNCTION__);

    printk("-------- timer_list demo running --------\n");
    timer = (struct timer_list *)kmalloc(sizeof(struct timer_list), GFP_KERNEL); //Must malloc first.
    init_timer(timer);
    printk("timer_list address:0x%p\n", timer);
    timer->expires = jiffy + HZ * 5;
    timer->data = jiffy;
    timer->function = timer_response;

    add_timer(timer);

    
    return 0;
}

static void __exit mymd_exit()
{
   printk("%s()\n", __FUNCTION__);

   if(timer != NULL){
       printk("freeing timer_list object!\n");
       int fret = del_timer(timer);
       printk("timer_list free ret:%d\n", fret);
   }
}

module_init(mymd_init);
module_exit(mymd_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("chorm");

 

关于定时到时响应函数这里提一点额外的知识:定时到时响应函数严格来讲是一种“软中断”。在软中断中编程需要注意有以下几点规则不可触犯,否则会让你的程序在运行时产生意料之外的结果:

1、不允许访问用户空间的内存。

2、current指针是无效的。

3、不能执行休眠或调度操作。包括但不限于:udelay() , mdelay() , msleep() , GFP_KERNEL式的kmalloc() , 信号量等。

 

另外,在 <linux/hardirq.h> 中定义了一个宏定义可以查询当前是属于“进程上下文”还是“中断上下文”。中断上下文即指上面提到的“软中断”环境,进程上下文则是普通环境。这个查询宏定义的原型如下:

#define in_interrupt()        (irq_count())

这个宏定义会返回一个 int 型值。返回值 0 表示当前在“进程上下文”中,非 0 表示处于“中断上下文”中。

 

另外,关于定时中断类似的实现方式还有 tasklet 与 workqueue 两种。这两种机制的相关知识可以在笔者的另一篇博文中找到,这里就不再赘述了。

https://www.cnblogs.com/chorm590/p/12294386.html

 


 

posted @ 2020-05-18 21:18  大窟窿  阅读(2420)  评论(0编辑  收藏  举报