Linux设备驱动——内核定时器

摘要:
内核定时器的数据结构structtimer_list{structlist_headentry;unsignedlongexpires;void;unsignedlongdata;structtvec_base*base;/*...*/};其中expires字段表示期望定时器执行的jiffies值,到达该jiffies值时,将调用function函数,并传递data作为参数。需要注意的是expires的值是32位的,因为内核定时器并不适用于长的未来时间点。inttimer_pending;这个函数用来判断一个定时器是否被添加到了内核链表中以等待被调度运行。注意,当一个定时器函数即将要被运行前,内核会把相应的定时器从内核链表中删除。

内核定时器使用

内核定时器是内核用来控制在未来某个时间点(基于jiffies)调度执行某个函数的一种机制,其实现位于<Linux/timer.h>和kernel/timer.c文件中。

被调度的函数肯定是异步执行的,它类似于一种“软件中断”,而且是处于非进程的上下文中,所以调度函数必须遵守以下规则:

1)没有current指针、不允许访问用户空间。因为没有进程上下文,相关代码和被中断的进程没有任何联系。

2)不能执行休眠(或可能引起休眠的函数)和调度。

3)任何被访问的数据结构都应该针对并发访问进行保护,以防止竞争条件。

内核定时器的调度函数运行过一次后就不会再被运行了(相当于自动注销),但可以通过在被调度的函数中重新调度自己来周期运行。

在SMP系统中,调度函数总是在注册它的同一CPU上运行,以尽可能获得缓存的局域性。

内核定时器的数据结构

structtimer_list {
    structlist_head entry; 
    unsigned longexpires;
    void (*function)(unsigned long);
    unsigned longdata; 
    struct tvec_base *base;
    /*... */};

其中expires字段表示期望定时器执行的jiffies值,到达该jiffies值时,将调用function函数,并传递data作为参数。当一个定时器被注册到内核之后,entry字段用来连接该定时器到一个内核链表中。base字段是内核内部实现所用的。

需要注意的是expires的值是32位的,因为内核定时器并不适用于长的未来时间点。

初始化

在使用structtimer_list之前,需要初始化该数据结构,确保所有的字段都被正确地设置。初始化有两种方法。

方法一:

DEFINE_TIMER(timer_name, function_name, expires_value, data);

该宏会定义一个名叫timer_name内核定时器,并初始化其function,expires,name和base字段。

方法二:

structtimer_list mytimer;
void init_timer(struct timer_list *timer);

上述init_timer函数将初始化struct timer_list的 entry的next 为 NULL ,并未base指针赋值

tm->expires = ;
tm->function = ;
tm->data = ;

setup_timer(&mytimer, (*function)(unsigned long), unsigned long data);方法也可以用于初始化定时器并赋值其成员,源代码为:

static inline void setup_timer(struct timer_list * timer, void (*function)(unsigned long), unsigned longdata)
{
  timer->function =function;
  timer->data =data;
  init_timer(timer);
}

注意,无论用哪种方法初始化,其本质都只是给字段赋值,所以只要在运行add_timer()之前,expires,function和data字段都可以直接再修改。

关于上面这些宏和函数的定义,参见include/linux/timer.h。

注册

定时器要生效,还必须被连接到内核专门的链表中,这可以通过 add_timer(struct timer_list *timer) 来实现。

重新注册(修改)

要修改一个定时器的调度时间,可以通过调用 mod_timer(struct timer_list *timer, unsigned long expires)。mod_timer()会重新注册定时器到内核,而不管定时器函数是否被运行过。

注销

注销一个定时器,可以通过 del_timer(struct timer_list *timer)del_timer_sync(struct timer_list *timer)

其中del_timer_sync是用在SMP系统上的(在非SMP系统上,它等于del_timer),当要被注销的定时器函数正在另一个cpu上运行时,del_timer_sync()会等待其运行完,所以这个函数会休眠。另外还应避免它和被调度的函数争用同一个锁。对于一个已经被运行过且没有重新注册自己的定时器而言,注销函数其实也没什么事可做。

int timer_pending(const struct timer_list *timer);

这个函数用来判断一个定时器是否被添加到了内核链表中以等待被调度运行。注意,当一个定时器函数即将要被运行前,内核会把相应的定时器从内核链表中删除(相当于注销)。

使用范例

/*实现每隔一秒向内核log中打印一条信息 */#include <linux/init.h>#include <linux/module.h>#include <linux/kernel.h>#include <linux/time.h>#include <linux/timer.h>

