Linux内核定时器使用及说明

2025-12-06 19:50

本文主要是介绍Linux内核定时器使用及说明,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《Linux内核定时器使用及说明》文章详细介绍了Linux内核定时器的特性、核心数据结构、时间相关转换函数以及操作API,通过示例展示了如何编写和使用定时器,包括按键消抖的应用...

1.Linux内核定时器特征

  1. 依赖Linux系统心跳节拍发生器的定时器;
  2. 基于未来的时间点的一种定时方式;
  3. 支持多段定时,互不干扰:一段定时还没有完成,可以再启动新一段定时;
  4. 内核定时器支持的是单次定时方式,不是循环定时;
  5. 内核定时器定时精度不高,如果需要高精度定时需要使用硬件定时器或使用内核高精度定时器;
  6. 定时器的超时处理函数只会在注册它的CPU上运行,不会存在并发问题。

2.Linux内核定时器核心数据结构

内核使用struct timer_list表示一个内核定时器,定义如下:

struct timer_list {
       struct hlist_node entry;                                    //实现定时器结构链表化管理
       unsigned long             expires;                          //到期时间点      不是时长
       void                     (*function)(unsigned long);        //超时处理函数
       unsigned long             data;
       u32               flags;
       int                 slack;
};

重要成员:

到期时间,基于未来时间点的时钟节拍数。等于定时时刻当前的时钟节拍计数(存储在系统的全局expires变量jiffies)+定时时长对应的时钟节拍数量。

示例:从现在开始定时1秒:expires = jiffies + 1*Hz

Hz:表示一秒对应的时钟节拍数

  • function超时处理函数,并不是硬件中断服务程序。
  • data传递给超时处理函数的参数,可以把一个变量的地址转换成unsigned long

3.Linux内核时间相关转换函数

定时使用的不是时长,是基于未来时间点,单位是时钟节拍。

HZ宏:1秒对应时钟节拍。

unsigned long msecs_to_jiffies(const unsigned int m)             //把ms转换为时钟节拍
unsigned long usecs_to_phpjiffies(const unsigned int u)             //把us转换为时钟节拍

示例:定时2s+200ms+500us

到期时间:jiffies + 2*HZ+msecs_to_jiffies(200)+msecs_to_jiffies(500)

4.Linux内核定时器操作相关API

4.1 静态定义结构体变量并且初始化(宏):DEFINE_TIMER

头文件

#include <linux/timer.h>

原型

#define DEFINE_TIMER(_name, _function, _expires, _data)             \

struct timer_list _name = TIMER_INITIALIZER(_function, _expires, _data)

参数

_name变量名

_function超时处理函数

_expires到期时间,一般在启动定时前需要重新初始化。

_data传递给超时处理函数的参数

功能

定义一个定时器结构并且初始化function, expires, data成员。

4.2 动态初始化定时器(宏):setup_timer

头文件

#include <linux/timer.h>

原型

#define setup_timer(timer, fn, data)      __setup_timer((timer), (fn), (data), 0)

参数

timer定时器结构指针

n超时处理函数

data超时处理函数参数

功能

运行时初始化定时器

struct timer_list btn_timer;
setup_timer(&btn_timer, fn, data)
btn_timer.expires = jiffies+定时时长对应的时钟节拍数;
启动定时器。。。

4http://www.chinasem.cn.3 注册定时器到内核:add_timer

头文件

#include <linux/timer.h>

原型

void add_timer(struct timer_list *timer)

返回值

功能

向内核注册一个定时器,同步启动定时。本质上把定时器结构添加到定时链表

注意:同一个结构变量在启动定时后,没有超时前重复注册没有用的。

struct timer_list timer,
……
add_timer(&timer);
add_timer(&timer);
……

4.4 注销内核定时器:del_timer,del_timer_sync

头文件

#include <linux/timer.h>

原型

int del_timer(struct timer_list *timer)

int del_timer_sync(struct timer_list *timer)

功能

从内核中注销一个定时器。(本质上把定时器结构从定时链表取走)

区别:  del_timer 单处理器系统安全删除定时器函数,del_timer_sync SMP系统安全删除定时器的函数。

4.5 修改定时器定时时间:mod_timer

头文件

#include <linux/timer.h>

原型

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

参数

timer定时器指针;

expires未来的到期时钟节拍。

功能

修改定时器时间以及启动定时。

5.Linux内核定时器示例

