在前面的休眠-唤醒、POLL机制中,都是通过休眠等待某一个事件的发生,而程序一旦陷入休眠,就没法再执行其它任务,相当于整个程序卡死了。在很多的场景中,如果发生了某一个事件我们就去处理它,没有发生事件那就可以做其它的事情。这种正常执行程序,当中断发生时才去执行的方式就叫做异步通知方式。本实验我们将结合LED驱动程序验证异步通知处理方式下线程不会休眠。在应用程序中调用LED驱动程序让LED闪烁(定时亮灭实现闪烁效果),同时使用异步通知方式在按键按下将按键值放入缓存,通知线程去执行异步通知处理函数,在函数中读取缓存,打印按键值。这两个驱动程序都在应用程序中被调用,属于同一个线程。
1 异步通知使用流程
异步通知的流程概括起来就是驱动程序发通知,调用应用程序提供的处理函数,然后返回内核,再返回应用层。
更具体的流程如下:
(1)谁发:驱动程序发
(2)发什么:信号
(3)发什么信号:信号的种类很多,具体发送什么信号,对于异步通知机制,发SIGIO信号
(4)怎么发:使用内核提供的函数
(5)发给谁:发给应用程序APP,因此APP要把自己告诉驱动,驱动才能知道发给谁
注意:同一个驱动程序,可以被多个应用程序调用,因此驱动-应用程序是一个一对多的关系,那么驱动发信号时就要明确是发给哪个应用程序APP了,因此使用异步通知机制的应用程序需要把自己告诉驱动,同时驱动也应该支持异步通知机制,二者相互配合才能完成。至于在多个应用程序都使用同一个驱动程序的异步通知机制时该如何处理,先不讨论。一种方法就是给所有使用该驱动程序的应用程序都发,然后由应用程序的异步通知处理函数确定是不是它的信号来决定做什么动作。
(6)应用程序收到后做什么:执行信号处理函数
(7)信号处理函数和信号如何挂钩:应用程序APP向驱动程序注册信号处理函数
在 Linux 内核源文件 include\uapi\asmgeneric\signal.h 中,有很多信号的宏定义,这些就是Linux系统中不同类型的信号,实际上就是用不同的整数表示不同的信号,然后给它一个宏名,让代码看起来更直观
对于应用程序来说,通过signal函数来注册处理某一类信号的函数,并且既然是调用别的函数来注册,那就信号的处理函数的函数格式就要遵循规定,用法如下:
除此之外,应用程序还需要完成以下步骤:
(1)打开驱动,通过文件节点打开
(2)把应用程序的进程PID告诉内核,因为一个驱动程序可以被多个应用程序调用,必须告诉驱动是哪个进程需要接收信号,这样驱动程序才能知道发信号给谁
(3)应用程序是否要接收信号的意愿
在驱动程序中,需要发送信号:
(1)应用程序向驱动发送进程PID时需要记录应用程序的PID
(2)使能驱动程序的异步通知功能
(3)应用程序通过open系统调用打开驱动程序文件节点时,会调用内核中的sys_open函数,在sys_open中会调用我们编写的驱动程序中的file_oprations结构体的open函数,因此我们的驱动程序中自己写的open函数其实只是sys_open函数执行中的一个部分。sys_open还负责创建对应的file结构体,并且在函数结束时返回file结构体对应的文件句柄fd给应用程序。在file结构体中有一个f_flags字段,这个字段里有一个FASYNC位,这个位置1时表明使能异步通知功能,并且当这个位发生变化时,file中的file_operations结构体中的fasync函数被调用。
(4)发生中断时或有数据时,调用内核函数kill_fasync发送信号。
APP收到信号后就去执行信号处理函数,这个过程比较复杂,在最后的内核详解会进行详细介绍。
总的来说,使用异步通知的流程如下图所示。
2 编程
2.1 驱动程序编程
(1)在file_oprations结构体中提供对应的fasync函数。这个函数会在应用程序中fcntl(fd,F_SETFL,oflags | FASYNC)设置flag中FASYNC位时被调用一次。
(2)在合适的时机使用kill_fasync发送信号
在drv_fasync函数中,通常会调用一个fasync_helper函数,这个函数会分配、构造一个fasync_struct结构体,并且当驱动文件的 flag 被设置为 FAYNC 时,button_async->fa_file = filp
当驱动文件的 flag 被设置为 非FAYNC 时,button_async->fa_file = NULL
发信号时,使用kill_fasync函数即可
在这个函数中,第一个参数在前面设置flag中FASYNC标志位的时候就已经被构造了,因此可以使用它向进程发送信号。
2.2 应用程序APP编程
步骤如下:
(1)编写信号处理函数,因为是使用内核提供的机制,就必须遵守异步通知机制中信号处理函数的格式,其格式如下:
void (*func)(int sig)
(2)注册信号处理函数
signal(SIGIO,func)
(3)通过设备节点打开驱动,使用open系统调用
(4)将进程PID告诉驱动
fcntl(fd, F_SETOWN, getpid())
(5)使能驱动的FASYNC,这一步会导致驱动中的fasync函数第一次被调用
flags = fcntl(fd, F_GETFL); //里面F_GETFL其实就是get flag的缩写,表示获取标志
fcntl(fd, F_SETFL, flags | FASYNC); //F_SETFL就是set flag的缩写,表示设置标志
2.1 驱动编程
驱动程序代码如下:
点击查看代码
/*
基于总线设备驱动模型进行驱动开发,platform_device由设备树产生,并且使用GPIO子系统和pinctr子系统
在该驱动程序中需要自己构造platform_driver结构体并且要和设备树生成的platform_device结构体匹配
1、使用循环缓冲区记录按键值
2、采用异步通知机制,当发生按键中断时通过向进程发信号,然后信号处理函数会被调用,在其中读取按键值
*/
#include "asm-generic/errno-base.h"
#include "asm-generic/poll.h"
#include "asm-generic/siginfo.h"
#include "asm/signal.h"
#include "asm/uaccess.h"
#include "linux/err.h"
#include "linux/export.h"
#include "linux/gpio/driver.h"
#include "linux/irqreturn.h"
#include "linux/kdev_t.h"
#include "linux/nfs_fs.h"
#include "linux/of.h"
#include "linux/wait.h"
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/sched.h>
#include <linux/ktime.h>
#include <linux/delay.h>
#include <linux/fcntl.h>
static int major; //主设备号
static struct class *key_class;
static int count; //引脚树,也即按键数量
/*
设置按键中断时,会使用到引脚的引脚编号(老版本),引脚描述结构体,引脚中断号,引脚标志位
因此这些信息可以使用一个结构体来保存,一个引脚按键就对应一个,如果有多个按键就可以用结构体数组
按键中断本质是引脚输入电平变化引起的中断,因此如果有外设在完成工作后发出电平变化信号的话,可以使用这种中断方式
比如AD7606在完成转换后BUSY引脚会变成持续一段时间的高电平,那么可以使用这个高电平产生引脚中断然后去读取AD7606的AD转换值
*/
struct gpio_interrupt{
int gpio_num; //GPIO引脚编号,老版本
struct gpio_desc *desc; //gpio引进描述结构体
int irq; //中断号
int flag;
};
struct fasync_struct *key_fasync_struct;
struct gpio_interrupt *key_gpio; //定义一个结构体指针,在probe函数中再根据设备树中定义的引脚数量动态分配内存
#define BUF_LEN 128 //缓冲区大小
static int key_buf[BUF_LEN];
static int r, w; //定义缓冲区写指针,r=w时表示缓冲区空,(w+1) % BUF_LEN = r时表示缓冲区满
//实际上BUF_LEN个缓存单元只能用BUF_LEN-1个,因为要判断是否满/空
#define next_position(x) (((x) + 1) % BUF_LEN)
static bool is_buf_empty(void)
{
// printk("%s %s line %d, w = %d, r = %d\n", __FILE__, __FUNCTION__, __LINE__, w, r);
return (r == w);
}
static bool is_buf_full(void)
{
// printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return (r == next_position(w));
}
static void put_key_value(int key)
{
// printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
if(!is_buf_full())
{
key_buf[w] = key;
w = next_position(w); //更新写指针
}
}
static int get_key_value(void)
{
int key;
if(!is_buf_empty())
{
key = key_buf[r];
r = next_position(r); //更新指针
}
else
{
key = -1;
printk("%s %s line %d, buf is empty! failed to read\n", __FILE__, __FUNCTION__, __LINE__);
}
return key;
}
int key_open(struct inode *node, struct file *file)
{
/*打开文件时会执行,在这里可以做一些初始化操作
设置GPIO为输入
*/
int i;
r = 0;
w = 0;
for(i = 0; i < count; i++)
{
gpiod_direction_input(key_gpio[i].desc);
}
return 0;
}
static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);
ssize_t key_read(struct file *file, char __user *buf, size_t count, loff_t *offset)
{
/*用户程序使用read系统调用时会调用,返回值最终会返回给用户程序中的read
读取按键值,如果循环缓冲区有内容,那么直接读取缓冲区内容,否则休眠
因此可以使用缓冲区是否为空作为休眠的信号
*/
int err;
int key;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
//如果buf空,is_buf_empty()返回1, !is_buf_empty()是0,那么陷入睡眠
wait_event_interruptible(gpio_key_wait, !is_buf_empty());
//如果没有休眠或者从休眠唤醒,说明缓冲区有内容了,读缓冲区
key = get_key_value();
//把按键值写入用户空间
err = copy_to_user(buf, &key, sizeof(key));
if(err)
{
printk("failed copy data to user\n");
return -EFAULT;
}
return sizeof(key);
}
int key_fasync(int fd, struct file *file, int on)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
if(fasync_helper(fd, file, on, &key_fasync_struct) >= 0) //在file结构体中的flag的FASYNC位发生变化时调用
{
return 0;
}
else
{
return -EIO;
}
}
int key_close(struct inode *node, struct file *file)
{
/*用户程序调用close时会执行*/
int i;
for(i = 0; i < count; i++)
{
gpiod_direction_output(key_gpio[i].desc, 0);
}
return 0;
}
struct file_operations key_opr = {
.owner = THIS_MODULE,
.open = key_open,
.read = key_read,
.fasync = key_fasync,
.release = key_close,
};
static irqreturn_t gpio_isr(int irq, void *dev_id)
{
/*中断处理函数上半部
发生中断后,会执行相应的中断处理函数,在内核中已经提供了相应的中断处理函数
在里面会判断中断号,关中断,我们所写的中断处理函数被注册进内核后就作为内核中断
处理函数的回调函数被调用,在里面我们可以写上我们想执行的操作
*/
struct gpio_interrupt *key = dev_id; //获取引脚信息
int val; //引脚状态
int key_val; //引脚编号 | 引脚状态
val = gpiod_get_value(key->desc); //获取引脚电平状态
val ^= (1); //因为设备树中设置低电平有效,因此引脚电平的逻辑值和真实值是相反的
key_val = (key->gpio_num << 8) | val; //记录引脚状态和引脚编号
// printk("%s %s line %d: KeyVal: %d\n", __FILE__, __FUNCTION__, __LINE__, key_val);
put_key_value(key_val); //把按键状态放入缓冲区
wake_up_interruptible(&gpio_key_wait); //唤醒进程
kill_fasync(&key_fasync_struct, SIGIO, POLL_IN); //发信号。第一个参数是二重指针,说明如果有多个进程,它会全部发送信号
/*在异步通知中,是POLL_IN,POLLIN是poll机制的,要注意区分!*/
return IRQ_HANDLED; //返回IRQ_HANDLED表示已经处理中断,否则内核还会继续执行action链表中其他函数
}
int key_probe(struct platform_device *pdev)
{
/*
驱动和设备节点匹配时执行
在这里会做一些初始化操作:获取设备树信息,设置中断,注册驱动file_operation结构体
*/
struct device_node *node = pdev->dev.of_node; //获取设备树中引脚对应的device_node结构体
int err;
int i;
enum of_gpio_flags flag;
count = of_gpio_named_count(node, "key-gpios"); //获取设备树节点中定义了多少个GPIO
if(count <= 0)
{
printk("%s %s line %d, No available gpio, count = %d\n", __FILE__, __FUNCTION__, __LINE__, count);
return -1;
}
key_gpio = kzalloc(sizeof(struct gpio_interrupt) * count, GFP_KERNEL); //为自定义的引脚结构体分配内存
//获取设备树中定义的节点信息
for(i = 0; i < count; i++)
{
//获取引脚编号
// key_gpio[i].gpio_num = of_get_gpio_flags(node, i, &flag); //以后尽量使用命名式的函数
key_gpio[i].gpio_num = of_get_named_gpio_flags(node, "key-gpios", 0, &flag);
if(key_gpio[i].gpio_num < 0)
{
printk("%s %s line %d, of_get_gpio_flags failed\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}
//获取引脚描述结构体
key_gpio[i].desc = gpiod_get_index(&pdev->dev, "key", i); //这里使用get,那么在remove函数里就需要使用put释放引脚
key_gpio[i].flag = flag & OF_GPIO_ACTIVE_LOW;
key_gpio[i].irq = gpiod_to_irq(key_gpio[i].desc); //获取对应的中断号
}
//注册中断
for(i = 0; i < count; i++)
{
err = request_irq(key_gpio[i].irq, gpio_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "gpio_key", &key_gpio[i]);
}
/*注册file_operaton结构体,创建类和设备*/
major = register_chrdev(0, "key_interrupt", &key_opr);
key_class = class_create(THIS_MODULE, "key_class");
if(IS_ERR(key_class))
{
printk("%s %s line %d, class create failed\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "key_interrupt");
return PTR_ERR(key_class);
}
//创建设备节点,不需要为每个引脚都创建设备节点,所有引脚(按键)共用一个设备节点
device_create(key_class, NULL, MKDEV(major, 0), NULL, "key");
return 0;
}
int key_remove(struct platform_device *pdev)
{
int i;
/*卸载驱动时执行,在这里要撤销中断,注销驱动、类和设备节点,释放动态分配的内存*/
//删除设备节点
device_destroy(key_class, MKDEV(major, 0));
//删除类
class_destroy(key_class);
//撤销驱动
unregister_chrdev(major, "key_interrupt");
//撤销中断,释放GPIO
for(i = 0; i < count; i++)
{
free_irq(key_gpio[i].irq, &key_gpio[i]);
gpiod_put(key_gpio[i].desc);
}
kfree(key_gpio); //释放内存
return 0;
}
static const struct of_device_id keys_match_table[] = {
{
.compatible = "mykey_driver_interrupt"
}
};
struct platform_driver key_driver = {
.probe = key_probe,
.remove = key_remove,
.driver = {
.name = "key_interrupt",
.of_match_table = keys_match_table,
},
};
static int __init key_drv_init(void)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = platform_driver_register(&key_driver);
return err;
}
static void __exit key_drv_exit(void)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
platform_driver_unregister(&key_driver);
}
module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("GPL");
2.2 应用编程
在应用程序中,需要使能驱动的fasync功能,具体代码如下:
点击查看代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <poll.h>
#include <signal.h>
/*
按键中断异步通知实验,如果没有发生中断,,程序不会主动休眠,LED灯正常闪烁
发生中断时,在信号处理函数中对按键进行处理
*/
static int fd_button; //按键文件句柄,必须全局变量,因为信号处理函数要用
/*信号处理函数*/
void key_func(int sig)
{
int key_val;
read(fd_button, &key_val, sizeof(key_val));
printf("key_val: %d\n", key_val);
}
void delay_short(volatile unsigned int n)
{
/*
短时延时函数
*/
while(n--) {}
}
void delay(volatile unsigned int n) /* 实现延时n ms*/
{
while(n--)
{
delay_short(0x7fff); //大概延时1ms
}
}
int main(int argc, char** argv)
{
int fd_led;
char state;
int flag;
/*判断参数*/
if(argc != 3)
{
printf("Usage: %s <buton_dev> <led_dev>\n", argv[0]);
}
fd_button = open(argv[1], O_RDWR);
if(fd_button == -1)
{
printf("can not open file %s\n", argv[1]);
return -1;
}
fd_led = open(argv[2], O_RDWR);
if(fd_led == -1)
{
printf("can not open file %s\n", argv[2]);
return -1;
}
/*注册信号处理函数*/
signal(SIGIO, key_func);
/*将进程PID告诉驱动*/
fcntl(fd_button, F_SETOWN, getpid());
//如果没有F_SETOWN就会不能正常运行,这是必须的,因为在内核的函数中有一个switch判断,根据F_SETOWN才会执行将这个进程的PID记录在file结构体中,否则就不会记录,从而导致进程接收不到信号
/*使能驱动的异步通知功能*/
flag = fcntl(fd_button, F_GETFL);
fcntl(fd_button, F_SETFL, flag | FASYNC);
state = 0; //初始化0,因此最开始灯是灭的
while(1)
{
write(fd_led, &state, sizeof(state));
state ^= (1); //在0和1之间不断取反
delay(500);
}
close(fd_button);
return 0;
}
在应用程序中同时运行LED闪烁,运行程序后可以看到LED一直在闪烁,按键按下触发中断后应用程序中的信号处理函数被调用读取循环缓存区中的按键值
3 异步通知内核工作过程详解
未完待续...