定时器是编程中常用到的一个机制,在实际项目中几乎是不可避免的,常见表现有以下场景。
内核定时器是内核用于控制在未来某个时间点或者特定时间段内调度执行某个函数的一种机制。内核定时器是一个软定时器,软定时器最终依赖于cpu的硬件定时器实现,对于linux内核来说,依赖于系统时钟节拍。既然是软定时器,就存在软中断,处理函数在软中断中执行。另外,内核定时器还有个特点就是,定时器只会执行一次,超时后即退出,如果需要一个周期性的定时器,需要在超时处理函中重新开启定时器。内核定时器特点归纳起来就是几点:
TIMER_SOFTIRQ
软中断内核定时器基于“软中断”机制,被调度函数在软中断的下半部执行,就需遵循软中断的使用原则,因此函数被调度的时候是处于非进程的上下文中。
semaphore、mutex
)内核定时器依赖于系统时钟节拍,我们有必要了解时钟节拍是什么。时钟节拍即是1秒内系统时钟的中断次数,时钟节拍的高低决定软定时器的最小精度。以典型的100Hz时钟节拍来说,软定时器的最小精度是10ms。linux内核的时钟节拍是可以配置,一般是100Hz,随着cpu性能的增加,可以适当提高时钟节拍,但一般不会超过1000Hz。
---> Kernel Features
---> Timer frequency
时钟节拍的选择范围是100—1000Hz,为什么不能太低或者太高呢?我们知道操作系统调度和时间管理都是基于时钟节拍的,如果时钟节拍太低,会使系统调度实时性降低,时间管理精度降低,100Hz的时间节拍是经过长时间的考验,是最合理的时钟节拍。
既然时钟节拍决定系统调度和时间管理,那为什么不是越高越好?较高的时钟节拍有一定的优势,就是时间精度提高了,能够适用于时间要求更严格的场景。时钟节拍是系统时钟1秒内的中断次数,提高时钟节拍,意味着cpu节拍定时器的中断频率增加,这会增加cpu的负荷,使得cpu需花费更多的时间去处理中断信息。cpu 主要时间都去处理中断信息了,那就无法及时处理操作系统的任务调度,这是不能允许的(软件时不时卡住了能忍不?)。因此,系统的时钟节拍不是越高越好,非苛刻的场合,采用默认的时钟节拍(100Hz)可以满足需求了。当然,现在的cpu性能越来越强,而且是多核,即使使用1000Hz的时钟节拍,增加的cpu资源比例也是很低很低,在可以接受范围。
linux内核使用一个全局变量jiffies
(64位系统为jiffies_64
)来描述(记录)从系统启动以来产生的节拍的总数,系统启动时,内核将jiffies初始化为0,之后cpu时钟每次时钟中断一次,变量执行自加。根据时钟节拍描述,我们可以使用时钟节拍来实现功能:
/*
* The 64-bit value is not atomic - you MUST NOT read it
* without sampling the sequence number in jiffies_lock.
* get_jiffies_64() will do this for you as appropriate.
*/
extern u64 __cacheline_aligned_in_smp jiffies_64;
extern unsigned long volatile __cacheline_aligned_in_smp __jiffy_arch_data jiffies;
对于32位系统,使用的是jiffies
变量,64位系统使用的是jiffies_64
变量。实质上linux为兼容不同的32和64系统,jiffies
使用的即是jiffies_64
变量的低32位空间。在64位系统下,jiffies
和jiffies_64
表示同一个值。因此,建议在编程时,统一使用jiffies
,保证程序的兼容性。
jiffies
计数超出变量范围后会发生溢出,计数将从0重新开始计算,也就是绕回现象。无论是32位还是64位的jiffies
都存在绕回问题,对于32位jiffies
,最大取值为2^32 ,64位的jiffies
最大取值为2^64。以1000Hz的时钟节拍计算,32位jiffies
在系统连续运行49.7天即发生绕回;对于64位jiffies
,大约需要5.8亿年才发生绕回,对于这种情况可以认为不存在绕回问题。
linux内核提供了几个函数,用于判断jiffies
是否发生绕回现象,位于"include/linux/jiffies.h"
中声明。
/*
* These inlines deal with timer wrapping correctly. You are
* strongly encouraged to use them
* 1. Because people otherwise forget
* 2. Because if the timer wrap changes in future you won't have to
* alter your driver code.
*
* time_after(a,b) returns true if the time a is after time b.
*
* Do this with "<0" and ">=0" to only test the sign of the result. A
* good compiler would generate better code (and a really good compiler
* wouldn't care). Gcc is currently neither.
*/
#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)
#define time_after_eq(a,b) \
(typecheck(unsigned long, a) && \
typecheck(unsigned long, b) && \
((long)((a) - (b)) >= 0))
#define time_before_eq(a,b) time_after_eq(b,a)
time_after
返回真,否则返回假time_before
返回真,否则返回假time_after_eq
返回真,否则返回假time_before
返回真,否则返回假jiffies
值,b为待比较的值 类似的,也可以使用上述函数来做短暂延时处理,通过特定时间点的时钟节拍与jiffies
比较,达到延时的目的。以延时10s为例,可以这样编写伪代码。
uint32_t time_out = 0;
time_out = jiffies + (10*Hz);
if (timer_after(jiffies, time_out))
{
/* todo */
}
else
{
/* 延时10s时间到,todo */
}
上述代码中,Hz
是一个linux定义的宏,即是时钟节拍数,位于"include/asm-generic/param.h"
定义。
# undef HZ
# define HZ CONFIG_HZ /* Internal kernel timer frequency */
# define USER_HZ 100 /* some user interfaces are */
# define CLOCKS_PER_SEC (USER_HZ) /* in "ticks" like times() */
2.3节中,我们使用到一个jiffies
和时间换算,linux内核提供了几个常用的jiffies
和时间换算函数,位于"include/linux/jiffies.h"
中声明。
函数 | 功能 |
---|---|
unsigned int jiffies_to_msecs(const unsigned long j); |
jiffies类型转换为毫秒 |
unsigned int jiffies_to_usecs(const unsigned long j); |
jiffies类型转换为微秒 |
u64 jiffies_to_nsecs(const unsigned long j) |
jiffies类型转换为纳秒 |
unsigned long msecs_to_jiffies(const unsigned int m) |
毫秒转换为jiffies类型 |
unsigned long usecs_to_jiffies(const unsigned int u) |
微秒转换为jiffies类型 |
unsigned long nsecs_to_jiffies(u64 n); |
纳秒转换为jiffies类型 |
如果是短暂延时,也可以直接使用linux内核提供的几个延时函数。
函数 | 功能 |
---|---|
mdelay(n) |
毫秒延时 |
void ndelay(unsigned long x) |
微秒延时 |
udelay(n) |
纳秒延时 |
三者函数实现的本质都是忙等待,“mdelay”、"ndelay"
是通过"udelay"
衍生出来的。“mdelay”、"ndelay"
位于"include/linux/delay.h"
定义,"udelay"
位于"include/asm-generic/delay.h"
定义。使用时注意正确引用头文件。
注:
udelay一般适用于一个时间比较短的延时,udelay最大支持2ms延时。如果传入的数大于2000,系统会认为这是一个错误的延时调用。因此如果需要2ms以上的延时需要使用mdelay函数。
linux内核使用struct timer_list
数据结构描述定时器,位于"include/linux/timer.h"
定义。
struct timer_list {
/*
* All fields that change during normal runtime grouped to the
* same cacheline
*/
struct hlist_node entry;
unsigned long expires;
void (*function)(unsigned long);
unsigned long data;
u32 flags;
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
};
使用内核定时器,有几个成员变量需要关注的。
expires
,超时时间,单位:节拍数function
,超时回调处理函data
,私有数据,可以传递给function
使用使用一个内核定时器时,首先就是定义一个定时器实例,然后初始化上述3个成员变量(函数)即可。
引用头文件
#include
定义一个定时器实例,然后调用初始化函数init_timer
初始化。
void init_timer (struct timer_list *timer);
timer
,待初始化的定时器 初始化完成后,调用add_timer
向内核注册定时器,注册后,定时器即启动。
void add_timer (struct timer_list *timer);
timer
,待注册的定时器 也可以调用DEFINE_TIMER
宏完成注册。
DEFINE_TIMER(_name, _function, _expires, _data)
_name
,定时器名称_function
,超时回调函数_expires
,超时时间_data
,私有数据 linux 内核提供了两个删除定时器的函数,分别是del_timer
和del_timer_sync
。使用del_timer
应等待所有处理器调度退出后,才执行删除,否则导致异常。del_timer_sync
带同步性质,函数内部会等待所有处理器调度完再删除定时器,一般推荐使用del_timer_sync
。一个定时器不论是否激活都可以使用这两个函数删除。
int del_timer (struct timer_list *timer);
int del_timer_sync (struct timer_list *timer);
timer
,待删除的定时器注:
del_timer_sync
不能用于中断上下文中
mod_timer
函数用于修改一个定时器的超时时间,该函数会使定时器重新注册到内核,即是如果一个定时器还没激活,调用该函数后,会激活定时器开始运行。
int mod_timer (struct timer_list *timer, unsigned long expires);
timer
,待修改的定时器expires
,待修改超时时间值【1】定义一个定时器实例
【2】编写超时回调处理函数
【3】初始化定时器
【4】设置定时器参数,包括超时时间、超时回调函数、私有数据
【5】注册(启动)定时器
【6】如果需要实现周期定时器,在超时函数内再启动定时器
【7】定时器出口函数(删除)
伪代码
#include
#include
struct timer_list timer;
void timer_function(unsigned long arg)
{
/* todo */
mod_timer(&timer, jiffies+msecs_to_jiffies(10));
}
int timer_init(void)
{
init_timer(&timer);
timer.data = 5;
timer.expires = jiffies + msecs_to_jiffies(5);
timer.function = timer_function;
add_timer(&timer);
return 0;
}
void timer_exit(void)
{
del_timer_sync( &timer );
}