/* AUTHOR: Pinus
* Creat on : 2018-10-24
* KERNEL : linux-4.4.145
* BOARD : JZ2440(arm9 s3c2440)
* REFS : 韦东山视频教程第二期
*/
概述
已有的方式:
1. 查询:极度耗费资源
2. 中断:可以沉睡但app会一直运行
3. poll:可以指定沉睡时间
共同点:都是应用程序主动去调用驱动
异步通知机制全称是"信号驱动的异步IO",就是一旦设备就绪,则驱动主动通知应用程序,应用程序根本就不需要轮询设备,类似于硬件的中断概念,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。
在linux中,异步通知是使用signal(信号)这里使用的是信号"SIGIO"来实现的,而在linux,大概有30种信号,比如大家熟悉的ctrl+c的SIGINT信号,和常用进程间发信号 kill -9 PID 结束某一个进程。
1.应用层程序将自己注册为接收来自设备文件的SIGIO信号的进程
2.驱动实现相应的接口,以期具有向所有注册接收这个设备驱动SIGIO信号的应用程序发SIGIO信号的能力。
3.驱动在适当的位置调用发送函数,应用程序即可接收到SIGIO信号,运行信号处理函数
实验
目标: 按键按下时驱动程序通知应用程序
1. 应用程序:注册信号,信号处理函数
在Linux命令行中使用 man signal 查找signal的用法
NAME
signal - ANSI C signal handling
SYNOPSIS
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
可见signal()函数指定信号(signum)的处理函数(handler),当程序接收到指定信号时调用处理函数,处理函数(handler)由自己编写(有些信号系统有默认处理函数)。
man fcntl
NAME
fcntl - manipulate file descriptor
SYNOPSIS
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ ); //根据不同的cmd,和不同的参数进行不同的操作
fcntl(fd, F_SETOWN, getpid()); // 告诉内核,发给谁
/*
F_SETOWN
Set the process ID or process group ID that will receive SIGIO
and SIGURG signals for events on file descriptor fd to the ID
given in arg. A process ID is specified as a positive value; a
process group ID is specified as a negative value. Most com‐
monly, the calling process specifies itself as the owner (that
is, arg is specified as getpid(2)). */
Oflags = fcntl(fd, F_GETFL); //获取文件标记
/*
F_GETFL (void)
Get the file access mode and the file status flags; arg is
ignored. */
fcntl(fd, F_SETFL, Oflags | FASYNC); // 改变fasync标记,最终会调用到驱动的faync > fasync_helper:每当FASYNC标志改变时,驱动程序中的fasync()得到执行。驱动中应该事先fasync()函数。
/* F_SETFL (int)
Set the file status flags to the value specified by arg. 还会去调用drv_fasync初始化异步通知链表 */
直接上应用程序:
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
/*
* fasync
*/
int fd;
void my_signal_func()
{
unsigned char key_val;
read(fd, &key_val, 1);
printf("key_val: 0x%x\n",key_val);
}
int main(int argc, char **argv)
{
int Oflags;
signal(SIGIO, my_signal_func);
fd = open("/dev/buttons", O_RDWR);
if (fd < 0){
printf("can't open dev nod !\n");
return -1;
}
fcntl(fd, F_SETOWN, getpid());
Oflags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, Oflags | FASYNC);
while(1){
sleep(1000);
}
return 0;
}
2. 谁发: 驱动; 发给谁: app; 怎么发: IRQ_handler中:
驱动大致仍然遵循前文案,这里只列出更改部分的代码
static struct fasync_struct *button_async;
static const struct file_operations jz2440_buttons_fops = {
.owner = THIS_MODULE,
.read = buttons_drv_read,
.open = buttons_drv_open,
.release = buttons_drv_close,
.poll = buttons_drv_poll,
.fasync = buttons_drv_fasync, /* 新增信号函数 */
};
int buttons_drv_fasync (int fd, struct file *file, int on)
{
printk("driver: button fasync\n");
return fasync_helper(fd, file, on, &button_async); /* 真正有用的 */
}
在按键中断处理程序中发送信号
static irqreturn_t button_irq_handle(int irq, void *dev_id)
{
struct intpin_desc * pindesc = (struct intpin_desc *)dev_id;
unsigned int pinval;
pinval = gpio_get_value(pindesc->pin);
if (pinval){
/*松开*/
key_val = 0x80 | pindesc->key_val;
}else{
/*按下*/
key_val = pindesc->key_val;
}
//ev_press = 1;
//wake_up_interruptible(&button_waitq); /*唤醒*/
kill_fasync(&button_async, SIGIO, POLL_IN); /* 信号发送程序 */
return IRQ_HANDLED;
}
信号发送出去后,app便会执行信号处理函数,即读取按键值
分析
1.signal函数,。。。布吉岛,功能是绑定信号处理函数
2.fcntl
由以前的经验也能猜出来了,
/fs/fcntl.c
SYSCALL_DEFINE3(fcntl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
|
do_fcntl(fd, cmd, arg, f.file);
这个函数好直观,这里只看用到的几个命令
static long do_fcntl(int fd, unsigned int cmd, unsigned long arg, struct file *filp)
{
...
switch (cmd) {
...
case F_GETFL:
err = filp->f_flags;
break;
case F_SETFL:
err = setfl(fd, filp, arg);
break;
..
case F_SETOWN:
f_setown(filp, arg, 1);
err = 0;
break;
...
default:
break;
}
return err;
}
第一个命令比较直观,跳过
先来分析第三个f_setown()
f_setown(filp, arg, 1)
__f_setown(filp, pid, type, force);
f_modown(filp, pid, type, force);
static void f_modown(struct file *filp, struct pid *pid, enum pid_type type, int force)
{
...
if (force || !filp->f_owner.pid) {
put_pid(filp->f_owner.pid);
filp->f_owner.pid = get_pid(pid); // 设置了程序进程pid
filp->f_owner.pid_type = type;
if (pid) {
const struct cred *cred = current_cred();
filp->f_owner.uid = cred->uid;
filp->f_owner.euid = cred->euid;
}
}
...
}
再来第二个setfl(),这个调用了我们定义的驱动fasync函数
static int setfl(int fd, struct file * filp, unsigned long arg)
{
...
/* 如果设置了FASYNC标志
* ->fasync() is responsible for setting the FASYNC bit.
*/
if (((arg ^ filp->f_flags) & FASYNC) && filp->f_op->fasync) {
error = filp->f_op->fasync(fd, filp, (arg & FASYNC) != 0);
//前面接触了这么多内核驱动接口,看到filp->f_op->fasync,应该知道这是调用我们注册的自定义fasync函数了,实际上是调用fasync_helper()
// 感觉这才是这个函数最大的意义,名字取的不好。。。
...
}
...
//修改flags
filp->f_flags = (arg & SETFL_MASK) | (filp->f_flags & ~SETFL_MASK);
...
}
我们的底层驱动中干了什么?
buttons_drv_fasync
fasync_helper(fd, file, on, &button_async); // static struct fasync_struct *button_async;
/*
* fasync_helper() is used by almost all character device drivers
* to set up the fasync queue, and for regular files by the file
* lease code. It returns negative on error, 0 if it did no changes
* and positive if it added/deleted the entry.
被绝大多数字符设备调用设置fasync(异步通知)队列,错误返回负数,成功添加|删除入口返回0
*/
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
{
if (!on)
return fasync_remove_entry(filp, fapp);
return fasync_add_entry(fd, filp, fapp);
}
/*
* Add a fasync entry. Return negative on error, positive if
* added, and zero if did nothing but change an existing one.
添加一个异步通知入口,错误返回负数,添加返回正数,改变存在项返回0
*/
static int fasync_add_entry(int fd, struct file *filp, struct fasync_struct **fapp)
{
...
/*
* fasync_insert_entry() returns the old (update) entry if
* it existed.
*
* So free the (unused) new entry and return 0 to let the
* caller know that we didn't add any new fasync entries.
如果存在要添加的入口项,则释放新分配的入口,返回0,让用户知道
*/
if (fasync_insert_entry(fd, filp, fapp, new)) {
fasync_free(new);
return 0;
}
return 1;
}
综上,当应用程序使用F_SETFL命令后,会调用驱动的drv_fasync()->fasync_helper(),经过一系列操作,将当前设备添加进内核异步通知链表。
以上所有准备工作都已经完成。
功能实现,是靠:
kill_fasync(&button_async, SIGIO, POLL_IN);
void kill_fasync(struct fasync_struct **fp, int sig, int band)
{
...
kill_fasync_rcu(rcu_dereference(*fp), sig, band);
...
}
static void kill_fasync_rcu(struct fasync_struct *fa, int sig, int band)
{
while (fa) {
struct fown_struct *fown;
unsigned long flags;
...
if (fa->fa_file) {
/*
struct fown_struct {
rwlock_t lock; // protects pid, uid, euid fields
int pid; // pid or -pgrp where SIGIO should be sent
uid_t uid, euid; // uid/euid of process setting the owner
void *security;
int signum; // posix.1b rt signal to be delivered on IO
};
*/
fown = &fa->fa_file->f_owner; // fasync_struct ->fa_file->f_owner
//通过异步对象的文件指针知道其属主进程
...
send_sigio(fown, fa->fa_fd, band);
}
...
}
}
分析到此为止send_sigio()发送信号的具体细节就不深究了
总结
将进程“添加”到异步通知队列中,实则是通过文件指针关联对象,将对象添加到队列中。定位进程则是反过来,根据文件指针查找,然后根据对应文件指针去找关联的进程,所以有时候会出现一个文件指针可以找到多个进程(多个进程打开了该文件)。这种情况下,应用程序仍然必须借助于poll/select来确定输入的来源。
从用户程序的角度考虑:为了启动文件的异步通知机制,用户程序必须执行两个步骤。首先,她们指定一个进程作为文件的“属主(owner)”。当进程使用fcntl系统调用执行F_SETOWN命令时,属主进程的进程ID号就被保存在filp->f_owner中。这一步是必需的,目的是为了让内核知道应该通知哪个进程;然后,为了真正启动异步通知机制,用户程序还必须在设备中设置FASYNC标志,这通过fcntl的F_SETFL命令完成的。 执行完这两个步骤之后,输入文件就可以在新数据到达时请求发送一个SIGIO信号。该信号被发送到存放在filp->f_owner中的进程(如果是负值就是进程组)。
从驱动程序角度考虑:F_SETOWN被调用时对filp->f_owner赋值,此外什么也不做;在执行F_SETFL启用FASYNC时,调用驱动程序的fasync方法。只要filp->f_flags中的FASYNC标识发生了变化,就会调用该方法,以便把这个变化通知驱动程序,使其能正确响应。文件打开时,FASYNC标志被默认为是清除的。当数据到达时,所有注册为异步通知的进程都会被发送一个SIGIO信号。