异步通知
生活例子:买奶茶
- 在旁边等着,眼睛盯着店员,生怕别人插队,他一做好你就知道:你是主动等待他做好,这叫“同步”。
- 付钱后就去玩手机了,店员做好后他会打电话告诉你:你是被动获得结果,这叫“异步”。
使用流程
驱动程序怎么通知 APP: 发信号,这只有 3 个字,却可以引发很多问题:
- 谁发:驱动程序发。
- 发什么:信号。
- 发什么信号:SIGIO
- 怎么发:内核里提供有函数
- 发给谁:APP,APP 要把自己告诉驱动
- APP 收到后做什么:执行信号处理函数
- 信号处理函数和信号,之间怎么挂钩: APP 注册信号处理函数
Linux 系统中也有很多信号,在 Linux 内核源文件 include\uapi\asmgeneric\signal.h
中,有很多信号的宏定义
#define SIGHUP 1
#define SIGINT 2
#define SIGQUIT 3
#define SIGILL 4
#define SIGTRAP 5
#define SIGABRT 6
#define SIGIOT 6
#define SIGBUS 7
#define SIGFPE 8
#define SIGKILL 9
#define SIGUSR1 10
#define SIGSEGV 11
#define SIGUSR2 12
#define SIGPIPE 13
#define SIGALRM 14
#define SIGTERM 15
#define SIGSTKFLT 16
#define SIGCHLD 17
#define SIGCONT 18
#define SIGSTOP 19
#define SIGTSTP 20
#define SIGTTIN 21
#define SIGTTOU 22
#define SIGURG 23
#define SIGXCPU 24
#define SIGXFSZ 25
#define SIGVTALRM 26
#define SIGPROF 27
#define SIGWINCH 28
#define SIGIO 29
#define SIGPOLL SIGIO
/*
#define SIGLOST 29
*/
#define SIGPWR 30
#define SIGSYS 31
#define SIGUNUSED 31
/* These should not be considered constants from userland. */
#define SIGRTMIN 32
#ifndef SIGRTMAX
#define SIGRTMAX _NSIG
#endif
就 APP 而言,你想处理 SIGIO 信息,那么需要提供信号处理函数,并且要跟 SIGIO 挂钩。这可以通过一个 signal 函数来“给某个信号注册处理函数”,用法如下:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
APP 还要做什么事?想想这几个问题:
- 内核里有那么多驱动,你想让哪一个驱动给你发 SIGIO 信号?
APP 要打开驱动程序的设备节点。 - 驱动程序怎么知道要发信号给你而不是别人?
APP 要把自己的进程 ID 告诉驱动程序。 - APP 有时候想收到信号,有时候又不想收到信号:
应该可以把 APP 的意愿告诉驱动。
驱动程序要做什么?发信号。
- APP 设置进程 ID 时,驱动程序要记录下进程 ID;
- APP 还要使能驱动程序的异步通知功能,驱动中有对应的函数:
- APP 打开驱动程序时,内核会创建对应的 file 结构体, file 中有 f_flags;f_flags 中有一个 FASYNC 位,它被设置为 1 时表示使能异步通知功能。当 f_flags 中的 FASYNC 位发生变化时,驱动程序的 fasync 函数被调用。
- 发生中断时,有数据时,驱动程序调用内核辅助函数发信号。这个辅助函数名为 kill_fasync。
综上所述,使用异步通知,也就是使用信号的流程如下图所示:
应用编程
重点从2开始
② 注册信号处理函数:APP 给 SIGIO 这个信号注册信号处理函数 func,以后 APP 收到 SIGIO信号时,这个函数会被自动调用;
③ 把APP的PID(进程ID)告诉驱动程序,这个调用不涉及驱动程序,在内核的文件系统层次记录 PID;
④ 读取驱动程序文件 Flag;
⑤ 设置Flag里面的FASYNC位为1:当FASYNC位发生变化时,会导致驱动程序的fasync被调用;
⑥⑦ 调用faync_helper,它会根据FAYSNC的值决定是否设置button_async->fa_file=驱动文件 filp:驱动文件 filp 结构体里面含有之前设置的 PID;
⑧ APP 可以做其他事;
⑨⑩按下按键,发生中断,驱动程序的中断服务程序被调用,里面调用kill_fasync 发信号;
⑪⑫⑬ APP 收到信号后,它的信号处理函数被自动调用,可以在里面调用 read 函数读取按键。
总结
应用层需要做的:
- 写信号处理函数
static void sig_func(int sig)
{
in val;
read(fd, &val, 4);
printf("get button: 0x%x\n", val);
}
- 注册信号处理函数
// 3注册信号处理函数
signal(SIGIO, sig_func);
- 打开文件
fd = open(argv[1], O_RDWR);
- 把APP的pid告诉驱动程序;
fcntl(fd ,F_SETOWN, getpid());
- 读取驱动程序flag并设置FASYNC位为1
flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | FASYNC);
驱动编程
驱动层需要做的:
- 提供对应的fasync函数,并调用fasync_helper函数;
struct fasync_struct *button_fasync;
static int gpio_key_drv_fasync(int fd, struct file *file, int on)
{
if (fasync_helper(fd, file, on, &button_fasync) >= 0)
{
return 0;
}
else
{
return -EIO;
}
}
// 定义自己的file_operations结构体
static struct file_operations gpio_key_drv = {
.owner = THIS_MODULE,
.read = gpio_key_drv_read,
.poll = gpio_key_drv_poll,
.fasync = gpio_key_drv_fasync,
};
fasync_helper 函数会分配、构造一个fasync_struct结构体button_async:
驱动文件的 flag 被设置为 FAYNC 时:
button_async->fa_file = filp; // filp 表示驱动程序文件,里面含有之前设置的 PID
驱动文件被设置为非 FASYNC 时:
button_async->fa_file = NULL;
以后想发送信号时,使用 button_async 作为参数就可以,它里面“可能”含有 PID。
什么时候发信号呢?在本例中,在 GPIO 中断服务程序中发信号。
- 中断处理函数中使用kill fasync发送信号。
kill_fasync(&button_fasync, SIGIO, POLL_IN);
- 第 1 个参数: button_async->fa_file 非空时,可以从中得到 PID,表示发给哪一个 APP;
- 第 2 个参数表示发什么信号: SIGIO;
- 第 3 个参数表示为什么发信号: POLL_IN,有数据可以读了。 (APP 用不到这个参数)
编译
测试
把编译出来的设备树文件拷贝到/boot目录下
reboot重启
安装驱动,强制安装
insmod -f gpio_key_drv.ko
执行测试程序
./button_test /dev/100ask_gpio_key