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.