概述
Linux内核中的定时器是一种机制,用于在特定时间间隔内执行某些操作。它是通过内核中的计时器实现的,计时器会在指定的时间间隔内触发一个中断,这个中断用于启动定时器。定时器通常用于处理周期性任务,例如更新屏幕或处理网络流量。
在Linux内核中,有两种类型的定时器:硬件定时器和软件定时器。硬件定时器是由计算机的硬件设备提供的,例如时钟或定时器芯片。而软件定时器则是由内核代码实现的。
要在Linux内核中使用定时器,您需要编写一个处理程序来处理定时器中断。这个处理程序将在定时器触发时自动调用。处理程序应该执行您想要在定时器触发时执行的任务。例如,如果您想要在每秒钟更新屏幕,您可以编写一个处理程序来更新屏幕,并将它与一个每秒钟触发的定时器关联起来。
定时器编程
软件意义上的定时器最终依赖硬件定时器来实现,内核在时钟中断发生后检测各定时器是否到期,到期后的定时器处理函数将作为软中断在底半部执行。实质上,时钟中断处理程序会唤起TIMER_SOFTIRQ软中断,运行当前处理器上到期的所有定时器。
相关API
在Linux内核中,定时器的相关结构体和API主要有以下几个:
-
头文件
#include <linux/timer.h>
-
struct timer_list
结构体:这个结构体定义了定时器的属性,例如定时器的过期时间、定时器的处理函数等。它的定义如下: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 };
其中,
entry
是一个链表节点,用于将定时器添加到内核的定时器链表中。expires
是定时器的过期时间,以 jiffies 为单位。function
是定时器的处理函数,当定时器到期时,内核会调用这个函数。data
是传递给处理函数的参数。 -
init_timer()
函数:这个函数用于初始化一个定时器。它的定义如下:void init_timer(struct timer_list *timer);
您需要先创建一个
struct timer_list
结构体,然后将其传递给init_timer()
函数以进行初始化。 -
setup_timer()
函数:这个函数用于初始化一个定时器,并设置一些数据。它的定义如下:void setup_timer(struct timer_list *timer, void (*function)(unsigned long), unsigned long data);
您需要先创建一个
struct timer_list
结构体和定时器处理函数,然后将其传递给setup_timer()
函数以进行设置并初始化。data参数一般设置为设备结构指针&xxx_dev
,以方便在定时器处理函数中使用。 -
add_timer()
函数:这个函数用于将一个定时器添加到内核的定时器链表中。它的定义如下:int add_timer(struct timer_list *timer);
在调用这个函数之前,您需要设置定时器的过期时间和处理函数。调用这个函数后,定时器将被添加到内核的定时器链表中,直到它被删除或者停止。
-
del_timer()
函数:这个函数用于从内核的定时器链表中删除一个定时器。它的定义如下:int del_timer(struct timer_list *timer);
调用这个函数后,定时器将被从内核的定时器链表中删除,不再触发中断。
-
mod_timer()
函数:这个函数用于修改一个定时器的过期时间。它的定义如下:int mod_timer(struct timer_list *timer, unsigned long expires);
调用这个函数后,定时器的过期时间将被修改。如果定时器已经在内核的定时器链表中,它将被重新排序以反映新的过期时间。
模板
/* xxx设备结构体 */
struct xxx_dev {
struct cdev cdev;
...
timer_list xxx_timer; /* 设备要使用的定时器 */
};
/* xxx驱动中的某函数 */
xxx_func1(…)
{
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 + msecs_to_jiffies(delay_ms);
/* 添加(注册)定时器 */
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);
...
/* 调度定时器再执行 */
mod_timer(&dev->xxx_timer, jiffies + msecs_to_jiffies(delay_ms));
...
}
jiffies
是 Linux 内核中的一个全局变量,它表示自系统启动以来的时钟滴答数。在内核中,许多定时器和时间相关的操作都是以 jiffies 为单位进行的。
msecs_to_jiffies()
是一个函数,用于将毫秒转换为 jiffies。它的定义如下:
unsigned long msecs_to_jiffies(const unsigned int m);
unsigned long usecs_to_jiffies(const unsigned int u);
unsigned long timespec_to_jiffies(const struct timespec *value);
因此,jiffies + msecs_to_jiffies(delay_ms)
的含义是:在当前时间的基础上,向前延迟 delay_ms 毫秒,并转换为 jiffies 单位。这个值将作为定时器的过期时间,当这个时间到达时,定时器的处理函数将被调用。
在定时器处理函数中,在完成相应的工作后,往往会延后expires并将定时器再次添加到内核定时器链表中,以便定时器能再次被触发。
高精度定时器
Linux内核中的高精度定时器是一种可调度的软件定时器,它提供了纳秒级别的精度,可以用于实现高精度的事件处理,如网络协议栈、实时媒体处理等。在内核中,高精度定时器是通过一个称为hrtimer的结构体来实现的。
下面是使用高精度定时器的基本步骤:
-
创建一个hrtimer结构体,设置定时器的超时时间和回调函数。
-
将定时器添加到内核的定时器队列中,等待触发。
-
当定时器超时时,内核会调用回调函数,执行相应的操作。
-
如果需要,可以在回调函数中重新设置定时器的超时时间,并将其重新添加到定时器队列中。
可以从sound/soc/fsl/imx-pcm-fiq.c中提取出一个使用范例,如以下代码所示。
#include <linux/hrtimer.h>
static enum hrtimer_restart snd_hrtimer_callback(struct hrtimer *hrt)
{
...
hrtimer_forward_now(hrt, ns_to_ktime(iprtd->poll_time_ns));
return HRTIMER_RESTART;
}
static int snd_imx_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
struct snd_pcm_runtime *runtime = substream->runtime;
struct imx_pcm_runtime_data *iprtd = runtime->private_data;
switch (cmd) {
case SNDRV_PCM_TRIGGER_START:
case SNDRV_PCM_TRIGGER_RESUME:
case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
...
hrtimer_start(&iprtd->hrt, ns_to_ktime(iprtd->poll_time_ns),
HRTIMER_MODE_REL);
}
...
}
static int snd_imx_open(struct snd_pcm_substream *substream)
{
...
hrtimer_init(&iprtd->hrt, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
iprtd->hrt.function = snd_hrtimer_callback;
...
return 0;
}
static int snd_imx_close(struct snd_pcm_substream *substream)
{
...
hrtimer_cancel(&iprtd->hrt);
...
}
在声卡打开的时候通过hrtimer_init()初始化了hrtimer,并指定回调函数为snd_hrtimer_callback();在启动播放(第15~21行SNDRV_PCM_TRIGGER_START)等时刻通过hrtimer_start()启动了hrtimer;iprtd->poll_time_ns纳秒后,时间到snd_hrtimer_callback()函数在中断上下文被执行,它紧接着又通过hrtimer_forward_now()把hrtimer的时间前移了iprtd->poll_time_ns纳秒,这样周而复始;直到声卡被关闭,后面又调用了hrtimer_cancel()取消在open时初始化的hrtimer。
其它
在定时器的代码实例中我们可以看到它的精度依赖于jiffies
,jiffies
是 Linux 内核中的一个全局变量,它表示自系统启动以来的时钟滴答数。系统时钟滴答的快慢是由系统频率决定的。系统频率是可以设置的,可以通过图形化界面设置。
如何查看系统频率呢?
可以在源码中查看:
# .config文件
CONFIG_HZ_FIXED=0
CONFIG_HZ_100=y
# CONFIG_HZ_200 is not set
# CONFIG_HZ_250 is not set
# CONFIG_HZ_300 is not set
# CONFIG_HZ_500 is not set
# CONFIG_HZ_1000 is not set
CONFIG_HZ=100
系统的频率是100HZ, 因为HZ表示一秒的节拍数,所以系统频率精度是10ms,也就是说jiffies
的精度是10ms。所以定时器的最高精度也就是10ms,定时时间低于10ms的会不准。我们可以做一个实验来验证,如下:
printk("%s: msecs_to_jiffies(1):%ld\n", __func__, msecs_to_jiffies(1));
printk("%s: msecs_to_jiffies(2):%ld\n", __func__, msecs_to_jiffies(2));
printk("%s: msecs_to_jiffies(10):%ld\n", __func__, msecs_to_jiffies(10));
printk("%s: msecs_to_jiffies(11):%ld\n", __func__, msecs_to_jiffies(11));
printk("%s: msecs_to_jiffies(20):%ld\n", __func__, msecs_to_jiffies(20));
printk("%s: usecs_to_jiffies(1):%ld\n", __func__, usecs_to_jiffies(1));
printk("%s: usecs_to_jiffies(2):%ld\n", __func__, usecs_to_jiffies(2));
printk("%s: usecs_to_jiffies(10):%ld\n", __func__, usecs_to_jiffies(10));
printk("%s: usecs_to_jiffies(11):%ld\n", __func__, usecs_to_jiffies(11));
printk("%s: usecs_to_jiffies(11):%ld\n", __func__, usecs_to_jiffies(10000));
/driver # insmod hello.ko
hello_init: msecs_to_jiffies(1):1
hello_init: msecs_to_jiffies(2):1
hello_init: msecs_to_jiffies(10):1
hello_init: msecs_to_jiffies(11):2
hello_init: msecs_to_jiffies(20):2
hello_init: usecs_to_jiffies(1):1
hello_init: usecs_to_jiffies(2):1
hello_init: usecs_to_jiffies(10):1
hello_init: usecs_to_jiffies(11):1
hello_init: usecs_to_jiffies(10000):1
从以上实验可以看到,虽然系统提供了msecs_to_jiffies
和usecs_to_jiffies
,但不一定是百分百准确了,要依赖于当前系统频率。所以当我们需要使用到高于系统频率的精度时,要使用高精度定时器。
内核延时
短延时
Linux内核中提供了下列3个函数以分别进行纳秒、微秒和毫秒延迟:
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
上述延迟的实现原理本质上是忙等待,它根据CPU频率进行一定次数的循环。有时候,人们在软件中进行下面的延迟:
void delay(unsigned int time)
{
while(time--);
}
ndelay()、udelay()和mdelay()函数的实现方式原理与此类似。内核在启动时,会运行一个延迟循环校准(Delay Loop Calibration),计算出lpj(Loops Per Jiffy),内核启动时会打印如下类似信息:
Calibrating delay loop (skipped), value calculated using timer frequency.. 6.00 BogoMIPS (lpj=30000)
如果我们直接在bootloader传递给内核的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的实例。
1 /* 延迟100个jiffies */
2 unsigned long delay = jiffies + 100;
3 while(time_before(jiffies, delay));
4
5 /* 再延迟2s */
6 unsigned long delay = jiffies + 2*Hz;
7 while(time_before(jiffies, delay));