5.1 内核定时器编程步骤

  • 编写一个定时器超时处理函数。
  • 定义struct timer_list结构变量。
  • 初始化上一步定义的结构变量。
  • 重新初始化到期时间,马上启动定时器(注册定时器)。
  • 如果不需要定时器,可以删除定时器(注销定时器),这一步是可选。

5.2 定时2s

#include<linux/module.h>
#include<linux/init.h>
#include<linux/timer.h>

static void timer_func(unsigned long data);

//静态定义:定义并且初始化
DEFINE_TIMER(my_timer,timer_func,0,(unsigned long)"LIU");

//定时器超时函数
static void timer_func(unsigned long data)
{
	printk("data:%s\r\n",(char *)data);
	//启动新一次定时,实现循环定时
	mod_timer(&my_timer,jiffies + 2*HZ);
}

static int __init timer_list_init(void)
{
	//重新初始化到期时间
	my_timer.expires = jiffies + 2*HZ;	//2s
	
	//马上启动定时器
	printk("-----begin------\r\n");
	add_timer(&my_timer);
	printk("------end-------\r\n");
    return 0; 
}

static void __exit timer_list_exit(void)
{
	//删除定时器
	del_timer_sync(&my_timer);
}

module_init(timer_list_init);			// 指定模块初始化函数
module_exit(timer_list_exit);			// 指定模块清理函数
MODULE_LICENSE("GPL");					// 声明模块许可证(GPL v2) 

Linux内核定时器使用及说明

5.3 循环定时

#include<linux/module.h>
#include<linux/init.h>
#include<linux/timer.h>

static void timer_func(unsigned long data);

//静态定义:定义并且初始化
DEFINE_TIMER(my_timer,timer_func,0,(unsigned long)"Time out");

//定时器超时函数
static void timer_func(unsigned long data)
{
	printk("data:%s\r\n",(char *)data);
	//启动新一次定时,实现循环定时
	mod_timer(&my_timer,jiffies + 2*HZ);
}

static int __init timer_list_init(void)
{
	//重新初始化到期时间
	my_timer.expires = jiffies + 2*HZ;	//2s
	
	//马上启动定时器
	printk("-----begin------\r\n");
	add_timer(&my_timer);
	printk("------end-------\r\n");
    return 0; 
}

static void __exit timer_list_exit(void)
{
	//删除定时器
	del_timer_sync(&my_timer);
}

module_init(timer_list_init);			// 指定模块初始化函数
module_exit(timer_list_exit);			// 指定模块清理函数
MODULE_LICENSE("GPL");					// 声明模块许可证(GPL v2) 

Linux内核定时器使用及说明

5.4 全局驱动数据

#include <linux/module.h>
#include <linux/init.h>
#include <linux/timer.h>

//全局驱动数据
struct drv_data{
	struct timer_list timer;
	char *data;
	u64 expires;
};
static struct drv_data gvar;

//定时器超时处理函数
static void timer_func(unsigned long data)
{
	struct drv_data *pdata = (struct drv_data *)data;
	printk("data:%s\r\n",pdata->data);
	//启动新一次定时,实现循环定时
	mod_timer(&pdata->timer,jiffies + pdata->expires);
}

static int __init timer_list_init(void)
{
	//普通成员初始化
	gvar.data    = "Time out";
	gvar.expires = 2*HZ;
	
	//初始化定时器
	setup_timer(&gvar.timer,timer_func, (unsigned long)&gvar);
	
	//重新初始化到期时间
	gvar.timer.expires = jiffies + gvar.expires;	//2s
	
	//马上启动定时器
	printk("-----begin------\r\n");
	add_timer(&gvar.timer);
	printk("------end-------\r\n");
    return 0; 
}

static void __exit timer_list_exit(void)
{
	//删除定时器
	del_timer_sync(&gvar.timer);
}
module_init(timer_list_init);			// 指定模块初始化函数
module_exit(timer_list_exit);			// 指定模块清理函数
MODULE_LICENSE("GPL");					// 声明模块许可证(GPL v2) 

Linux内核定时器使用及说明

5.5 Linux内核定时器应用:实现按键消抖

5.5.1 按键的抖动分析

Linux内核定时器使用及说明Linux内核定时器使用及说明

裸机按键:检测,   延时,  再检测,确认按键

传统的防抖方法用户体验较差,因编程为延时时间难以精确设定,还会占用较多的CPU资源。

