【Linux驱动编程】如何使用内核定时器

文章目录

  • 1 内核定时器
    • 1.1 内核定时器特点
    • 1.2 内核定时器使用原则
  • 2 时钟节拍描述
    • 2.1 时钟节拍(tick rate)
      • 时钟节拍范围
    • 2.2 时钟节拍描述
    • 2.3 jiffies绕回问题
    • 2.4 jiffies与时间换算
  • 3 内核定时器描述
    • 3.1 内核定时器常用API
      • 初始化定时器
      • 注册定时器
      • 删除定时器
      • 修改超时时间
    • 3.2 内核定时器使用步骤


1 内核定时器

  定时器是编程中常用到的一个机制,在实际项目中几乎是不可避免的,常见表现有以下场景。

  • 周期性执行一个任务
  • 短暂延时等待
  • 执行时间统计
      不仅仅是应用程序中使用到定时器,在linux内编程中也经常使用到。如编写驱动时不可避免使用定时器,按键消抖、延时等待硬件就绪等等。

1.1 内核定时器特点

  内核定时器是内核用于控制在未来某个时间点或者特定时间段内调度执行某个函数的一种机制。内核定时器是一个软定时器,软定时器最终依赖于cpu的硬件定时器实现,对于linux内核来说,依赖于系统时钟节拍。既然是软定时器,就存在软中断,处理函数在软中断中执行。另外,内核定时器还有个特点就是,定时器只会执行一次,超时后即退出,如果需要一个周期性的定时器,需要在超时处理函中重新开启定时器。内核定时器特点归纳起来就是几点:

  • 内核定时器是一个软定时器
  • 引起TIMER_SOFTIRQ软中断
  • 依赖于系统时钟节拍
  • 只执行一次

1.2 内核定时器使用原则

  内核定时器基于“软中断”机制,被调度函数在软中断的下半部执行,就需遵循软中断的使用原则,因此函数被调度的时候是处于非进程的上下文中。

  • 不允许访问进程空间,因为没有进程上下文,与中断进程没有任何关联
  • 不能调用会引起睡眠的函数或者调度机制(如semaphore、mutex
  • 针对并发数据的保护机制

2 时钟节拍描述

2.1 时钟节拍(tick rate)

  内核定时器依赖于系统时钟节拍,我们有必要了解时钟节拍是什么。时钟节拍即是1秒内系统时钟的中断次数,时钟节拍的高低决定软定时器的最小精度。以典型的100Hz时钟节拍来说,软定时器的最小精度是10ms。linux内核的时钟节拍是可以配置,一般是100Hz,随着cpu性能的增加,可以适当提高时钟节拍,但一般不会超过1000Hz。

---> Kernel Features
   ---> Timer frequency

【Linux驱动编程】如何使用内核定时器_第1张图片

内核时钟节拍配置

时钟节拍范围

  时钟节拍的选择范围是100—1000Hz,为什么不能太低或者太高呢?我们知道操作系统调度和时间管理都是基于时钟节拍的,如果时钟节拍太低,会使系统调度实时性降低,时间管理精度降低,100Hz的时间节拍是经过长时间的考验,是最合理的时钟节拍。


  既然时钟节拍决定系统调度和时间管理,那为什么不是越高越好?较高的时钟节拍有一定的优势,就是时间精度提高了,能够适用于时间要求更严格的场景。时钟节拍是系统时钟1秒内的中断次数,提高时钟节拍,意味着cpu节拍定时器的中断频率增加,这会增加cpu的负荷,使得cpu需花费更多的时间去处理中断信息。cpu 主要时间都去处理中断信息了,那就无法及时处理操作系统的任务调度,这是不能允许的(软件时不时卡住了能忍不?)。因此,系统的时钟节拍不是越高越好,非苛刻的场合,采用默认的时钟节拍(100Hz)可以满足需求了。当然,现在的cpu性能越来越强,而且是多核,即使使用1000Hz的时钟节拍,增加的cpu资源比例也是很低很低,在可以接受范围。


2.2 时钟节拍描述

  linux内核使用一个全局变量jiffies(64位系统为jiffies_64)来描述(记录)从系统启动以来产生的节拍的总数,系统启动时,内核将jiffies初始化为0,之后cpu时钟每次时钟中断一次,变量执行自加。根据时钟节拍描述,我们可以使用时钟节拍来实现功能:

  • 计算系统运行时间,t = jiffies/Hz
  • 软定时器实现
  • 延时函数
/*
 * 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位系统下,jiffiesjiffies_64表示同一个值。因此,建议在编程时,统一使用jiffies,保证程序的兼容性。

【Linux驱动编程】如何使用内核定时器_第2张图片

jiffies内存空间

2.3 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)
  • a小于b,time_after返回真,否则返回假
  • a大于b,time_before返回真,否则返回假
  • a小于等于b,time_after_eq返回真,否则返回假
  • a大于等于b,time_before返回真,否则返回假
  • a一般传入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.4 jiffies与时间换算

  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函数。


3 内核定时器描述

  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个成员变量(函数)即可。


3.1 内核定时器常用API

引用头文件

#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_timerdel_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,待删除的定时器
  • 返回值,如果定时器还没激活返回0,否则返回1

注:
del_timer_sync不能用于中断上下文中


修改超时时间

  mod_timer 函数用于修改一个定时器的超时时间,该函数会使定时器重新注册到内核,即是如果一个定时器还没激活,调用该函数后,会激活定时器开始运行。

int mod_timer (struct timer_list *timer, unsigned long expires);
  • timer,待修改的定时器
  • expires,待修改超时时间值
  • 返回值,修超时时间前还没激活返回0,否则返回1

3.2 内核定时器使用步骤

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

你可能感兴趣的:(Linux驱动编程)