imx6ull-驱动开发篇31——Linux异步通知
目录
异步通知
异步通知概念
信号
signal 函数
驱动中的信号处理
fasync_struct 结构体
fasync 函数
kill_fasync 函数
应用程序对异步通知的处理
异步通知
Linux 应用程序可以通过阻塞或者非阻塞这两种方式来访问驱动设备:
- 通过阻塞方式访问的话,应用程序会处于休眠态,等待驱动设备可以使用,
- 非阻塞方式,会通过 poll 函数来不断的轮询,查看驱动设备文件是否可以使用。
这两种方式都需要应用程序主动地去查询设备的使用情况,这就需要用到异步通知,也就是驱动可以通过主动向应用程序发送信号。
异步通知概念
-
进程先发起I/O请求,内核在操作完成后主动通知进程(通过信号或回调),期间进程无需阻塞或轮询。
-
异步机制:操作结果通过回调/信号返回。
维度 | 说明 |
---|---|
通知方式 | 信号( |
设置函数 |
|
优点 | 资源利用率最高,适合高吞吐场景 |
缺点 | 编程复杂度高,需处理信号竞争 |
适用场景 | 高性能服务器(如Nginx)、磁盘I/O密集型应用 |
阻塞、非阻塞、异步通知,这三种是针对不同的场合提出来的不同的解决方法,在实际的工作和学习中,根据自己的实际需求选择合适的处理方法即可。
信号
异步通知的核心就是信号,在 arch/xtensa/include/uapi/asm/signal.h 文件中定义了 Linux 所支持的所有信号,这些信号如下所示:
/* 信号宏定义列表(Linux标准信号)*/
#define SIGHUP 1 // 终端挂起或控制进程终止
#define SIGINT 2 // 终端中断(Ctrl+C触发)
#define SIGQUIT 3 // 终端退出(Ctrl+\触发)
#define SIGILL 4 // 非法指令
#define SIGTRAP 5 // 调试断点
#define SIGABRT 6 // 进程中止(abort()触发)
#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 // 定时器信号(alarm()触发)
#define SIGTERM 15 // 进程终止(kill默认发送)
#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 // I/O就绪(异步I/O事件)
#define SIGPWR 30 // 电源故障/重启
#define SIGSYS 31 // 非法系统调用
这些信号中,除了 SIGKILL(9)和 SIGSTOP(19)这两个信号不能被忽略外,其他的信号都可以忽略。
这些信号就相当于中断号,不同的中断号代表了不同的中断,不同的中断所做的处理不同,因此,驱动程序可以通过向应用程序发送不同的信号来实现不同的功能。
signal 函数
在应用程序中使用信号,那么就必须设置信号所使用的信号处理函数,在应用程序中使用 signal 函数来设置指定信号的处理函数。
signal 函数原型如下所示:
sighandler_t signal(int signum, sighandler_t handler)
- signum:要设置处理函数的信号。
- handler: 信号的处理函数。
- 返回值: 设置成功的话返回信号的前一个处理函数,设置失败的话返回 SIG_ERR。
信号处理函数原型如下所示:
typedef void (*sighandler_t)(int)
使用这两个函数的示例代码如下:
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>// SIGINT信号处理函数(Ctrl+C触发)
void sigint_handler(int signum)
{printf("\nSIGINT signal received! (Signal number: %d)\n", signum);exit(EXIT_SUCCESS); // 正常退出程序
}int main(void)
{// 注册信号处理函数if (signal(SIGINT, sigint_handler) == SIG_ERR) {perror("Failed to register signal handler");return EXIT_FAILURE;}printf("Press Ctrl+C to trigger SIGINT...\n");// 保持程序运行while(1) {pause(); // 更推荐使用pause()替代空循环}return 0;
}
我们设置 SIGINT 信号的处理函数为 sigint_handler,当按下 CTRL+C向 signaltest 发送 SIGINT 信号以后 sigint_handler 函数就会执行。
此函数先输出一行“SIGINT signal!”字符串,然后调用 exit 函数关闭 signaltest 应用程序。
驱动中的信号处理
fasync_struct 结构体
fasync_struct 结构体内容如下:
/*** struct fasync_struct - 管理异步通知(SIGIO)的核心数据结构* @fa_lock: 自旋锁,保护结构体的并发访问* @magic: 魔数(FA_MAGIC),用于验证结构有效性* @fa_fd: 关联的文件描述符* @fa_next: 指向下一个异步通知结构的指针(形成链表)* @fa_file: 关联的file结构指针* @fa_rcu: RCU(Read-Copy-Update)回调头,用于安全释放** 作用:将进程与设备文件关联,当设备事件发生时通过SIGIO通知进程*/
struct fasync_struct {spinlock_t fa_lock; // 保护链表的自旋锁int magic; // 必须等于FA_MAGIC(0x4601)int fa_fd; // 用户空间的文件描述符struct fasync_struct *fa_next; // 形成单链表struct file *fa_file; // 关联的file结构struct rcu_head fa_rcu; // RCU释放机制
};
我们需要在驱动程序中,定义一个 fasync_struct 结构体指针变量,添加定义到设备结构体中:
struct fasync_struct *async_queue; /* 异步相关结构体 */
fasync 函数
要使用异步通知,需要在设备驱动中实现 file_operations 操作集中的 fasync 函数,此函数格式如下所示:
int (*fasync) (int fd, struct file *filp, int on)
fasync 函数里面一般通过调用 fasync_helper 函数来初始化前面定义的 fasync_struct 结构体指针。
fasync_helper 函数原型如下:
/*** fasync_helper - 管理异步通知(SIGIO)的注册/注销* @fd: 用户空间文件描述符(实际未使用)* @filp: 关联的file结构指针* @on: 激活标志(1=注册,0=注销)* @fapp: 指向驱动中fasync_struct指针的指针** 返回值:* ≥0: 成功(返回0表示无变化)* <0: 错误码(如内存不足)** 作用:维护驱动中的异步通知链表,当设备事件发生时通过kill_fasync()发送SIGIO信号*/
int fasync_helper(int fd, struct file *filp, int on, struct fasync_struct **fapp);
第四个参数就是要初始化的 fasync_struct 结构体指针变量。
当应用程序通过“fcntl(fd, F_SETFL, flags | FASYNC)”改变fasync 标记的时候,驱动程序 file_operations 操作集中的 fasync 函数就会执行。
关闭驱动文件的时候,需要在 file_operations 操作集中的 release 函数中释放 fasync_struct, fasync_struct 的释放函数同样为 fasync_helper。
示例代码如下:
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/module.h>
#include <linux/sched/signal.h>MODULE_LICENSE("GPL");// 设备私有数据结构
struct my_device {struct fasync_struct *fasync_queue; // 异步通知队列头// 其他设备字段...
};// fasync函数实现
static int my_fasync(int fd, struct file *filp, int on)
{struct my_device *dev = filp->private_data;int ret;// 调用fasync_helper管理异步通知队列ret = fasync_helper(fd, filp, on, &dev->fasync_queue);if (ret < 0)return ret;printk(KERN_INFO "fasync %s for fd %d\n", on ? "added" : "removed", fd);return 0;
}// release函数实现
static int my_release(struct inode *inode, struct file *filp)
{// 强制移除所有异步通知监听my_fasync(-1, filp, 0);printk(KERN_INFO "Device released, fasync queue cleared\n");return 0;
}// 中断处理函数(示例)
static irqreturn_t data_irq(int irq, void *dev_id)
{struct my_device *dev = dev_id;// 当数据就绪时通知监听进程if (dev->fasync_queue) {kill_fasync(&dev->fasync_queue, SIGIO, POLL_IN);printk(KERN_INFO "Sent SIGIO to listeners\n");}return IRQ_HANDLED;
}// file_operations结构体
static const struct file_operations my_fops = {.owner = THIS_MODULE,.fasync = my_fasync, // 异步通知方法.release = my_release, // 必须清理fasync队列// 其他操作...
};// 设备初始化(示例)
static int __init my_init(void)
{struct my_device *dev;dev = kzalloc(sizeof(*dev), GFP_KERNEL);if (!dev)return -ENOMEM;// 注册设备等操作...return 0;
}module_init(my_init);
kill_fasync 函数
当设备可以访问的时候,驱动程序需要向应用程序发出信号,相当于产生“中断”。
kill_fasync函数负责发送指定的信号, kill_fasync 函数原型如下所示:
void kill_fasync(struct fasync_struct **fp, int sig, int band)
函数参数和返回值含义如下:
- fp:要操作的 fasync_struct。
- sig: 要发送的信号。
- band: 可读时设置为 POLL_IN,可写时设置为 POLL_OUT。
应用程序对异步通知的处理
应用程序对异步通知的处理包括以下三步:
- 注册信号处理函数
- 将应用程序的进程号告诉给内核
- 开启异步通知
1、注册信号处理函数
应用程序根据驱动程序所使用的信号来设置信号的处理函数,应用程序使用 signal 函数来设置信号的处理函数。
2、将应用程序的进程号告诉给内核
使用如下命令将本应用程序的进程号告诉给内核:
fcntl(fd, F_SETOWN, getpid())
3、开启异步通知
使用如下两行程序开启异步通知:
flags = fcntl(fd, F_GETFL); /* 获取当前的进程状态 */
fcntl(fd, F_SETFL, flags | FASYNC); /* 开启当前进程异步通知功能 */
通过 fcntl 函数设置进程状态为 FASYNC,经过这一步,驱动程序中的 fasync 函数就会执行。
用户空间配置流程大概如图:
下一讲实验,我们编写相应的驱动和测试代码。