早期的按键驱动程序设计:当按键中断触发时,直接读取按键状态来判断按键动作,这种方法容易受到机械抖动的影响。

引入内核定时器机制后,可以在按键中断触发时启动一个定时任务。待定时器超时时,在其回调函数中读取按键的状态值。利用这一机制,可以每次在未超时时重新设置定时器,从而确保每次从当前时刻重新开始计算延时,有效避免了由于抖动带来的误判。这种方式不仅提高了检测的准确性,还优化了系统资源的使用效率。

中断程序启动定时器,每次从当前时间开始定时10ms,中断程序返回

  1. 当出现第一个下降沿,进入中断,启动10ms定时,退出中断
  2. 定时10ms过程中,出现抖动,上升沿出现,进入中断,启动10ms定时,退出中断
  3. 定时10ms过程中,出现抖动,下降沿出现,进入中断,启动10ms定时,退出中断
  4. 重复2,3步骤……直到最后一次抖动边沿,10ms后就有机会超时。
  5. 在超时函数读取按键状态

松开检测方法相同。

5.5.2 按键驱动程序改进

drv-btn.c

//增加内核定时器功能
#include<linux/module.h>
#include<linux/fs.h>
#include<linux/miscdevice.h>
#include<asm/uAccess.h>
#include<linux/interrupt.h>
#include<linux/irq.h>
#include<linux/gpio.h>
#include<linux/slab.h>

#define LEDS_MINOR	255
#define DEVICE_NAME  "my-buttons" // 设备节点名称,将在/dev目录下创建 

#ifndef ARRAYSIZE
#define ARRAYSIZE(a)	(sizeof(a)/sizeof(a[0]))
#endif

//按键数量,在模块初始化函数中进行计算
static int key_size;
//按键缓冲区,一个元素存放一个按键值,'1'表示按下,'0'表示松开
//在模块的初始化函数中分配缓冲区空间
static char *keys_buf;	

//使用面向对象思想设计按键,把一个按键信息进行封装
struct key_info{
	int id;					//按键编号
	int gpio;				//统一的GPIO编号
	unsigned long flags;	//触发方式
	char *name;				//按键名
	int irq;				//中断编号
	struct timer_list timer;//增加一个定时器作消抖
};

//实例化对象
static struct key_info keys[]={
	[0]={
		.id 	= 0,
		.gpio 	= 5,
		.flags 	= IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
		.name 	= "key-0",
	},
	[1]={
		.id 	= 0,
		.gpio 	= 54,
		.flags 	= IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
		.name 	= "key-js1",
	},	
};

static int key_open (struct inode *pinode, struct file *pfile)
{
    printk("line:%d,%s is call\n",__LINE__,__FUNCTION__);
    return 0;    
}

loff_t key_llseek (struct file *pfile, loff_t offset, int whence)
{
    return 0;    
}

static ssize_t key_read (struct file *pfile, char __user *buf, size_t count, loff_t *offset)    
{
    int ret;
	if(count > key_size)	count = key_size;
    if(count == 0)			return 0;
	
	//准备数据,但是按键数据在中断中实时更新,不需要在这里读取
	//复制数据到用户空间
	ret = copy_to_user(buf, keys_buf, count);
	if(ret){
		printk("error:copy_to_user\r\n");
		ret = -EFAULT;
		goto errot_copy_to_user;
	}
	return count;  // 返回成功读取的字节数  
errot_copy_to_user:
	return ret;
}

int key_release (struct inode *pinode, struct file *pfile)
{
    return 0;
}

// 文件操作结构体(关联实现的操作函数) 
static const struct file_operations dev_fops = {
    .open = key_open,      // 打开设备 
    .read = key_read,      // 读取设备状态 
	.owner = THIS_MODULE,
};

// 杂项设备定义 
static struct miscdevice key_device = {
    .name = DEVICE_NAME,	// 设备名称(出现在/dev目录下) 
    .minor = LEDS_MINOR, 	// 次设备号(建议使用动态分配) 
    .fops = &dev_fops,		// 关联文件操作函数集 
};

//超时处理函数
void btn_timer(unsigned long data)
{

	int s;
	struct key_info *pdata = (struct key_info *)data;

	//检测当前的电平状态
	s = !gpio_get_value(pdata->gpio);
	keys_buf[pdata->id]='0'+s;	//保存状态
}

