Linux驱动开发之定时器

 

概述

Linux内核中的定时器是一种机制,用于在特定时间间隔内执行某些操作。它是通过内核中的计时器实现的,计时器会在指定的时间间隔内触发一个中断,这个中断用于启动定时器。定时器通常用于处理周期性任务,例如更新屏幕或处理网络流量。

在Linux内核中,有两种类型的定时器:硬件定时器和软件定时器。硬件定时器是由计算机的硬件设备提供的,例如时钟或定时器芯片。而软件定时器则是由内核代码实现的。

要在Linux内核中使用定时器,您需要编写一个处理程序来处理定时器中断。这个处理程序将在定时器触发时自动调用。处理程序应该执行您想要在定时器触发时执行的任务。例如,如果您想要在每秒钟更新屏幕,您可以编写一个处理程序来更新屏幕,并将它与一个每秒钟触发的定时器关联起来。

定时器编程

软件意义上的定时器最终依赖硬件定时器来实现,内核在时钟中断发生后检测各定时器是否到期,到期后的定时器处理函数将作为软中断在底半部执行。实质上,时钟中断处理程序会唤起TIMER_SOFTIRQ软中断,运行当前处理器上到期的所有定时器。

相关API

在Linux内核中,定时器的相关结构体和API主要有以下几个:

  1. 头文件

     #include <linux/timer.h>
    
  2. 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 是传递给处理函数的参数。

  3. init_timer() 函数:这个函数用于初始化一个定时器。它的定义如下:

    void init_timer(struct timer_list *timer);
    

    您需要先创建一个 struct timer_list 结构体,然后将其传递给 init_timer() 函数以进行初始化。

  4. setup_timer() 函数:这个函数用于初始化一个定时器,并设置一些数据。它的定义如下:

    void setup_timer(struct timer_list *timer, void (*function)(unsigned long), unsigned long data);
    

    您需要先创建一个 struct timer_list 结构体和定时器处理函数,然后将其传递给 setup_timer() 函数以进行设置并初始化。data参数一般设置为设备结构指针&xxx_dev,以方便在定时器处理函数中使用。

  5. add_timer() 函数:这个函数用于将一个定时器添加到内核的定时器链表中。它的定义如下:

    int add_timer(struct timer_list *timer);
    

    在调用这个函数之前,您需要设置定时器的过期时间和处理函数。调用这个函数后,定时器将被添加到内核的定时器链表中,直到它被删除或者停止。

  6. del_timer() 函数:这个函数用于从内核的定时器链表中删除一个定时器。它的定义如下:

    int del_timer(struct timer_list *timer);
    

    调用这个函数后,定时器将被从内核的定时器链表中删除,不再触发中断。

  7. 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的结构体来实现的。

下面是使用高精度定时器的基本步骤:

  1. 创建一个hrtimer结构体,设置定时器的超时时间和回调函数。

  2. 将定时器添加到内核的定时器队列中,等待触发。

  3. 当定时器超时时,内核会调用回调函数,执行相应的操作。

  4. 如果需要,可以在回调函数中重新设置定时器的超时时间,并将其重新添加到定时器队列中。

可以从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。

其它

在定时器的代码实例中我们可以看到它的精度依赖于jiffiesjiffies 是 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_jiffiesusecs_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));