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.

原文链接: https://www.cnblogs.com/fortunely/p/16485198.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    Linux 内核定时器

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/402494

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月21日 上午11:05
下一篇 2023年4月21日 上午11:05

相关推荐