//按键中断函数
//设置了双边触发,按下和松开都会进入这个函数
irqreturn_t btns_irq_handler(int irq,void *devid)
{
	struct key_info *pdata = (struct key_info *)devid;

	//启动新一次定时
	mod_timer(&pdata->timer,jiffies + msecs_to_jiffies(20));
	return IRQ_HANDLED;
}

static int __init xyd_btn_init(void)
{
    int ret,i;

	key_size = ARRAY_SIZE(keys);	//计算按键数量

	//分配按键缓冲区
	keys_buf = kzalloc(key_size,GFP_KERNEL);
	if(keys_buf == NULL)
		return -EFAULT;
	
	//循环注册中断
	for(i = 0;i < key_size;i++){
		keys[i].irq = gpio_t编程o_irq(keys[i].gpio);
		if(keys[i].irq < 0)
		{
			printk("error:gpio_to_irq\r\n");
			goto error_gpio_to_irq;  
		}
		printk("irq:%d\r\n",keys[i].irq);
		
		//传递每个按键结构变量地址,发生中断时可以通过参数取得
		ret = request_irq(keys[i].irq, btns_irq_handler, keys[i].flags, keys[i].name, (void *)&keys[i]);
		if(ret < 0)
		{
			printk("error:request_irq\r\n");
			goto error_request_irq;  
		}

		//初始化按键的定时器,参数是按键自身的结构内存地址
		setup_timer(&keys[i].timer,btn_timer, (unsigned long)&keys[i]);
	}
    ret = misc_register(&key_device);
    if(ret < 0){
        printk("error:misc_register\r\n");
        goto error_misc_register;  
    }
    return 0;  								// 初始化成功 

error_misc_register:
error_request_irq:
	while(--i >= 0){
		free_irq(keys[i].irq, (void *)&keys[i]);	//注销中断
	}
	
error_gpio_to_irq:
	kfree(keys_buf);		//释放按键缓冲区空间
    return ret; 
}

static void __exit key_btn_exit(void)
{
	int i = key_size;
	while(--i >= 0){
		free_irq(keys[i].irq, (void *)&keys[i]);	//注销中断
	}
	misc_deregister(&key_device);	//注销杂项设备
	kfree(keys_buf);				//释放按键缓冲区空间
}

module_init(xyd_btn_init);		  								// 指定模块初始化函数 
module_exit(key_btn_exit);  									// 指定模块清理函数 
MODULE_LICENSE("GPL");											// 声明模块许可证(GPL v2) 
MODULE_AUTHOR("LIU");    										// 模块作者 
MODULE_DESCRIPTION("RK3399 GPIO KEY Control Driver"); 			// 模块描述 

app.c

#include<stdio.h>              // 标准输入输出库(printf, perror等)
#include<stdlib.h> 
#include<string.h> 
#include<sys/types.h>          // 系统数据类型定义(如dev_t)
#include<sys/stat.h>           // 文件状态信息(文件模式等)
#include<fcntl.h>              // 文件控制选项(open等)
#include<unistd.h>             // 系统调用封装(lseek, read, write, sleep等)
#include<sys/ioctl.h>          // I/O控制操作(ioctl)
#include<errno.h> 

#define BTN_SIZE	1					// 按键数量
#define DEV_NAME 	"/dev/my-buttons"  	// 默认设备名

int main(int argc, char **argv)
{
    int fd,ret, i;                
    const char *devname; 				// 设备路径指针(初始化为默认路径)
    unsigned char pre_buf[BTN_SIZE+1],recv_buf[BTN_SIZE+1];

	memset(pre_buf,'0',BTN_SIZE);
	memset(recv_buf,'0',BTN_SIZE);
	
    if(argc == 1) 
        devname = DEV_NAME;
    else if(argc == 2)
        devname= argv[1];
    else {
        printf("Usage:%s [/dev/devname]\r\n", argv[0]);
        return 0;
    }

    fd = open(devname, O_RdwR);  			// O_RDWR:以读写模式打开
    if(fd < 0) {
        perror("open");   					// 打印系统错误信息
        printf("fd=%d\r\n", fd);
        return -1;            				// 打开失败退出程序
    }
    printf("fd=%d\r\n", fd);  				// 成功打开后输出fd值

    while(1) {
		ret = read(fd,recv_buf,BTN_SIZE);	// 读取按键数据
		if(ret < 0){
			if(errno != EAGAIN){
				perror("read");
				exit(-1);
			}else	continue;
		}

		//只在状态发生变化时候才输出
		for(i = 0;i < BTN_SIZE;i++){
			//分别判断每一个按键状态是否发生变化
			if(recv_buf[i] != pre_buf[i]){
				//更新当前状态为上一次状态
				pre_buf[i] = recv_buf[i];

				//判断这次变化是按下还是松开
				if(pre_buf[i] == '1')	printf("KEY%d is press!\r\n",i+1);
				else					printf("KEY%d is up!\r\n",i+1);
			}
		}
    }    
    return 0;
}

