Linux驱动异步通知机制详解
Linux驱动异步通知机制详解
1. 异步通知基本概念
1.1 什么是异步通知
异步通知(Asynchronous Notification)是Linux系统中一种重要的进程间通信机制,它允许驱动程序在特定事件发生时主动通知应用程序,而不是让应用程序通过轮询的方式不断查询设备状态。这种机制可以显著提高系统效率,减少CPU资源的浪费。
在传统的轮询模式中,应用程序需要不断地调用read()等系统调用来检查设备是否有数据可读,这种方式不仅消耗大量的CPU资源,而且响应速度也不够及时。而异步通知机制通过信号(Signal)的方式,在设备状态发生变化时立即通知应用程序,实现了事件驱动的编程模型。
1.2 SIGIO信号
异步通知的核心是SIGIO信号。当一个文件描述符被设置为支持异步通知后,每当该文件描述符对应的设备有数据可读或可写时,内核就会向拥有该文件描述符的进程发送SIGIO信号。应用程序可以通过signal()或sigaction()系统调用来注册SIGIO信号的处理函数,从而在信号到达时执行相应的处理逻辑。
1.3 FASYNC标志
要启用异步通知功能,需要在文件操作结构体中实现fasync()方法,并在应用程序中通过fcntl()系统调用设置FASYNC标志。FASYNC标志告诉内核,当文件描述符对应的设备状态发生变化时,需要发送SIGIO信号。
2. 驱动程序实现
2.1 数据结构定义
struct key_desc {char name[10];int gpio;int irqnum;unsigned char value;irqreturn_t (*handler)(int, void *);
};struct imx6uirq_dev {dev_t devid;int major;int minor;struct cdev cdev;struct class *class;struct device *device;struct device_node *key_nd;struct key_desc key[KEY_NUM];struct timer_list timer;atomic_t keyvalue;atomic_t release;struct fasync_struct *fasync_queue;
};
驱动程序定义了两个主要的数据结构:key_desc
用于描述按键的相关信息,包括名称、GPIO引脚、中断号、键值和中断处理函数;imx6uirq_dev
是设备的主结构体,包含了字符设备所需的各种成员,以及用于异步通知的fasync_queue
指针。
2.2 文件操作结构体
static const struct file_operations imx6uirq_fops = {.owner = THIS_MODULE,.open = imx6uirq_open,.read = imx6uirq_read,.fasync = imx6uirq_fasync,.release = imx6uirq_release,
};
文件操作结构体中实现了四个关键方法:
imx6uirq_open
:打开设备文件时调用imx6uirq_read
:读取设备数据时调用imx6uirq_fasync
:管理异步通知队列imx6uirq_release
:关闭设备文件时调用
其中fasync
方法是实现异步通知的关键。
2.3 fasync方法实现
static int imx6uirq_fasync(int fd, struct file *filp, int on)
{struct imx6uirq_dev *dev = filp->private_data;return fasync_helper(fd, filp, on, &dev->fasync_queue);
}
imx6uirq_fasync
方法调用了内核提供的fasync_helper
函数来管理异步通知队列。这个函数会根据on
参数的值来添加或删除文件描述符到异步通知队列中。当应用程序通过fcntl()设置FASYNC标志时,on
为1,文件描述符会被添加到队列;当清除FASYNC标志时,on
为0,文件描述符会被从队列中删除。
2.4 release方法实现
static int imx6uirq_release(struct inode *inode, struct file *filp)
{imx6uirq_fasync(-1, filp, 1);return 0;
}
在设备文件关闭时,需要调用imx6uirq_fasync
方法将文件描述符从异步通知队列中移除,以防止后续的信号发送导致错误。
2.5 中断处理
static irqreturn_t key0_handler(int irq, void *filp)
{struct imx6uirq_dev *dev = filp;dev->timer.data = (volatile long)filp;mod_timer(&dev->timer, jiffies + msecs_to_jiffies(20));return IRQ_HANDLED;
}
中断处理函数中使用了定时器来实现按键消抖。当按键中断发生时,不是立即处理按键事件,而是启动一个20ms的定时器。这样可以避免由于机械按键的弹跳效应导致的多次中断。
2.6 定时器处理函数
static void timer_func(unsigned long arg)
{struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;int value = 0;value = gpio_get_value(dev->key[0].gpio);if (value == 0){atomic_set(&dev->keyvalue, dev->key[0].value);}else{atomic_set(&dev->release, 1);}if (atomic_read(&dev->release)){kill_fasync(&dev->fasync_queue, SIGIO, POLL_IN);}
}
定时器处理函数首先读取按键的当前状态,如果按键被按下,则设置键值;如果按键被释放,则设置释放标志。关键的是最后的kill_fasync
调用,它会向所有注册了异步通知的进程发送SIGIO信号,通知它们设备状态已经改变。
3. 应用程序实现
3.1 信号处理函数
static void sigio_signal_func(int num)
{int ret = 0;int keyvalue;ret = read(fd, &keyvalue, sizeof(keyvalue));if (ret < 0){printf("User: fail error\r\n");}else{printf("User: sigio signal, keyvalue is: %d\r\n", keyvalue);}
}
应用程序首先定义了一个SIGIO信号的处理函数sigio_signal_func
。当收到SIGIO信号时,这个函数会被调用。在信号处理函数中,应用程序调用read()系统调用来读取按键值,并将其打印出来。
3.2 启用异步通知
signal(SIGIO, sigio_signal_func);int ret = fcntl(fd, F_SETOWN, getpid());
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | FASYNC);
应用程序通过以下三个步骤来启用异步通知:
- 使用
signal()
系统调用注册SIGIO信号的处理函数 - 使用
fcntl(fd, F_SETOWN, getpid())
告诉内核哪个进程应该接收SIGIO信号 - 使用
fcntl(fd, F_SETFL, flags | FASYNC)
设置FASYNC标志,启用异步通知功能
4. 工作流程分析
4.1 初始化阶段
- 驱动程序加载时,
imx6uirq_init
函数被调用 - 分配设备号,注册字符设备
- 创建设备节点
- 初始化按键和定时器
4.2 应用程序打开设备
- 应用程序调用open()打开设备文件
- 驱动程序的
imx6uirq_open
函数被调用 - 将设备结构体指针保存到filp->private_data中
4.3 启用异步通知
- 应用程序调用signal()注册SIGIO信号处理函数
- 调用fcntl()设置F_SETOWN,指定信号接收进程
- 调用fcntl()设置FASYNC标志
- 驱动程序的
imx6uirq_fasync
函数被调用,将文件描述符添加到异步通知队列
4.4 按键事件处理
- 用户按下按键,触发外部中断
- 中断处理函数
key0_handler
被调用 - 启动20ms定时器进行按键消抖
- 定时器超时后,
timer_func
函数被调用 - 读取按键状态,如果是按键释放,则调用
kill_fasync
kill_fasync
向应用程序发送SIGIO信号- 应用程序的SIGIO信号处理函数被调用
- 在信号处理函数中调用read()读取按键值
5. 设备树配置
5.1 设备节点定义
key{compatible = "alientek,key";pinctrl-names = "default";pinctrl-0 = <&pinctrl_key>;states = "okay";key-gpios = <&gpio1 18 GPIO_ACTIVE_HIGH>;interrupt-parent = <&gpio1>;interrupts = <18 IRQ_TYPE_EDGE_BOTH>;
};
在设备树中定义了按键设备节点,指定了以下信息:
- compatible:兼容性字符串,用于匹配驱动程序
- pinctrl-0:引脚控制配置
- key-gpios:按键连接的GPIO引脚
- interrupt-parent:中断父节点
- interrupts:中断号和触发类型
5.2 引脚控制配置
pinctrl_key: keygrp {fsl,pins = <MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0xF080>;
};
引脚控制配置指定了GPIO1_18引脚的工作模式,将其配置为GPIO输入模式,并设置了适当的电气特性参数。
6. Makefile分析
KERNERDIR := /home/ubuntu2004/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENTDIR := $(shell pwd)obj-m := imx6uirq.o
build : kernel_moduleskernel_modules:$(MAKE) -C $(KERNERDIR) M=$(CURRENTDIR) modulesclean:$(MAKE) -C $(KERNERDIR) M=$(CURRENTDIR) clean
Makefile中定义了以下几个关键变量:
- KERNERDIR:内核源码目录路径
- CURRENTDIR:当前目录路径
- obj-m:指定要编译的模块对象
编译时,会进入内核源码目录,通过M参数指定模块源码位置,让内核构建系统来编译模块。
https://gitee.com/dream-cometrue/linux_driver_imx6ull