一、前言
首先回顾一下前两次的实验,前面几次实验我们使用了阻塞或者是非阻塞实验读取按键值:
阻塞读取:当进程访问的驱动不可用或数据未准备好时,进程进入interruptible的休眠状态,也就是将当前进程添加到等待队列,等到按键按下进入中断,在中断中将线程唤醒(如果通过定时器消抖也可以在定时器中将线程唤醒),读取按键值;
非阻塞读取:在应用层使用select或poll函数,在超时时间内进行查询,数据准备好就进行读取,超时就重新开始select或poll函数查询;
阻塞读取以及非阻塞读取这两种方法都是应用程序主动读取,但是最好的方式就是驱动程序能够主动发送信号给应用程序,表示驱动已经准备好读取或写入数据。接下来我们就开始学习通过异步通知完成按键的读取工作。
二、异步通知
1、定义
异步通知(Asynchronous notification)类似于中断,中断配置好以后就可以去处理其他事情,当中断发生以后就会触发我们提前设置好的中断服务函数,在中断服务函数中具体处理。对于信号来说,更相当于是软件上对中断的一种模拟,当设备可用或数据可以读写,驱动程序会向应用程序发送一个信号,相当于驱动程序发送过来一个中断,然后应用程序使用自定义的服务函数响应这个中断。
2、Linux支持信号
#define SIGHUP 1 /* 终端挂起或控制进程终止 */
#define SIGINT 2 /* 终端中断(Ctrl+C 组合键) */
#define SIGQUIT 3 /* 终端退出(Ctrl+\组合键) */
#define SIGILL 4 /* 非法指令 */
#define SIGTRAP 5 /* debug 使用,有断点指令产生 */
#define SIGABRT 6 /* 由 abort(3)发出的退出指令 */
#define SIGIOT 6 /* IOT 指令 */
#define SIGBUS 7 /* 总线错误 */
#define SIGFPE 8 /* 浮点运算错误 */
#define SIGKILL 9 /* 杀死、终止进程 */
#define SIGUSR1 10 /* 用户自定义信号 1 */
#define SIGSEGV 11 /* 段违例(无效的内存段) */
#define SIGUSR2 12 /* 用户自定义信号 2 */
#define SIGPIPE 13 /* 向非读管道写入数据 */
#define SIGALRM 14 /* 闹钟 */
#define SIGTERM 15 /* 软件终止 */
#define SIGSTKFLT 16 /* 栈异常 */
#define SIGCHLD 17 /* 子进程结束 */
#define SIGCONT 18 /* 进程继续 */
#define SIGSTOP 19 /* 停止进程的执行,只是暂停 */
#define SIGTSTP 20 /* 停止进程的运行(Ctrl+Z 组合键) */
#define SIGTTIN 21 /* 后台进程需要从终端读取数据 */
#define SIGTTOU 22 /* 后台进程需要向终端写数据 */
#define SIGURG 23 /* 有"紧急"数据 */
#define SIGXCPU 24 /* 超过 CPU 资源限制 */
#define SIGXFSZ 25 /* 文件大小超额 */
#define SIGVTALRM 26 /* 虚拟时钟信号 */
#define SIGPROF 27 /* 时钟信号描述 */
#define SIGWINCH 28 /* 窗口大小改变 */
#define SIGIO 29 /* 可以进行输入/输出操作 */
#define SIGPOLL SIGIO /* #define SIGLOS 29 */
#define SIGPWR 30 /* 断点重启 */
#define SIGSYS 31 /* 非法的系统调用 */
#define SIGUNUSED 31 /* 未使用信号 */
上面的信号中我们比较熟悉的是SIGINT信号,也就是在终端里面的Ctrl+C,还有我们即将用到的SIGIO信号,表示可以进行输入输出操作。
3、驱动程序中信号的操作
1)fasync_struct结构体
欲使用异步通知,必须定义异步通知相关的结构体。异步操作结构体定义为:
struct fasync_struct {
spinlock_t fa_lock;
int magic;
int fa_fd;
struct fasync_struct *fa_next;
struct file *fa_file;
struct rcu_head fa_rcu;
};
一般将结构体定义在设备结构体中,形式为结构体指针变量。
struct imx6uirq_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
struct device_node *nd; /* 设备节点 */
atomic_t keyvalue; /* 有效的按键键值 */
atomic_t releasekey; /* 标记是否完成一次完成的按键,包括按下和释放 */
struct timer_list timer;/* 定义一个定时器*/
struct irq_keydesc irqkeydesc[KEY_NUM]; /* 按键init述数组 */
unsigned char curkeynum; /* 当前init按键号 */
wait_queue_head_t r_wait; /* 读等待队列头 */
struct fasync_struct *async_queue; /* 异步相关结构体 */
};
2)fasync函数 & fasync_helper函数
※ fasync函数: 驱动开发者需要实现file_operations操作函数集合中fasync函数,当应用程序通过“fcntl(fd, F_SETFL, flags | FASYNC)” 改变fasync 标记的时候,驱动程序 file_operations操作集中的 fasync 函数就会执行。定义如下 :
从上图可以看出,fasync函数里面一般通过fasync_helper函数来初始化前面定义fasync_strcut结构体指针变量(大概是因为设备结构体中声明的是结构体指针变量,并没有分配空间)。
※ fasync_helper函数: fasync_helper定义如下,该函数的第四个参数是指向fasync_struct结构体指针的指针,用来初始化fasync_struct结构体指针变量。
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp);
※ 释放: 当然还需要对设备结构体中定义的异步操作结构体指针变量进行释放,对应的释放函数同样为fasync_helper,一般直接使用file_operations操作函数集合中fasync函数,因为其内部实现为fasync_helper函数,只不过其形参需要如下图所示:
3)kill_fasync函数
使用的异步操作结构体已经声明,对应的fcntl函数也已经写好,怎么才能发送信号呢?当然是使用kill_fasync函数了:
void kill_fasync(struct fasync_struct **fp, int sig, int band);
使用此函数,相当于驱动程序对应用程序产生一个信号“中断”,sig为要发送的信号,band可读设置为POLL_IN,可写设置为POLL_OUT。
4、应用程序中信号的操作
1) 注册信号处理函数
中断有中断处理函数,信号当然也有信号的处理函数,指定对应信号的处理函数的signal函数声明如下:
sighandler_t signal(int signum, sighandler_t handler);
signum:要设置处理函数的信号。
handler:信号的处理函数。
2) 将本应用程序进程号注册到内核
fcntl(fd, F_SETOWN, getpid());
F_SETOWN:设置当前进程为设备文件owner;
getpid():获取进程识别码。
3) 开启异步通知功能
flags = fcntl(fd, F_GETFL); /* 获取当前的进程状态 */
fcntl(fd, F_SETFL, flags | FASYNC); /* 开启当前进程异步通知功能 */
通过 fcntl 函数设置进程状态为 FASYNC,经过这一步,驱动程序中的 fasync 函数就会执行。
三、程序框图
上图为个人理解,如有错误,请在评论区指出。
2022/06/05 于武汉