Linux内核_软件定时器

1 内核节拍

1.1 系统节拍

Linux内核通过CONFIG_HZ来配置系统节拍频率,打开文件 include/asm-generic/param.h,有如下内容:

#ifndef __ASM_GENERIC_PARAM_H
#define __ASM_GENERIC_PARAM_H

#include <uapi/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() */
#endif /* __ASM_GENERIC_PARAM_H */

其中CONFIG_HZ表示系统节拍。

1.2 全局jiffies变量

Linux 内核使用全局变量 jiffies 来记录系统从启动以来的系统节拍数,系统启动的时候会将 jiffies 初始化为 0,jiffies按照CONFIG_HZ设定的频率计数,表示内核的节拍数,jiffies 定义在文件 include/linux/jiffies.h 中,定义如下:

extern u64 __jiffy_data jiffies_64;
extern unsigned long volatile __jiffy_data jiffies;

iffies_64 和 jiffies 其实是同一个东西,jiffies_64 用于 64 位系统,而 jiffies 用于 32 位系统。 为了兼容不同的硬件,jiffies 其实就是 jiffies_64 的低 32 位。

Linux内核提供的处理时间绕回:

函数 描述
time_after(unkown, known) unkown在known之后,则返回真
time_before(unkown, known)
time_after_eq(unkown, known)
time_before_eq(unkown, known)

Linux 内核提供了几个 jiffies 和 ms、us、ns 之间的转换函数如下:

函数 描述
int jiffies_to_msecs(const unsigned long j) 将毫秒转换为 jiffies 类型。
int jiffies_to_usecs(const unsigned long j) 将微秒转换为 jiffies 类型。
u64 jiffies_to_nsecs(const unsigned long j) 将纳秒转换为 jiffies 类型。
long msecs_to_jiffies(const unsigned int m) 将 jiffies 类型的参数 j 分别转换为对应的毫秒。
long usecs_to_jiffies(const unsigned int u) 将 jiffies 类型的参数 j 分别转换为对应的微秒。
unsigned long nsecs_to_jiffies(u64 n) 将 jiffies 类型的参数 j 分别转换为对应的纳秒。

1.3 深入系统滴答

在开发板执行以下命令,可以看到 CPU0 下有一个数值变化特别快,它就是滴答中断:

# cat /proc/interrupts
CPU0
16: 2532 GPC 55 Level i.MX Timer Tick
19: 22 GPC 33 Level 2010000.ecspi
20: 384 GPC 26 Level 2020000.serial
21: 0 GPC 98 Level sai
  • 以 100ASK_IMX6ULL 为例,滴答中断名字就是“i.MX Timer Tick”。在 Linux内核源码目录下执行以下命令:
$ grep "i.MX Timer Tick" * -nr
drivers/clocksource/timer-imx-gpt.c:319: act->name = "i.MX Timer Tick";
  • 打开 timer-imx-gpt.c 319 行左右,可得如下源码:
act->name = "i.MX Timer Tick";
act->flags = IRQF_TIMER | IRQF_IRQPOLL;
act->handler = mxc_timer_interrupt;
act->dev_id = ced;
return setup_irq(imxtm->irq, act);
  • mxc_timer_interrupt 应该就是滴答中断的处理函数,代码如下:
static irqreturn_t mxc_timer_interrupt(int irq, void *dev_id)
{
    struct clock_event_device *ced = dev_id;
    struct imx_timer *imxtm = to_imx_timer(ced);
    uint32_t tstat;
    tstat = readl_relaxed(imxtm->base + imxtm->gpt->reg_tstat);
    imxtm->gpt->gpt_irq_acknowledge(imxtm);
    ced->event_handler(ced);
    return IRQ_HANDLED;
}

对于jiffies的累加,是在ced->event_handler(ced)中进行的,最终调用的是tick_handle_periodic函数。

2 内核定时器

2.1 timer_list结构

定时器是一个很常用的功能,需要周期性处理的工作都要用到定时器。Linux 内核定时器 采用系统时钟来实现。内核定时器并不是周期性运行的,超时以后就会自动关闭。 Linux 内核使用 timer_list 结构体表示内核定时器,timer_list 定义在文件include/linux/timer.h 中:

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
};

  1. 在 add_timer 之前,直接修改:
timer.expires = jiffies + xxx; // xxx 表示多少个滴答后超时,也就是 xxx*10ms
timer.expires = jiffies + 2*HZ; // HZ 等于 CONFIG_HZ,2*HZ 就相当于 2 秒
  1. 在 add_timer 之后,使用 mod_timer 修改:
