趣味定时器

学习完中断后,接下来都是定时中断这一巧妙的中断模式。定时器是在设定时间并且时间一到后就会响应执行设定好的事件,但其只处理一次,如同机械时钟的闹钟功能。而日常中的手机等电子设备设置闹钟后可配置为隔多久再响一次,这需要实现循环定时中断,在下面的内容中会对比说明。
在Linux Kernel中有两种常用的定时器,两者主要是精度的区别,在了解使用它们前,先了解如下几个概念:
1.节拍率(tick rate):系统定时器频率,该值在Linux Kernel里是静态配置好的,使用HZ这个宏,在include/asm-generic/param.h文件中有如下宏:
# define HZ CONFIG_HZ
而CONFIG_HZ在make menuconfig时已默认配置好,可以打开源码根目录下的.config文件,在我的机器上配置为CONFIG_HZ=250,即系统每1/CONFIG_HZ秒发生一次时钟中断,也就是说这种情况下时钟的精度为4ms,这在接下来说到的传统jiffies定时器时会用到。
2.节拍:两次时钟中断的间隔就是一个节拍(tick),其值为节拍率的倒数。
3.jiffies:全局变量,记录系统启动至今所产生的节拍数,通过jiffies/HZ就可知道启动后总共运行的秒数。在include/linux/jiffies.h文件中可看到如下内容:
extern u64 __jiffy_data jiffies_64;
extern unsigned long volatile __jiffy_data jiffies;
分别对应64位和32位系统的声明。
了解了上面的概念后,我们先来了解下传统(标准)的内核定时器,其使用到jiffies和HZ,故而其精度也就是在ms级别,在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
};
其中我们等会涉及到的参数有如下:
1.expires:定时器超时触发的到期时间,以节拍为单位;
2.function:定时器到期触发的处理函数指针;
3.data:处理函数对应的参数。
涉及到的函数有:
1.初始化定时器:
在include/linux/timer.h文件里有如下宏用于初始化定时器:
#define init_timer(timer) \
        __init_timer((timer), 0)
#define setup_timer(timer, fn, data) \
        __setup_timer((timer), (fn), (data), 0)
其中__setup_timer宏如下:
#define __setup_timer(_timer, _fn, _data, _flags) \
        do { \
                __init_timer((_timer), (_flags)); \
                (_timer)->function = (_fn); \
                (_timer)->data = (_data); \
        } while (0)
通过对比可知init_timer和setup_timer两者的区别了。
2.定时器操作:
a.启动定时器 在kernel/timer.c文件中有如下两函数:
int mod_timer(struct timer_list *timer, unsigned long expires) ;
void add_timer(struct timer_list *timer) ;
其中add_timer最终调用mod_timer修改定时器参数,两函数调用后该定时器就生效。
b.删除定时器
在kernel/timer.c文件中有如下函数:
int del_timer(struct timer_list *timer) ;
上面这几个函数对应的头文件都为include/linux/timer.h。
了解了上面这些基本信息后,接下来还是轻写一段代码感受下:
#include <linux/timer.h>
#include <linux/module.h>

#define SEC_COUNT 5
#define MS_COUNT 200

struct timer_list slam_timer;
unsigned long slam_data = SEC_COUNT;

void slam_timer_function(unsigned long data)
{
        printk("<1>I'm in timer,data=%lu\n",data);
        /* every SEC_COUNT seconds call an timer interrupt
        mod_timer(&slam_timer, jiffies + HZ * SEC_COUNT);
        */

        // every MS_COUNT ms call an timer interrupt
        mod_timer(&slam_timer, jiffies + msecs_to_jiffies(MS_COUNT));
}

int init_module(void)
{
        printk("<1>Install timer module!\n");
        /* every SEC_COUNT seconds call an timer interrupt
        init_timer(&slam_timer);
        slam_timer.expires = jiffies + HZ * SEC_COUNT;
        slam_timer.data = slam_data;
        slam_timer.function = slam_timer_function;

        add_timer(&slam_timer);
        */

        // every MS_COUNT ms call an timer interrupt
        setup_timer(&slam_timer, slam_timer_function, slam_data);
        mod_timer(&slam_timer, jiffies + msecs_to_jiffies(MS_COUNT));

        return 0;
}

void cleanup_module(void)
{
        printk("<1>Remove timer module!\n");
        del_timer(&slam_timer);
}
相应的Makefile文件内容为:
obj-m += timer_example.o

CUR_PATH:=$(shell pwd)
LINUX_KERNEL_PATH:=/home/xinu/linux-3.13.6

all:
        make -C $(LINUX_KERNEL_PATH) M=$(CUR_PATH) modules

clean:
        make -C $(LINUX_KERNEL_PATH) M=$(CUR_PATH) clean
