参考资料出自:
内核定时器(也称为动态定时器)是内核在以后某一个时刻运行一段程序或进程的基础,软件定时器可以在一个确切的时间点上(更严格地说是一个时间点以后)激活相应的程序段或进程。软件定时器在设备驱动程序中被大量应用以检测设备的状态。
使用一个软件定时器很简单,只需做一些初始化工作,设置一个相对于当前时刻的超时时间和超时处理函数,将其插入到内核定时器队列中即可,设置的超时处理函数会在定时器超时时自动运行。下面介绍如何使用内核定时器和实现内核定时器的内部架构。
1.内核定时器的使用方法
内核定时器由数据结构timer_list表示,该结构表示了一个待处理的延迟任务,我们称该数据结构为内核定时器节点。该数据结构的详细内容请看下面的代码清单。
代码清单--数据结构timer_list
功能简介:该数据结构保存了内核定时器节点的相关信息,包括定时器超时时间和超时处理函数等。
文件:src/include/Linux/timer.h
11 struct timer_list { 12 struct list_head entry;
13 unsigned long expires;
14 void (*function)(unsigned long); 15 unsigned long data; 16 struct timer_base_s *base; };
成员变量entry:该内核链表表头类型成员变量用于将该内核定时器节点连接到系统中的定时器链表中。
成员变量expires:该无符号长整型变量保存了该定时器的超时时间,用于和系统核心变量jiffies进行比较。
成员变量function:该函数指针变量保存了内核定时器超时后要执行的函数,即定时器超时处理函数。
成员变量data:该无符号长整型变量用作定时器超时处理函数的参数。
成员变量base:该指针变量表明了该内核定时器节点归属于系统中哪一个处理器,在使用函数init_timer()初始化内核定时
器节点的过程中,将该指针指向了一个每处理器变量tvec_bases的成员变量t_base。
在了解了内核定时器节点数据结构的相关内容之后,下面来看如何在自己的代码中使用一个内核定时器节点,实现在一段时间后执行一个延迟处理任务。
①首先,使用下面语句声明一个内核定时器数据结构。
struct timer_list my_timer;
②使用函数init_timer()对上一步声明的内核定时器结构进行初始化。函数init_timer()主要设置该内核定时器归属系统中哪一个处理,并初始化内核定时器链表指针的next域为NULL。
init_timer(&my_timer);
③使用下面的语句来设置内核定时器的超时时间expires、超时处理函数function、超时处理函数所使用的参数data。
my_timer.expires = jiffies + delay; my_timer.data = 0; my_timer.function = my_function;
④也是最后一步,通过函数add_timer()来激活内核定时器,使用的语句如下:
add_timer(&my_timer);
通过上面4步,我们就创建了一个内核定时器节点my_timer。该内核定时器在当前时刻以后delay个时钟中断后超时,执行超时处理函数my_function,传给超时处理函数的参数为0。
除了上述过程中介绍的内核定时器接口函数之外,内核同时提供了以下接口函数来辅助对内核定时器的操作。
intmod_timer(structtimer_list*timer,unsignedlongexpires):该函数负责修改内核定时器timer的超时字段expires。该函数可以修改激活和没有激活的内核定时器的超时时间,并把它们都设置为激活状态;返回值为0表示修改的内核定时器在修改之前处于未激活状态,返回值为1表示修改的内核定时器在修改之前处于已激活状态。
intdel_timer(structtimer_list*timer)、intdel_timer_sync(structtimer_list*timer):这两个函数负责从链表中删除内核定时器timer。它们的区别在于,后者在多处理器系统中会确保其他处理器上没有处理或者处理完毕当前内核定时器timer时才退出。
2.内核定时器架构
与softirq、工作队列两种中断下半部的处理方法类似,每一个内核定时器节点与系统中的处理器通过一个每处理器变量联系起来。内核在文件src/kernel/timer.c中的第88行使用下面的语句分配了一个名称为tvec_bases、类型为tvec_base_t的每处理器变量。
static DEFINE_PER_CPU(tvec_base_t, tvec_bases);
其中,tvec_base_t是数据结构structtvec_t_base_s通过语句typedef定义的一个别名,数据结构structtvec_t_base_s用来记录系统中每一个处理器上待处理内核定时器节点的相关信息。有关该数据结构的详细内容请看下面的代码清单。
代码清单--数据结构tvec_t_base_s
功能简介:该数据结构用于有效组织当前处理器上所有待处理内核定时器节点,以支持快速访问超时内核定时器节点。
该数据结构在同一文件中的第77行开始定义,代码如下。接下来是对这些成员用途的分析、说明。
struct tvec_t_base_s {struct timer_base_s t_base;unsigned long timer_jiffies;tvec_root_t tv1;tvec_t tv2;tvec_t tv3;tvec_t tv4;tvec_t tv5;} ____cacheline_aligned_in_smp;
成员变量t_base的数据类型为structtimer_base_s,它在文件src/kernel/timer.c中的第64行开始定义,代码如下:
truct timer_base_s {spinlock_t lock;struct timer_list *running_timer;);
其中,成员变量running_timer记录了正在本地处理器上进行超时处理的内核定时器;另外一个自旋锁成员变量lock用于保护每处理器变量tvec_bases的本地拷贝。
无符号长整型变量timer_jiffies记录了该数据结构中所包含的定时器中最早超时时间,根据该变量可以计算出超时定时器节点所在链表的表头。
变量tv1的类型为tvec_root_t,该数据结构在文件src/kernel/timer.c中的第73行开始定义,代码如下。该数据结构中包含了TVR_SIZE个链表表头指针。
typedef struct tvec_root_s {struct list_head vec[TVR_SIZE];} tvec_root_t;
变量tv2、tv3、tv4、tv5的类型为tvec_t,该数据结构在文件src/kernel/timer.c中的第69行开始定义,代码如下:
ypedef struct tvec_s {struct list_head vec[TVN_SIZE];)tvec_t;
这5个变量一共包含了256+64×4=512个链表表头,每个链表表头指针指向了一个待处理内核定时器链表。由当前处理器处理的内核定时器根据其超时时间的不同分布在这512个链表上。通过上面的分析,可以用一个形象化的示意图来描述内核定时器的架构,如图所示。
内核定时器架构示意图 |
定时器的使用非常方便,只需要执行一些初始化的操作,设置一个超时时间,指定超时发生时执行的函数,然后激活定时器就可以了。它的处理和工作队列还是有点类似的。其实,在Linux内核开发中,很多的操作都是类似的。还有一点需要注意的,内核定时器并不是周期运行,它在超时后自动销毁。因此,如果要实现周期轮询,就需要在定时器执行函数返回前再次激活定时器。
下面看看一个实现轮询操作的小例子: structtimer_listpolling_timer;init_timer(&polling_timer);
polling_timer.data=(unsignedlong)something;polling_timer.function=polling_handler;polling_timer.expires=jiffies+2*HZ;add_timer(&polling_timer);voidpolling_handler(unsignedlongdata){
...polling_timer.expires=jiffies+2*HZ;add_timer(&polling_timer);}jiffies是Linux内核中的一个全局变量,用来记录自系统启动以来产生的节拍的总数。启动时,内核将该变量初始化为0,此后,每次时钟中断处理程序都会增加该变量的值。
HZ是内核定义的宏,在i386体系结构中定义为:#defineHZ10002.6内核的时钟中断频率是1000,也就是说,在1秒里jiffies会被增加1000。因此jiffies+2*HZ表示推后2秒钟。有时,需要更改已经激活的定时器,采用如下函数:mod_timer(&polling_timer,jiffies+new_delay);如果需要在定时器超时前停止定时器,可以使用del_timer()函数:del_timer(&polling_timer);在多处理器的情况下使用:del_timer_sync(&polling_timer);注意,不需要为已经超时的定时器调用该函数,因为它们会自动被删除。内核定时器是在时钟中断发生后,作为软中断在下半部的上下文钟执行的。所有的定时器结构都以链表的形式存储。时钟中断发生后,内核按链表顺序依次执行。一般来说,定时器在超时后会立即执行,但是也有可能被推迟到下一个时钟节拍才能运行,所以不能用定时器来实现硬实时的操作。又因为内核定时器发生在软中断中,因此,定时器执行函数不能够睡眠,也不能够持有信号量。如果对硬件的访问需要使用信号量同步,或者可能睡眠(比如需要调用kmalloc内存分配,但是由于某种原因不能使用GFP_ATOMIC标志),就不能直接通过定时器来实现了。一个变通的做法是在内核定时器执行函数里调用工作队列,在工作队列处理函数中实现对硬件的访问。