一起分析Linux系统设计思想——05内核定时器的使用
Posted 穿越临界点
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一起分析Linux系统设计思想——05内核定时器的使用相关的知识,希望对你有一定的参考价值。
在学习资料满天飞的大环境下,知识变得非常零散,体系化的知识并不多,这就导致很多人每天都努力学习到感动自己,最终却收效甚微,甚至放弃学习。我的使命就是过滤掉大量的无效信息,将知识体系化,以短平快的方式直达问题本质,把大家从大海捞针的痛苦中解脱出来。
文章目录
1 内核定时器
1.1 为什么需要内核定时器
操作系统实际上是基于任务和事件触发的方式进行工作的(实际上所有的逻辑都逃不出组合逻辑和时序逻辑的范畴),因此必然存在下述需求:某任务或事件需要延迟一段时间后执行。
这段时间如何进行计时就成了问题焦点。
使用延时(忙等或让出CPU)、软件定时器、硬件定时器等方法都可以完成推迟一段时间后执行某个任务或事件的功能,只是颗粒度不同或者效率不同。
由于篇幅问题,今天我们只介绍内核定时器的使用。
1.2 接口说明
接口 | 说明 |
---|---|
init_timer() | 初始化timer_list结构体。初始化完成后才能对结构体中的其他值进行填充。 |
add_timer() | 激活定时器(将定时器节点挂入链表)。 |
mod_timer() | 修改超时时间,如果当前定时器未激活则激活该定时器。 |
del_timer() | 在定时器超时前停止定时器(注意超时后定时器会自动删除,不需要调用该函数)。 |
del_timer_sync() | 在SMP系统上使用。del_timer会马上返回,但是del_timer_sync会确保当函数返回时系统中没有任何处理器正在执行定时器对象上的定时器函数。 |
1.3 实现原理和注意事项
1.3.1 内核timer实现原理
首先声明内核timer不是硬件定时器,因为硬件定时器资源非常紧俏。
内核定时器的实现思路大体是这样的:时基由系统心跳提供(时间周期为1/HZ);在内核中申请的内核定时器全部挂到定时器链表上(根据超时时间分为多组链表,空间换时间 的设计思想,极大地提升了链表遍历的效率,大家在自己的项目中可以学习使用);当系统心跳到来时,即系统定时器触发中断,在硬中断上下文中不能长时间停留,因此只是抛出了一个 TIMER_SOFTIRQ 的软中断;硬中断结束后软中断会接过接力棒,由run_timer_softirq()函数对TIMER_SOFTIRQ进行处理;该函数会遍历链表中的定时器,并运行所有的超时定时器对象指向的后处理函数。
1.3.2 同步问题
因为触发内核定时器的线程和定时器超时后处理函数运行所在线程不是同一个线程——异步场景。异步就有可能产生竞态。因此,在使用内核定时器时需要遵守下述规则:
- 内核定时器的触发线程和后处理线程如何有共享资源需要加锁。
- 选择使用del_timer_sync()而不是del_timer()(在非SMP系统中del_timer_sync实际上就是del_timer)。
- 修改定时器超时时间只能使用mod_timer()函数(该函数中有锁),不可以直接给timer_list结构体赋值。
1.3.3 效率问题
使用任何一个接口进行编程时都要考虑系统效率问题。当然在这里也不例外。
在内核定时器的常用接口中del_timer_sync()函数中有spin-loop代码,不可以在中断上下文中使用。
该函数也不可以在锁定临界区时使用,一方面是有可能导致锁定资源的长时间无效占用;另一方面是导致更严重的死锁问题(例如del_timer_sync所在线程拿到了A锁,然后调用了del_timer_sync函数等待定时器后处理函数运行结束,但定时器后处理函数中恰恰再等待获取A锁,这样就产生了死局)。
1.3.4 粒度问题
内核定时器的时基为1/HZ,对于一般的系统而言是10ms,具体数值可以查看HZ的宏定义。所以,内核定时器的精度并没有那么高,精度是一个系统心跳周期。
1.3.5 实时性问题
此外,由于内核定时器的后处理并不是在硬中断的上下文运行的,而是使用了软中断技术,因此,内核定时器的实时性(时间的确定性)也是不能保证的。
2 机械按键消抖
机械按键的抖动模型大体相同,在此假设最强烈的抖动时间是10ms内。当然对于真正的工程项目最严谨的做法是用示波器进行测量。
2.1 软件消抖的三种策略
软件消抖常用的三种策略如下:
- 在按键电平稳定的情况下,当第一次检测到按键电平变化,开始10ms计时,计时时间到后重新检测按键电平并以此时的电平为准。
- 在按键电平稳定的情况下,当第一次检测到按键电平变化,开始10ms计时,在计时的过程中,有任何的电平变化都立即重新计时。
- 在按键电平稳定的情况下,有电平变化时立即获取按键输出电平,并开始10ms计时,忽略这其中抖动。
如果按键使用中断模式而不是轮循模式时,使用第2个方案实现起来更简单一些。
2.2 借助内核定时器消抖实战
2.2.1 驱动程序
本驱动程序基于之前的按键驱动程序修改(一起分析Linux系统设计思想——05字符设备驱动之按键驱动(三)),示例源码如下:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <asm/io.h> /* ioremap() */
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/interrupt.h>
#include <linux/irqreturn.h>
#include <linux/fcntl.h>
#define GPFCON_ADD_BASE 0x56000050
volatile unsigned int *pgpfcon = NULL;
volatile unsigned int *pgpfdata = NULL;
DECLARE_WAIT_QUEUE_HEAD(key_wait_q);
volatile int event_press = 0;
struct fasync_struct *key_fap = NULL;
struct timer_list my_timer;
/* 内核定时器后处理函数 */
void timer_handler(unsigned long data)
{
event_press = 1;
kill_fasync(&key_fap, SIGIO, POLL_IN);
}
irqreturn_t keys_handler(int irq, void *dev_id)
{
printk("irq = %d is triggered. \\n", irq);
mod_timer(&my_timer, jiffies+HZ/100); /*重置内核定时器并承担激活功能*/
return IRQ_HANDLED;
}
int cdriver_open(struct inode *inode, struct file *file)
{
int minor = MINOR(inode->i_rdev);
printk("cdriver open success!\\n");
request_irq(IRQ_EINT0, keys_handler, SA_TRIGGER_RISING, "key0", 1);
return 0;
}
int cdriver_release(struct inode *inode, struct file *file)
{
printk("cdriver released. \\n");
free_irq(IRQ_EINT0, 1);
return 0;
}
int key_fasync(int fd, struct file *filp, int on)
{
return fasync_helper(fd, filp, on, &key_fap);
}
ssize_t key_read(struct file *file, char __user *user_buff, size_t len, loff_t *offset)
{
if (copy_to_user(user_buff, &event_press, sizeof(event_press)) < 0)
return -EFAULT;
event_press = 0;
return 0;
}
struct file_operations cdriver_fops = {
.owner = THIS_MODULE,
.open = cdriver_open,
.read = key_read,
.release = cdriver_release,
.fasync = key_fasync,
};
int major = 0;
struct class *led_class = NULL;
struct class_device *led_class_dev[3] = {NULL};
int __init cdriver_init(void)
{
int minor = 0;
init_timer(&my_timer); /*初始化内核定时器数据结构*/
my_timer.function = timer_handler; /*挂载定时器后处理函数*/
major = register_chrdev(0, "key_driver", &cdriver_fops);
led_class = class_create(THIS_MODULE, "keys");
if (NULL == led_class)
return -EINVAL;
for (minor = 0; minor < 3; minor++) {
led_class_dev[minor] = class_device_create(led_class, NULL, MKDEV(major, minor), NULL, "key%d", minor);
if (NULL == led_class_dev[minor])
return -EINVAL;
}
return 0;
}
void __exit cdriver_exit(void)
{
int minor = 0;
delete_timer_sync(&my_timer); /*删除内核定时器*/
unregister_chrdev(major, "key_driver");
for (minor = 0; minor < 3; minor++)
class_device_unregister(led_class_dev[minor]);
class_destroy(led_class);
}
module_init(cdriver_init);
module_exit(cdriver_exit);
MODULE_LICENSE("GPL");
2.2.2 应用程序
应用程序复用的介绍按键驱动时的代码,没有改动,示例代码如下:
#include <stdio.h>
#include <fcntl.h>
#include <signal.h>
typedef void (*sighandler_t)(int);
int fd = 0;
void key_handler(int signum)
{
int val = 0;
printf("signum = %d\\n", signum);
if (read(fd, &val, sizeof(val)) < 0) {
printf("read failed. \\n");
perror("read");
} else {
if (val) {
printf("key pressed, val = %d \\n", val);
printf("\\n");
}
}
}
int main(void)
{
int flags = 0;
fd = open("/dev/key0", O_RDWR);
if (fd < 0) {
printf("open /dev/key_dev failed!\\n");
return -1;
}
signal(SIGIO, key_handler);
fcntl(fd, F_SETOWN, getpid()); //config
flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | FASYNC); //enable
for(;;) {
/*do something*/
usleep(20 * 1000);
}
(void)close(fd);
return 0;
}
2.2.3 测试结果
按动一次按键之后的打印如下:
irq = 16 is triggered. #产生中断
irq = 16 is triggered.
irq = 16 is triggered.
irq = 16 is triggered.
irq = 16 is triggered.
signum = 29
key pressed, val = 1 # 发送给应用层的按键值
从打印结果我们可以看到这次按下一次按键后共产生了5次中断,也就是产生了4次抖动,但最终都被我们滤掉了,只给应用层上报了一次按键值。
恭喜你又坚持看完了一篇博客,又进步了一点点!如果感觉还不错就点个赞再走吧,你的点赞和关注将是我持续输出的哒哒哒动力~~
以上是关于一起分析Linux系统设计思想——05内核定时器的使用的主要内容,如果未能解决你的问题,请参考以下文章