现象

Linux内核定时器使用及说明

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持China编程(www.chinasem.cn)。

这篇关于Linux内核定时器使用及说明的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1156369

相关文章

Python数据验证神器Pydantic库的使用和实践中的避坑指南

《Python数据验证神器Pydantic库的使用和实践中的避坑指南》Pydantic是一个用于数据验证和设置的库,可以显著简化API接口开发,文章通过一个实际案例,展示了Pydantic如何在生产环... 目录1️⃣ 崩溃时刻:当你的API接口又双叒崩了!2️⃣ 神兵天降:3行代码解决验证难题3️⃣ 深度

Linux镜像文件制作方式

《Linux镜像文件制作方式》本文介绍了Linux镜像文件制作的过程,包括确定磁盘空间布局、制作空白镜像文件、分区与格式化、复制引导分区和其他分区... 目录1.确定磁盘空间布局2.制作空白镜像文件3.分区与格式化1) 分区2) 格式化4.复制引导分区5.复制其它分区1) 挂载2) 复制bootfs分区3)

python中的flask_sqlalchemy的使用及示例详解

《python中的flask_sqlalchemy的使用及示例详解》文章主要介绍了在使用SQLAlchemy创建模型实例时,通过元类动态创建实例的方式,并说明了如何在实例化时执行__init__方法,... 目录@orm.reconstructorSQLAlchemy的回滚关联其他模型数据库基本操作将数据添

Spring配置扩展之JavaConfig的使用小结

《Spring配置扩展之JavaConfig的使用小结》JavaConfig是Spring框架中基于纯Java代码的配置方式,用于替代传统的XML配置,通过注解(如@Bean)定义Spring容器的组... 目录JavaConfig 的概念什么是JavaConfig?为什么使用 JavaConfig?Jav

Java使用Spire.Doc for Java实现Word自动化插入图片

《Java使用Spire.DocforJava实现Word自动化插入图片》在日常工作中,Word文档是不可或缺的工具,而图片作为信息传达的重要载体,其在文档中的插入与布局显得尤为关键,下面我们就来... 目录1. Spire.Doc for Java库介绍与安装2. 使用特定的环绕方式插入图片3. 在指定位

Springboot3 ResponseEntity 完全使用案例

《Springboot3ResponseEntity完全使用案例》ResponseEntity是SpringBoot中控制HTTP响应的核心工具——它能让你精准定义响应状态码、响应头、响应体,相比... 目录Spring Boot 3 ResponseEntity 完全使用教程前置准备1. 项目基础依赖(M

Java使用Spire.Barcode for Java实现条形码生成与识别

《Java使用Spire.BarcodeforJava实现条形码生成与识别》在现代商业和技术领域,条形码无处不在,本教程将引导您深入了解如何在您的Java项目中利用Spire.Barcodefor... 目录1. Spire.Barcode for Java 简介与环境配置2. 使用 Spire.Barco

Android使用java实现网络连通性检查详解

《Android使用java实现网络连通性检查详解》这篇文章主要为大家详细介绍了Android使用java实现网络连通性检查的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录NetCheck.Java(可直接拷贝)使用示例(Activity/Fragment 内)权限要求

C# 预处理指令(# 指令)的具体使用

《C#预处理指令(#指令)的具体使用》本文主要介绍了C#预处理指令(#指令)的具体使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录1、预处理指令的本质2、条件编译指令2.1 #define 和 #undef2.2 #if, #el

C#中Trace.Assert的使用小结

《C#中Trace.Assert的使用小结》Trace.Assert是.NET中的运行时断言检查工具,用于验证代码中的关键条件,下面就来详细的介绍一下Trace.Assert的使用,具有一定的参考价值... 目录1、 什么是 Trace.Assert?1.1 最简单的比喻1.2 基本语法2、⚡ 工作原理3