Linux异步通知机制详解
目录
声明
1. 异步通知简介
2.驱动中的信号处理
2.1 fasync_struct 结构体
2.2 fasync 函数
2.3 kill_fasync 函数
2.4 应用程序对异步通知的处理
声明
本博客所记录的关于正点原子i.MX6ULL开发板的学习笔记,(内容参照正点原子I.MX6U嵌入式linux驱动开发指南,可在正点原子官方获取正点原子Linux开发板 — 正点原子资料下载中心 1.0.0 文档),旨在如实记录我在学校学习该开发板过程中所遭遇的各类问题以及详细的解决办法。其初衷纯粹是为了个人知识梳理、学习总结以及日后回顾查阅方便,同时也期望能为同样在学习这款开发板的同学或爱好者提供一些解决问题的思路和参考。我尽力保证内容的准确性和可靠性,但由于个人知识水平和实践经验有限,若存在错误或不严谨之处,恳请各位读者批评指正。
责任声明:虽然我力求提供有效的问题解决办法,但由于开发板使用环境、硬件差异、软件版本等多种因素的影响,我的笔记内容不一定适用于所有情况。对于因参考本笔记而导致的任何直接或间接损失,我不承担任何法律责任。使用本笔记内容的读者应自行承担相关风险,并在必要时寻求专业技术支持。
1. 异步通知简介
Linux 应用程序可以通过阻塞或者非阻塞这两种方式来访问驱动设备,通过阻塞方式访问的话应用程序会处于休眠态,等待驱动设备可以使用,非阻塞方式的话会通过 poll 函数来不断的轮询,查看驱动设备文件是否可以使用。这两种方式都需要应用程序主动的去查询设备的使用情况。如果能提供一种类似中断的机制,当驱动程序可以访问的时候主动告诉应用程序那就最好了。
“信号”为此应运而生,信号类似于我们硬件上使用的“中断”,只不过信号是软件层次上的。算是在软件层次上对中断的一种模拟,驱动可以通过主动向应用程序发送信号的方式来报告自己可以访问了,应用程序获取到信号以后就可以从驱动设备中读取或者写入数据了。整个过程就相当于应用程序收到了驱动发送过来了的一个中断,然后应用程序去响应这个中断,在整个处理过程中应用程序并没有去查询驱动设备是否可以访问,一切都是由驱动设备自己告诉给应用程序的。
异步通知的核心就是信号,在 arch/xtensa/include/uapi/asm/signal.h 文件中定义了 Linux 所支持的所有信号,这些信号如下所示:
34 #define SIGHUP 1 /* 终端挂起或控制进程终止 */
35 #define SIGINT 2 /* 终端中断(Ctrl+C 组合键) */
36 #define SIGQUIT 3 /* 终端退出(Ctrl+\组合键) */
37 #define SIGILL 4 /* 非法指令 */
38 #define SIGTRAP 5 /* debug 使用,有断点指令产生 */
39 #define SIGABRT 6 /* 由 abort(3)发出的退出指令 */
40 #define SIGIOT 6 /* IOT 指令 */
41 #define SIGBUS 7 /* 总线错误 */
42 #define SIGFPE 8 /* 浮点运算错误 */
43 #define SIGKILL 9 /* 杀死、终止进程 */
44 #define SIGUSR1 10 /* 用户自定义信号 1 */
45 #define SIGSEGV 11 /* 段违例(无效的内存段) */
46 #define SIGUSR2 12 /* 用户自定义信号 2 */
47 #define SIGPIPE 13 /* 向非读管道写入数据 */
48 #define SIGALRM 14 /* 闹钟 */
49 #define SIGTERM 15 /* 软件终止 */
50 #define SIGSTKFLT 16 /* 栈异常 */
51 #define SIGCHLD 17 /* 子进程结束 */
52 #define SIGCONT 18 /* 进程继续 */
53 #define SIGSTOP 19 /* 停止进程的执行,只是暂停 */
54 #define SIGTSTP 20 /* 停止进程的运行(Ctrl+Z 组合键) */
55 #define SIGTTIN 21 /* 后台进程需要从终端读取数据 */
56 #define SIGTTOU 22 /* 后台进程需要向终端写数据 */
57 #define SIGURG 23 /* 有"紧急"数据 */
58 #define SIGXCPU 24 /* 超过 CPU 资源限制 */
59 #define SIGXFSZ 25 /* 文件大小超额 */
60 #define SIGVTALRM 26 /* 虚拟时钟信号 */
61 #define SIGPROF 27 /* 时钟信号描述 */
62 #define SIGWINCH 28 /* 窗口大小改变 */
63 #define SIGIO 29 /* 可以进行输入/输出操作 */
64 #define SIGPOLL SIGIO
65 /* #define SIGLOS 29 */
66 #define SIGPWR 30 /* 断点重启 */
67 #define SIGSYS 31 /* 非法的系统调用 */
68 #define SIGUNUSED 31 /* 未使用信号 */
除了 SIGKILL(9)和 SIGSTOP(19)这两个信号不能被忽略外,其他的信号都可以忽略。这些信号就相当于中断号,不同的中断号代表了不同的中断,不同的中断所做的处理不同。
使用中断的时候需要设置中断处理函数,同样的,如果要在应用程序中使用信号,那么就必须设置信号所使用的信号处理函数,在应用程序中使用 signal 函数来设置指定信号的处理函数, signal 函数原型如下所示:
#include <signal.h>typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);
函数参数和返回值含义如下: signum:要设置处理函数的信号。handler: 信号的处理函数。
返回值: 设置成功的话返回信号的前一个处理函数,设置失败的话返回 SIG_ERR。
使用示例:
/** 信号处理示例程序 - 捕获SIGINT信号(Ctrl+C)* 编译: gcc signal_example.c -o signal_example* 运行: ./signal_example*/#include <stdio.h> // 标准输入输出,用于printf等函数
#include <signal.h> // 信号处理相关函数和宏定义
#include <unistd.h> // 提供sleep()等POSIX系统调用/** 信号处理函数* @param signo 接收到的信号编号* 注意:* 1. 信号处理函数应尽量简单,避免使用非异步安全的函数* 2. 在信号处理函数中使用printf是不安全的,这里仅用于演示*/
void sig_handler(int signo) {// 检查接收到的信号是否为SIGINT(通常由Ctrl+C触发)if (signo == SIGINT) {printf("Received SIGINT (Ctrl+C pressed)\n");// 实际应用中,这里应该设置标志位而不是直接处理// 因为printf()在信号处理函数中不是异步安全的}
}int main() {/** 设置SIGINT的信号处理函数* signal()函数参数:* SIGINT - 要捕获的信号(中断信号,值通常为2)* sig_handler - 信号处理函数指针* 返回值:* 成功 - 返回之前的信号处理函数* 失败 - 返回SIG_ERR*/if (signal(SIGINT, sig_handler) == SIG_ERR) {// 如果设置信号处理失败,打印错误信息并退出perror("Error setting signal handler");return 1; // 返回非零表示错误退出}/** 主循环* 不断打印运行信息,每秒一次* 按下Ctrl+C会触发SIGINT信号* 按下Ctrl+\会触发SIGQUIT信号(未处理,默认终止程序)*/while(1) {printf("Program is running...\n"); // 打印运行状态sleep(1); // 休眠1秒/* * 注意:* 1. sleep()可能被信号中断而提前返回* 2. 实际应用中应考虑检查sleep的返回值* 3. 长时间运行的程序应该添加退出条件*/}return 0; // 理论上不会执行到这里
}/** 补充说明:* 1. 在生产环境中,建议使用sigaction()替代signal(),因为它提供更可靠的行为* 2. 信号处理函数中应避免使用不可重入函数(如malloc, printf等)* 3. 更好的做法是在信号处理函数中设置标志位,在主循环中检查并处理* 4. SIGKILL(9)和SIGSTOP(19)信号不能被捕获或忽略* 5. 多次按下Ctrl+C可能会导致信号处理重入问题*/
运行结果
2.驱动中的信号处理
2.1 fasync_struct 结构体
需要在驱动程序中定义一个 fasync_struct 结构体指针变量, fasync_struct 结构体内容如下:
/*** struct fasync_struct - 异步通知结构体(用于实现 FASYNC 机制)* * 当应用程序通过 fcntl(F_SETFL) 设置 O_ASYNC 标志时,* 内核会创建此结构体以实现文件描述符的异步事件通知(如 SIGIO 信号)。* 主要用于字符设备驱动、socket等需要事件驱动的场景。*/
struct fasync_struct {/* 自旋锁,保护该结构的并发访问 */spinlock_t fa_lock; // 防止多核竞争条件下的数据竞争/* 魔术字,用于调试和验证结构体有效性 */int magic; // 通常设为 FASYNC_MAGIC (0x4601)/* 关联的文件描述符(用户空间传入的 fd) */int fa_fd; // 需要通知的目标文件描述符/* 链表指针,链接所有异步通知结构 */struct fasync_struct *fa_next; // 所有异步结构通过链表管理/* 关联的 file 结构指针 */struct file *fa_file; // 指向内核文件对象(struct file)/* RCU(Read-Copy-Update)回调头,用于安全释放 */struct rcu_head fa_rcu; // 保证结构体在无锁读取时能安全释放
};
一般将 fasync_struct 结构体指针变量定义到设备结构体中
2.2 fasync 函数
如果要使用异步通知,需要在设备驱动中实现 file_operations 操作集中的 fasync 函数,此函数格式如下所示:
int (*fasync) (int fd, struct file *filp, int on)
fasync 函数里面一般通过调用 fasync_helper 函数来初始化前面定义的 fasync_struct 结构体指针, fasync_helper 函数原型如下:
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
fasync_helper 函数的前三个参数就是 fasync 函数的那三个参数,第四个参数就是要初始化的 fasync_struct 结构体指针变量。当应用程序通过“fcntl(fd, F_SETFL, flags | FASYNC)”改变fasync 标记的时候,驱动程序 file_operations 操作集中的 fasync 函数就会执行。
驱动程序中的 fasync 函数参考示例如下:
/* * 设备驱动私有数据结构体* 包含设备特定的状态和控制信息*/
struct xxx_dev {/* 其他设备相关成员... *//** async_queue - 异步通知队列头指针* 用于管理所有注册了异步通知(FASYNC)的进程* 当设备事件发生时,内核通过这个链表通知所有注册的进程*/struct fasync_struct *async_queue; /* 异步相关结构体 */
};/** xxx_fasync - 处理异步通知(FASYNC)的启用/禁用* @fd: 用户空间文件描述符* @filp: 内核文件对象指针* @on: 启用(1)或禁用(0)异步通知* * 返回值: 成功返回0,失败返回错误码* * 功能: 当用户调用fcntl(F_SETFL)设置/清除O_ASYNC标志时,* 内核会调用此函数来添加或移除fasync_struct*/
static int xxx_fasync(int fd, struct file *filp, int on)
{/* 从文件对象的private_data获取设备私有数据 */struct xxx_dev *dev = (struct xxx_dev *)filp->private_data;/** 使用内核提供的fasync_helper辅助函数* 参数:* fd - 用户空间文件描述符* filp - 内核文件对象* on - 启用/禁用标志* &dev->async_queue - 指向设备异步队列头的指针* * 这个函数会:* 1. 当on=1时: 创建并添加fasync_struct到async_queue链表* 2. 当on=0时: 从async_queue中移除对应的fasync_struct*/if (fasync_helper(fd, filp, on, &dev->async_queue) < 0)return -EIO; /* 操作失败返回I/O错误 */return 0; /* 成功返回0 */
}/* 设备文件操作集 */
static struct file_operations xxx_ops = {/* 其他操作... *//** .fasync - 异步通知操作函数指针* 必须实现这个函数才能支持O_ASYNC/FASYNC功能* 当用户空间调用fcntl(F_SETFL)设置O_ASYNC时,* 内核会通过此函数指针调用我们实现的xxx_fasync*/.fasync = xxx_fasync,/* 其他操作... */
};
在关闭驱动文件的时候需要在 file_operations 操作集中的 release 函数中释放 fasync_struct, fasync_struct 的释放函数同样为 fasync_helper, release 函数参数参考实例如下:
1 static int xxx_release(struct inode *inode, struct file *filp)
2 {
3 return xxx_fasync(-1, filp, 0); /* 删除异步通知 */
4 }
5
6 static struct file_operations xxx_ops = {
7 ......
8 .release = xxx_release,
9 };
第 3 行通过调用示例代码 53.1.2.3 中的 xxx_fasync 函数来完成 fasync_struct 的释放工作,但是,其最终还是通过 fasync_helper 函数完成释放工作。
2.3 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。
返回值: 无
2.4 应用程序对异步通知的处理
应用程序对异步通知的处理包括以下三步:
1、注册信号处理函数
应用程序根据驱动程序所使用的信号来设置信号的处理函数,应用程序使用 signal 函数来设置信号的处理函数。
2、将本应用程序的进程号告诉给内核
使用 fcntl(fd, F_SETOWN, getpid())将本应用程序的进程号告诉给内核。
3、开启异步通知
使用如下两行程序开启异步通知:
flags = fcntl(fd, F_GETFL); /* 获取当前的进程状态 */fcntl(fd, F_SETFL, flags | FASYNC); /* 开启当前进程异步通知功能 */
重点就是通过 fcntl 函数设置进程状态为 FASYNC,经过这一步,驱动程序中的 fasync 函数就会执行。