• Linux 内核定时器


    内核定时器

    软件意义的定时器依赖于硬件定时器来实现,内核在时钟中断发生后检测各定时器是否到期,到期后定时器处理函数将作为软中断在底半部执行。实质上,时钟中断处理程序会唤起TIMER_SOFTIRQ软中断,运行当前处理器上到期的所有定时器。

    定时器数据结构与函数

    Linux设备驱动编程中,可利用Linux内核提供 一组函数和数据结构来实现定时触发工作,或者完成周期性的任务。

    Linux内核提供的用于操作定时器的数据结构和函数位于<linux/timer.h>,定义/声明如下:

    1)timer_list结构体

    timer_list定义:

    struct timer_list {
        /*
         * All fields that change during normal runtime grouped to the
         * same cacheline
         */
        struct hlist_node    entry;
        unsigned long        expires; // 定时器到期时间(jiffies)
        void                 (*function)(unsigned long); // 超时处理函数
        unsigned long        data;   // 传递给function()的参数
        u32                  flags;
    
    #ifdef CONFIG_TIMER_STATS
        int             start_pid;
        void            *start_site;
        char            start_comm[16];
    #endif
    #ifdef CONFIG_LOCKDEP
        struct lockdep_map   lockdep_map;
    #endif
    };
    

    一个timer_list结构体对象对应一个定时器。当定时器到期后,超时处理函数function()将会被执行。
    驱动程序中,定义一个名为my_timer的定时器:

    struct timer_list my_timer;
    

    2)初始化定时器

    init_timer 是一个宏,用于初始化定时器,其原型等价于:

    void init_timer(struct timer_list *timer);
    

    setup_timer 也是一个宏,用于初始化定时器并赋值其成员,原型等价于:

    void setup_timer(struct timer_list *timer, void (*function)(unsigned long), unsigned long data, u32 flags);
    // 源代码
    #define setup_timer(timer, fn, data)                    \
        __setup_timer((timer), (fn), (data), 0)
    
    #define __setup_timer(_timer, _fn, _data, _flags)            \
        do {                                \
            __init_timer((_timer), (_flags));            \
            (_timer)->function = (_fn);                \
            (_timer)->data = (_data);                \
        } while (0)
    

    setup_timer与init_timer的区别在于,前者需要在调用时,指明超时处理函数、参数、标志位。

    TIMER_INITIALIZER(_function, _expires, _data) 宏用于赋值定时器结构体的function、expires、data、flags等成员,该宏等价于:

    #define TIMER_INITIALIZER(_function, _expires, _data) { \
            .entry = { .next = TIMER_ENTRY_STATIC },    \
            .function = (_function),            \
            .expires = (_expires),                \
            .data = (_data),                \
            .flags = (_flags),                \
            __TIMER_LOCKDEP_MAP_INITIALIZER(        \
                __FILE__ ":" __stringify(__LINE__))    \
        }
    

    3)增加定时器

    add_timer用于注册内核定时器,将定时器加入到内核动态定时器链表中。

    void add_timer(struct timer_list *timer);
    

    4)删除定时器

    用于从内核定时链表删除定时器。

    int del_timer(struct timer_list * timer);
    

    del_timer_sync()是del_timer()的同步版,在删除一个定时器时需要等待其被处理完,因此该函数的调用不能位于中断上下文。因为中断上下文要求执行迅速,不能做无谓的等待。

    5)修改定时器的expire

    函数用于修改定时器的到期时间,在新的被传入的expires到来后,才会执行定时器函数。

    int mod_timer(struct timer_list *timer, unsigned long expires);
    

    定时器时间单位

    内核源码根目录下,“ls -a”命令可以看到一个隐藏文件,这就是内核配置文件。打开后,可以看到这一项:

    CONFIG_HZ=100
    

    该项表示内核每秒会发生100次系统嘀嗒中断(tick),类似于人的心跳,这是Linux系统的心跳。每发生一次tick中断,全局遍历jiffies就会累加1。
    CONFIG_HZ的单位是HZ,值100表示每个嘀嗒是10ms。

    内核定时器timer_list 的时间就是基于jiffies的。如果我们要修改超时时间,通常用这2种方法:
    (1)在add_timer之前,直接修改:

    timer.expires = jiffies + xxx;    // xxx 表示多少各嘀嗒后超时,也就是xxx*10ms
    timer.expires = jiffies + 2 * HZ; // HZ等于CONFIG_HZ,2*HZ相当于2秒
    

    (2)在add_timer之后,使用mod_timer修改:

    mod_timer(&timer, jiffies + xxx);    // xxx 表示多少各嘀嗒后超时,也就是xxx*10ms
    mod_timer(&timer, jiffies + 2 * HZ); // HZ等于CONFIG_HZ,2*HZ相当于2秒
    

    定时器使用模板

    几个要素:1)定义定时器结构体;2)初始化定时器,并设置超时处理函数、参数、超时时间等;3)添加定时器;4)删除定时器;5)定义超时处理函数。

    /* xxx 字符设备结构体 */
    struct xxx_dev {
        struct cdev cdev;
        ...
        timer_list xxx_timer; /* 设备要用的定时器 */
    };
    
    /* xxx 驱动中的函数 */
    xxx_func(...)
    {
        struct xxx_dev *dev = filp->private_data;
        ...
        /* 初始化定时器 */
        init_timer(&dev->xxx_timer);
        dev->xxx_timer.function = &xxx_do_timer;  // 超时处理函数
        dev->xxx_timer.data = (unsigned long)dev; // 超时处理函数的参数
        dev->xxx_timer.expires = jiffies + delay; // 超时时间
        /* 添加(注册)定时器 */
        add_timer(&dev->xxx_timer);
        ...
    }
    
    /* xxx 驱动中的另一个函数 */
    xxx_func2(...)
    {
        ...
        /* 删除定时器 */
        del_timer(&dev->xxx_timer);
        ...
    }
    
    /* 定时器处理函数 */
    static void xxx_do_timer(unsigned long arg)
    {
        struct xxx_device *dev = (struct xxx_device*)arg;
        ...
        /* 调度定时器再执行 */
        dev->xxx_timer.expires = jiffies + delay;
        add_timer(&dev->xxx_timer);
        ...
    }
    

    内核延时

    短延迟

    Linux内核提供3个函数,分别以纳秒、微秒、毫秒为单位进行延迟:

    void ndelay(unsigned long nsecs);
    void udelay(unsigned long usecs);
    void mdelay(unsigned long msecs);
    

    延迟的实现原理是忙等等,根据CPU频率进行一定次数的循环。等价自定义实现于:

    void delay(unsigned long time)
    {
        while(time--);
    }
    

    内核启动时,会运行一个延迟循环校准(Delay Loop Calibration),计算出lpj(Loops Per Jiffy),内核启动时会打印如下类似信息:

    Calibrating delay loop... 530.84 BogoMIPS (lpj=1327104)
    

    如果我们直接在bootloader(uboot)传递给内核的bootargs中设置lpj=1327104,则可以省掉这个校准的过程,节约百毫秒开机时间。

    在内核中,最好不要直接使用mdelay(),因为毫秒级延时很大了,无谓的等待将耗费CPU资源。对于毫秒级以上的延时,内核提供下列函数:

    void msleep(unsigned int millisecs);
    unsigned long msleep_interruptible(unsigned int millisecs);
    void ssleep(unsigned int seconds);
    

    上述函数,将使得调用它的进程睡眠参数指定的时间为millisecs。区别在于msleep()、ssleep()不能被打断,msleep_interruptible()能被打断。
    注意:受系统Hz及进程调度的影响,msleep()及类似函数的精度有限。

    长延迟

    内核中进行延迟的一个很直观方法:比较当前jiffies(系统嘀嗒)和目标jiffies(设置为当前jiffies加上时间间隔的jiffies),直到未来jiffies达到目标jiffies。

    例,先用忙等待延迟100个jiffies,再延迟2s。

    /* 延迟100个jiffies */
    unsigned long delay = jiffies + 100; // jiffies默认10ms单位, +100 意指1s以后
    while (time_before(jiffies, delay));
    
    /* 再延迟2s */
    unsigned long delay = jiffies + 2*Hz; // Hz 单位1s
    while (time_before(jiffies,  delay));
    

    上述代码本质上都是忙等待。

    time_before() 对应还有个time_after(),实际上是将传入的未来时间jiffies和被调用时的jiffies进行一个简单的比较:

    #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)
    

    timebefore(a,b) 含义:如果a在b前面,即a < b,那么表达式为true;否则,表达式为假。

    睡着延迟

    睡着延迟是在等待的时间到来之前,进程处于睡眠状态,CPU资源被其他进程使用。

    schedule_timeout() :使得当前任务休眠至指定的jiffies之后,再重新被调度执行。
    msleep(),msleep_interrupt() 本质上都是依靠包含了schedule_timeout() 的schedule_timeout_uninterruptible()和schedule_timeout_interruptible()来实现睡眠的。

    schedule_timeout原型:

    #include <linux/sched.h>
    
    signed long schedule_timeout(signed long timeout);
    

    msleep和msleep_interrupt的实现:

    void msleep(unsigned int msecs)
    {
        unsigned long timeout = msecs_to_jiffies(msecs) + 1; // 将msec(毫秒单位)转换为jiffies并加1, 作为timeout
    
        while (timeout)
            timeout = schedule_timeout_uninterruptible(timeout);
    }
    
    unsigned long msleep_interruptible(unsigned int msecs)
    {
        unsigned long timeout = msecs_to_jiffies(msecs) + 1;
    
        while (timeout && !signal_pending(current))
            timeout = schedule_timeout_interruptible(timeout);
        return jiffies_to_msecs(timeout);
    }
    

    schedule_timeout的实现原理是向系统添加一个定时器,在定时器处理函数中唤醒与参数对应的进程。

    schedule_timeout_interruptible() 与 schedule_timeout_uninterruptible() 区别在于:前者在调用schedule_timeout() 之前置进程状态为TASK_INTERRUPTIBLE,后者置进程状态为TASK_UNINTERRUPTIBLE。也就是说,前者对应进程可以被中断唤醒,后者对应进程无法被中断唤醒。

    schedule_timeout_interruptible与schedule_timeout_uninterruptible的实现:
    
    signed long __sched schedule_timeout_interruptible(signed long timeout)
    {
        __set_current_state(TASK_INTERRUPTIBLE);
        return schedule_timeout(timeout);
    }
    
    signed long __sched schedule_timeout_uninterruptible(signed long timeout)
    {
        __set_current_state(TASK_UNINTERRUPTIBLE);
        return schedule_timeout(timeout);
    }
    
    #define __sched        __attribute__((__section__(".sched.text"))) // 将代码放到指定段".sched.text"
    

    还有两个函数:sleep_on_timeout,可以将当前进程添加到等待队列中,从而在等待队列上睡眠。当超时发生时,进程将被唤醒(后者可以在超时前被打断):

    sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout);
    
    interruptible_sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout);
    

    参考

    [1]宋宝华. Linux设备驱动开发详解[M]. 人民邮电出版社, 2010.

  • 相关阅读:
    bzoj 4660
    bzoj 4668
    二项式反演学习笔记
    bzoj 3622
    bzoj 5306
    bzoj 3625
    任意模数NTT(二)
    bzoj 4913
    bzoj 3456
    多项式问题之五——多项式exp
  • 原文地址:https://www.cnblogs.com/fortunely/p/16485198.html
Copyright © 2020-2023  润新知