相应的源文件目录树如下:
/home/xinu/xinu/linux_kernel_driver_l1/timer_example/
├── Makefile
└── timer_example.c
至此,标准的内核定时器了解并体验了一番,在上面的slam_timer_function函数里都会再调用mod_timer函数,这样做是为了实现循环定时器功能,即手机闹钟风格的定时器,如不调用,则为传统的机械闹钟风格的定时器。
接下来再了解下高精度的定时器hrtimer,其可实现ns级的精度,下面是相关内容:
在include/linux/hrtimer.h文件中有如下结构体:
struct hrtimer {
        struct timerqueue_node node;
        ktime_t _softexpires;
        enum hrtimer_restart (*function)(struct hrtimer *);
        struct hrtimer_clock_base *base;
        unsigned long state;
#ifdef CONFIG_TIMER_STATS
        int start_pid;
        void *start_site;
        char start_comm[16];
#endif
};
该结构体的说明可查看源码文件函数前的注释,该结构体需要hrtimer_init函数来初始化,在kernel/hrtimer.c文件里有如下内容:
void hrtimer_init(struct hrtimer *timer, clockid_t clock_id, enum hrtimer_mode mode) ;
初始化后如下函数来启动:
int hrtimer_start(struct hrtimer *timer, ktime_t tim, const enum hrtimer_mode mode);
如果要取消该定时器,那么有如下函数:
int hrtimer_cancel(struct hrtimer *timer) ;
上面的参数中有如下说明:
1.timer:hrtimer类型的定时器变量;
2.clock_id:在include/uapi/linux/time.h文件里有CLOCK_MONOTONIC等系统时钟的ID定义;
3.mode:在include/linux/hrtimer.h文件里有HRTIMER_MODE_REL等模式定义;
4.tim:在include/linux/ktime.h文件里有关于ktime_t类型的定义,其为一个union类型,定义如下:
union ktime {
        s64 tv64;
#if BITS_PER_LONG != 64 && !defined(CONFIG_KTIME_SCALAR)
        struct {
# ifdef __BIG_ENDIAN
                s32 sec, nsec;
# else
                s32 nsec, sec;
# endif
        } tv;
#endif
};

typedef union ktime ktime_t; /* Kill this */
对于hrtimer启动后,其根据其定时器超时处理函数的返回值来决定是不是重启定时器,在include/linux/hrtimer.h文件中有如下枚举:
enum hrtimer_restart {
        HRTIMER_NORESTART, /* Timer is not restarted */
        HRTIMER_RESTART, /* Timer must be restarted */
};
其中,返回HRTIMER_RESTART时,需调用如下函数更新定时器(在kernel/hrtimer.c文件):
u64 hrtimer_forward(struct hrtimer *timer, ktime_t now, ktime_t interval) ;
了解了这么多,还是继续实例体验吧:
#include <linux/hrtimer.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
//linux3.13.6 include/media/rc-core.h
//#define MS_TO_NS(msec) ((msec) * 1000 * 1000)
#define MS_TO_NS(x) (x * 1E6L)
static struct hrtimer slam_hrtimer;
ktime_t ktime;
unsigned long mdelay = 200L;

enum hrtimer_restart slam_hrtimer_callback(struct hrtimer * timer)
{
        printk("<1>slam_hrtimer_callback called(%ld).\n", jiffies);
        hrtimer_forward(&slam_hrtimer, timer->base->get_time(), ktime);
        return HRTIMER_RESTART;

        /* Or:
        hrtimer_start(&slam_hrtimer, ktime, HRTIMER_MODE_REL);
        return HRTIMER_NORESTART;
        */
}

int init_module(void)
{
        printk("<1>HR timer init.\n");
        ktime = ktime_set(0, MS_TO_NS(mdelay));
        hrtimer_init(&slam_hrtimer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);

        slam_hrtimer.function = &slam_hrtimer_callback;
        printk("<1>Start hrtimer to fire in %ld ms(%ld).\n", mdelay, jiffies);
        hrtimer_start(&slam_hrtimer, ktime, HRTIMER_MODE_REL);

        return 0;
}

void cleanup_module(void)
{
        int ret = hrtimer_cancel(&slam_hrtimer);
        if (ret) printk("<1>The hrtimer is still in use!\n");
        printk("<1>HR timer exit.\n");
}

MODULE_LICENSE("GPL");
相应的Makefile仅更新obj-m,故不给出文件内容,最终相应的文件目录树如下:
/home/xinu/xinu/linux_kernel_driver_l1/hrtimer_example/
├── hrtimer_example.c
└── Makefile
至此,趣味的定时器够好玩吧,体验一番后,也尝试了写驱动模块的另一框架,但还是建议使用之前的框架来写模块,我们继续玩转内核驱动的其他内容。加油!
参考网址:
http://uliweb.clkg.org/tutorial/view_chapter/83
http://edsionte.com/techblog/archives/1738
http://www.ibm.com/developerworks/library/l-timers-list/ http://www.ibm.com/developerworks/cn/linux/l-timers-list/ http://hi.baidu.com/dosculler/item/ac9ad22191578d0a77272c21 http://www.kaifazhe.com/mobile/397350.html
http://blog.csdn.net/hptsf/article/details/8669060

评论

此博客中的热门博文

I/O映射之I/O端口

通过Netlink检测网线插拔

使用seq_file