mod_timer(&timer, jiffies + xxx); // xxx 表示多少个滴答后超时,也就是 xxx*10ms
mod_timer(&timer, jiffies + 2*HZ); // HZ 等于 CONFIG_HZ,2*HZ 就相当于 2 秒

要使用内核定时器首先要先定义一个 timer_list 变量,表示定时器:

  • tiemr_list 结构体的expires 成员变量表示超时时间,单位为节拍数。比如我们现在需要定义一个周期为 2 秒的定时器,那么这个定时器的超时时间就是 jiffies+(2HZ),因此 expires=jiffies+(2HZ);

  • function 就是定时器超时以后的定时处理函数,我们要做的工作就放到这个函数里面,需要我们编写这个定时处理函数。

2.1 内核定时器相关API

  • init_timer (4.15之后好像弃用,改用timer_setup)

    初始化定时器,定义一个timer_list类型的变量之后需要通过init_timer对其初始化:

void init_timer(struct timer_list *timer)
  • add_timer

add_timer 函数用于向 Linux 内核注册定时器,使用 add_timer 函数向内核注册定时器以后,定时器就会开始运行,函数原型如下:

void add_timer(struct timer_list *timer)
  • del_timer

del_timer 函数用于删除一个定时器,不管定时器有没有被激活,都可以使用此函数删除。在多处理器系统上,定时器可能会在其他的处理器上运行,因此在调用 del_timer 函数删除定时器之前要先等待其他处理器的定时处理器函数退出。del_timer 函数原型如下:

int del_timer(struct timer_list * timer)

返回值:0,定时器还没被激活;1,定时器已经激活。

  • del_timer_sync

del_timer_sync 函数是 del_timer 函数的同步版,会等待其他处理器使用完定时器再删除,del_timer_sync 不能使用在中断上下文中。del_timer_sync 函数原型如下所示:

int del_timer_sync(struct timer_list *timer)

返回值:0,定时器还没被激活;1,定时器已经激活。

  • mod_timer

mod_timer 函数用于修改定时值,如果定时器还没有激活的话,mod_timer 函数会激活定时 器!函数原型如下:

int mod_timer(struct timer_list *timer, unsigned long expires)

函数参数和返回值含义如下: timer:要修改超时时间(定时值)的定时器。 expires:修改后的超时时间。 返回值:0,调用 mod_timer 函数前定时器未被激活;1,调用 mod_timer 函数前定时器已被激活。

  • timer_setup

    初始化定时器,定义一个timer_list类型的变量之后需要通过init_timer对其初始化:

void timer_setup(struct timer_list *timer, void (*callback)(struct timer_list *), unsigned int flags);

2.2 定时器的内部机制

定时器就是通过软件中断来实现的,它属于 TIMER_SOFTIRQ 软中断。对于 TIMER_SOFTIRQ 软中断,初始化代码如下:

void __init init_timers(void)
{
	/* ensure there are enough low bits for flags in timer->base pointer */
	BUILD_BUG_ON(__alignof__(struct tvec_base) & TIMER_FLAG_MASK);

	init_timer_cpus();
	init_timer_stats();
	timer_register_cpu_notifier();
	open_softirq(TIMER_SOFTIRQ, run_timer_softirq);
}

当发生硬件中断时,硬件中断处理完后,内核会调用软件中断的处理函数。对于 TIMER_SOFTIRQ,会调用 run_timer_softirq,它的函数如下:

static void run_timer_softirq(struct softirq_action *h)
{
	struct tvec_base *base = __this_cpu_read(tvec_bases);

	hrtimer_run_pending();

	if (time_after_eq(jiffies, base->timer_jiffies))
		__run_timers(base);
}

TIMER_SOFTIRQ的处理函数中,会从链表中把这些超时的timer取出来,执行其中的函数。

2.3 内核定时器使用流程

struct timer_list timer;

void timer_function(unsigned long arg) {
    /** 处理代码 */
    /** 
        如果需要周期性运行,则使用mod_timer重新设置超时时间并启动定时器。
    */
    mod_timer(&timer, jiffies + msces_to_jiffies(2000));
}

void init(void) {
    init_timer(&timer);                             // 初始化定时器
    timer.function = function;                          // 设置定时处理函数
    timer.expires = jiffies + msecs_to_jiffiess(2000);  // 设置超时时间
    add_timer(&timer);                                  /// 启动定时器
}



void exit(void) {
    del_timer(&timer);  // 删除定时器
    /** 或者使用 */
    del_timer_sync(&timer);
}

3 内核短延时函数

函数 描述
void ndelay(unsigned long nsecs) 纳秒延时
void udelay(unsigned long usecs) 微妙延时
void mdelay(unsigned long msecs) 毫秒延时