static structtimer_list tm;
structtimeval oldtv;

void callback(unsigned longarg)
{
    structtimeval tv;
    char *strp = (char*)arg;
    
    printk("%s: %lu, %s
", __func__, jiffies, strp);

    do_gettimeofday(&tv);
    printk("%s: %ld, %ld
", __func__,
        tv.tv_sec - oldtv.tv_sec,        //与上次中断间隔 s
        tv.tv_usec- oldtv.tv_usec);        //与上次中断间隔 ms

    oldtv =tv;
    tm.expires = jiffies+1*HZ;    
    add_timer(&tm);        //重新开始计时
}

static int __init demo_init(void)
{
    printk(KERN_INFO "%s : %s : %d - ok.
", __FILE__, __func__, __LINE__);

    init_timer(&tm);    //初始化内核定时器
do_gettimeofday(&oldtv);        //获取当前时间
    tm.function= callback;            //指定定时时间到后的回调函数
    tm.data    = (unsigned long)"hello world";        //回调函数的参数
    tm.expires = jiffies+1*HZ;        //定时时间
    add_timer(&tm);        //注册定时器

    return 0;
}

static void __exit demo_exit(void)
{
    printk(KERN_INFO "%s : %s : %d - ok.
", __FILE__, __func__, __LINE__);
    del_timer(&tm);        //注销定时器
}

module_init(demo_init);
module_exit(demo_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Farsight");
MODULE_DESCRIPTION("Demo for kernel module");

一些和时间相关的内容

linux/jiffies.h

计数值:
jiffies
u64 get_jiffies_64(void)

asm/param.h
每秒触发中断的次数
HZ

---------------------------------------------

时间值
秒数=(jiffies(new) - jiffies(old))/HZ
jiffies(new) = jiffies(old) + 秒*HZ

---------------------------------------------
linux/delay.h
延时函数
void ssleep(unsigned int seconds);
void msleep(unsigned int msecs);

---------------------------------------------
时间函数
linux/time.h
void do_gettimeofday(struct timeval *tv)

免责声明:文章转载自《Linux设备驱动——内核定时器》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇前端调用路由导出数据方法记录在ASP.NET项目中使用XPO的最佳准则下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Linux LVM扩容和缩容

将原硬盘上的LVM分区/dev/mapper/RHEL-Data由原来的60G扩展到80G Step1:将LVData扩容+20G,如下图: [root@esc data]# lvextend -L +20G /dev/RHEL/Data Size of logical volume RHEL/Data changed from 60.00 GiB (15...

linux驱动编写之阻塞与非阻塞

一、概念       应用程序使用API接口,如open、read等来最终操作驱动,有两种结果--成功和失败。成功,很好处理,直接返回想要的结果;但是,失败,是继续等待,还是返回失败类型呢?  如果继续等待,将进程休眠,那么这类驱动设计就是阻塞式的;如果不等待,返回失败的类型(原因),那么这类驱动的设计就是非阻塞式的。       在应用程序打开驱动文件的...

linux下安装nginx和配置

1、系统:centos6.8 2、安装准备: 安装nginx前,我们首先要确保系统安装了g++、gcc、openssl-devel、pcre-devel和zlib-devel软件,可通过如图所示命令进行检测,如果以安装我们可以通过图二所示卸载: yum install gcc-c++ yum -y install zlib zlib-devel open...

Linux程序包管理.md

rpm 简介 RPM包管理员(简称RPM,全称为The RPM Package Manager)是在Linux下广泛使用的软件包管理器。RPM此名词可能是指.rpm的文件格式的软件包,也可能是指其本身的软件包管理器(RPM Package Manager)。最早由Red Hat研制,现在也由开源社区开发。RPM通常随附于Linux发行版,但也有单独将R...

OGG初始化之将数据从文件加载到Replicat

要使用Replicat建立目标数据,可以使用初始加载Extract从源表中提取源记录,并将它们以规范格式写入提取文件。从该文件中,初始加载Replicat使用数据库接口加载数据。在加载过程中,更改同步组提取并复制增量更改,然后与加载结果进行协调。 在加载过程中,记录每次一个记录地应用于目标数据库,因此该方法比其他任何初始加载方法都要慢很多。该方法允许在源系...

Linux下swap升高的原因分析案例

机器配置:2 CPU,8GB 内存 需要预先安装 sysstat 等工具,如 yum install sysstat 终端中运行 free 命令,查看 Swap 的使用情况。 $ free total used free shared buff/cache available Mem:...