异步通知(或称信号驱动I/O)为Linux设备驱动提供了一种高效的通信机制。其核心在于:当设备事件(如数据到达)发生时,由驱动主动发送信号通知应用进程,从而将应用从轮询或阻塞等待中解放出来。
异步通知的工作原理
这是一种基于信号的机制,允许内核在设备事件(例如数据可用、缓冲区空闲)发生时,向用户空间进程发送一个特定的信号(通常是SIGIO)。
其核心流程分为用户空间与驱动空间两部分:
1. 用户空间的关键操作
- 使用
fcntl()系统调用设置文件描述符的FASYNC标志,启用异步通知。
- 通过
fcntl()的F_SETOWN命令,指定接收信号的进程ID或进程组ID。
- 使用
sigaction()等函数为SIGIO信号注册处理函数。
2. 驱动程序的核心实现
- 在驱动的
file_operations结构中实现.fasync方法。
- 在
.fasync方法中,调用内核的fasync_helper()函数来管理一个异步通知链表(struct fasync_struct *)。
- 当设备事件发生时(通常在中断或任务处理函数中),调用
kill_fasync()函数向链表中所有已注册的进程发送SIGIO信号。
核心函数与结构
| 内核函数/结构 |
描述 |
fasync_helper() |
负责添加或删除文件描述符到驱动的异步通知链表中。 |
kill_fasync() |
向链表中所有注册的进程发送信号(如SIGIO),并可携带POLLIN/POLLOUT等事件标志。 |
struct fasync_struct |
驱动内部用于管理所有异步通知请求的链表头。 |
总结起来,实现一个支持异步通知的字符设备驱动,主要涉及三个步骤:定义fasync_struct结构体指针,实现.fasync文件操作方法,在适当的时候调用kill_fasync()发送信号。
实战示例:一个简单的异步通知驱动
假设有一个字符设备,当它的缓冲区有新数据准备好时,需要主动通知用户空间进程。
1. 驱动程序 (async_dev.c)
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/sched.h> // for wake_up_interruptible
#include <linux/signal.h> // for kill_fasync
#include <linux/poll.h> // for poll/epoll/select (Good practice)
#include <asm/uaccess.h>
#define DEV_NAME "async_dev"
#define DEV_CNT 1
// 1. 定义 fasync 链表头
static struct fasync_struct *async_fasync_list = NULL;
static dev_t devno;
static struct cdev async_cdev;
static struct class *async_class;
// 缓冲区(简化,实际中可能是硬件寄存器或DMA缓冲区)
static char device_buffer[64];
static int buffer_data_size = 0;
/* --- 文件操作方法的实现 --- */
/**
* @brief 异步通知回调函数:用于设置或清除 FASYNC 标志
* @param inode 设备节点的 inode
* @param filp 文件指针
* @param mode FASYNC 标志 (0:清除, 1:设置)
* @return 0 成功
*/
static int async_fasync(int fd, struct file *filp, int mode)
{
// 调用内核的 fasync_helper 函数,负责维护 async_fasync_list 链表
return fasync_helper(fd, filp, mode, &async_fasync_list);
}
static int async_open(struct inode *inode, struct file *filp)
{
// 允许用户设置 F_SETOWN
// 如果没有这一步,用户空间调用 fcntl(fd, F_SETOWN, pid) 会失败
filp->private_data = &async_cdev;
printk(KERN_INFO "Async device opened.\n");
return 0;
}
static int async_release(struct inode *inode, struct file *filp)
{
// 清除 fasync 标志,防止关闭后仍然发送信号
async_fasync(-1, filp, 0);
printk(KERN_INFO "Async device closed.\n");
return 0;
}
static ssize_t async_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos)
{
// 简化:读取操作
if (buffer_data_size == 0)
return -EAGAIN; // 非阻塞模式下,没有数据返回 -EAGAIN
int len = min((int)count, buffer_data_size);
if (copy_to_user(buf, device_buffer, len)) {
return -EFAULT;
}
buffer_data_size = 0; // 假设数据被读取完
return len;
}
// 模拟数据到达(在实际驱动中,这通常发生在中断处理函数中)
void simulate_data_arrival(void)
{
printk(KERN_INFO "Simulating data arrival.\n");
// 假设有新数据写入 device_buffer
buffer_data_size = snprintf(device_buffer, sizeof(device_buffer),
"Hello from kernel at %ld", jiffies);
// 3. 发送 SIGIO 信号给所有注册的进程
if (async_fasync_list) {
printk(KERN_INFO "Sending SIGIO to user space.\n");
// 发送信号:SIGIO,并设置 POLLIN (表示有数据可读)
kill_fasync(&async_fasync_list, SIGIO, POLLIN);
}
}
// 供用户空间写入的函数(在这里触发数据到达模拟)
static ssize_t async_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
// 用户写入时,我们模拟数据到达
simulate_data_arrival();
return count;
}
static const struct file_operations async_fops = {
.owner = THIS_MODULE,
.open = async_open,
.release = async_release,
.read = async_read,
.write = async_write,
.fasync = async_fasync, // 关键:实现 fasync 方法
};
/* --- 模块加载与卸载 --- */
static int __init async_init(void)
{
// 1. 动态申请设备号
if (alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME) < 0) {
return -EFAULT;
}
// 2. 注册 cdev
cdev_init(&async_cdev, &async_fops);
async_cdev.owner = THIS_MODULE;
cdev_add(&async_cdev, devno, DEV_CNT);
// 3. 创建设备节点 (可选,方便测试)
async_class = class_create(THIS_MODULE, DEV_NAME);
device_create(async_class, NULL, devno, NULL, DEV_NAME);
printk(KERN_INFO "%s driver installed, Major: %d\n", DEV_NAME, MAJOR(devno));
return 0;
}
static void __exit async_exit(void)
{
device_destroy(async_class, devno);
class_destroy(async_class);
cdev_del(&async_cdev);
unregister_chrdev_region(devno, DEV_CNT);
printk(KERN_INFO "%s driver uninstalled.\n", DEV_NAME);
}
module_init(async_init);
module_exit(async_exit);
MODULE_LICENSE("GPL");
2. 用户空间测试程序 (async_user.c)
该程序会设置SIGIO信号处理机制,并配置文件描述符以接收信号。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
#define DEV_FILE "/dev/async_dev"
int fd;
// SIGIO 信号处理函数
void sigio_handler(int signo)
{
char buf[128];
ssize_t len;
// 尝试非阻塞读取数据
len = read(fd, buf, sizeof(buf) - 1);
if (len > 0) {
buf[len] = '\0';
printf(">>> 成功读取数据 (%zd 字节): %s\n", len, buf);
} else if (len == 0) {
printf(">>> 读取结束 (EOF).\n");
} else {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
printf(">>> 尝试读取,但设备再次变为空 (非阻塞模式).\n");
} else {
perror(">>> 读取失败");
}
}
}
int main()
{
// 1. 注册 SIGIO 信号处理器
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = sigio_handler;
sigaction(SIGIO, &sa, NULL);
// 2. 打开设备文件
fd = open(DEV_FILE, O_RDWR); // O_RDWR 是为了能触发驱动的 write 函数
if (fd < 0) {
perror("打开设备文件失败");
return -1;
}
printf("设备文件 %s 打开成功 (fd: %d).\n", DEV_FILE, fd);
// 3. 设置接收信号的进程ID (F_SETOWN)
// 获取当前进程 ID
int pid = getpid();
if (fcntl(fd, F_SETOWN, pid) < 0) {
perror("F_SETOWN 失败");
goto err_close;
}
printf("设置接收进程 ID 为 %d.\n", pid);
// 4. 设置异步通知标志 (FASYNC)
int flags = fcntl(fd, F_GETFL);
if (flags < 0) {
perror("F_GETFL 失败");
goto err_close;
}
if (fcntl(fd, F_SETFL, flags | FASYNC) < 0) {
perror("F_SETFL FASYNC 失败");
goto err_close;
}
printf("设置 FASYNC 标志成功。\n");
// 注意:通常也需要设置 O_NONBLOCK 才能在信号处理函数中进行非阻塞读取
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK | FASYNC) < 0) {
perror("F_SETFL O_NONBLOCK/FASYNC 失败");
goto err_close;
}
printf("已设置非阻塞模式。\n");
printf("\n程序进入等待状态,请在另一个终端向设备写入数据来触发信号:\n");
printf(" 例如: echo 'trigger' > %s\n\n", DEV_FILE);
// 持续运行,等待信号
while (1) {
sleep(1);
}
close(fd);
return 0;
err_close:
close(fd);
return -1;
}
进阶实践:集成到 globalfifo 驱动
下面展示如何为globalfifo(一个典型的字符设备驱动)增加异步通知支持。
1. 扩展设备结构体
需要在globalfifo_dev结构体中添加fasync_struct来管理异步通知链表。
struct globalfifo_dev {
struct cdev cdev;
unsigned int current_len; // 实际存储的数据长度
unsigned char mem[GLOBALFIFO_SIZE]; // 环形缓冲区
spinlock_t lock; // 保护数据结构
wait_queue_head_t r_wait; // 读等待队列
wait_queue_head_t w_wait; // 写等待队列
// **异步通知核心**
struct fasync_struct *async_queue; // 异步通知链表头
};
2. 实现 .fasync 方法
在驱动的file_operations结构中实现.fasync成员。当用户空间调用fcntl(fd, F_SETFL, O_ASYNC)时,此函数负责将进程添加到通知链表中,或在关闭文件时将其移除。
static int globalfifo_fasync(int fd, struct file *filp, int mode)
{
struct globalfifo_dev *dev = filp->private_data;
// 调用内核的 fasync_helper 函数来管理链表
// &dev->async_queue 就是我们要维护的链表头
return fasync_helper(fd, filp, mode, &dev->async_queue);
}
3. 在文件关闭时清除标志
当文件关闭时,必须清除异步通知标志,避免向无效的进程发送信号。
static int globalfifo_release(struct inode *inode, struct file *filp)
{
// 在释放设备时,调用 fasync_helper 清除 fasync 标志 (mode=0)
// -1 是一个占位符,因为我们只关心清除操作
globalfifo_fasync(-1, filp, 0);
return 0;
}
4. 在事件发生时发送信号
当数据状态发生改变(可读或可写)时,需要调用kill_fasync()发送信号。
-
写入后(数据可读):
static ssize_t globalfifo_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
struct globalfifo_dev *dev = filp->private_data;
// ... (处理写入数据,更新 dev->current_len)
// 1. 唤醒等待写入的进程 (如果有)
wake_up_interruptible(&dev->w_wait);
// 2. **发送异步通知 (SIGIO)**
if (dev->async_queue) {
// 发送 SIGIO 信号,携带 POLLIN (数据可读) 事件
kill_fasync(&dev->async_queue, SIGIO, POLLIN);
}
return written_count;
}
-
读取后(空间可写):
static ssize_t globalfifo_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos)
{
struct globalfifo_dev *dev = filp->private_data;
// ... (处理读取数据,更新 dev->current_len)
// 1. 唤醒等待读取的进程 (如果有)
wake_up_interruptible(&dev->r_wait);
// 2. **发送异步通知 (SIGIO)**
if (dev->async_queue) {
// 发送 SIGIO 信号,携带 POLLOUT (空间可写) 事件
kill_fasync(&dev->async_queue, SIGIO, POLLOUT);
}
return read_count;
}
5. 更新 file_operations 结构体
最后,确保设备驱动的文件操作结构体包含了.fasync成员。
static const struct file_operations globalfifo_fops = {
.owner = THIS_MODULE,
// ... 其他方法 (open, read, write, llseek, ioctl, poll)
.release = globalfifo_release,
.fasync = globalfifo_fasync, // 启用异步通知
};
通过以上步骤,一个功能完整的支持异步通知的字符设备驱动就实现了。它结合了等待队列与信号机制,能高效地响应设备状态变化,是Linux系统编程中提升I/O效率的重要手段之一。