4. 设备驱动注册异步通知:实现 fasync 文件操作、kill_fasync 函数调用时机

异步通知机制的核心在于:当设备数据可读、可写或发生异常时,驱动主动向用户空间进程发送 SIGIO 信号,从而避免用户进程轮询或阻塞等待。要实现这一机制,驱动必须完成两个关键步骤:注册异步通知(实现 fasync 文件操作)和 触发信号发送(在合适的时机调用 kill_fasync)。本章将深入剖析这两个步骤的实现细节与调用时机。

4.1 fasync 文件操作:驱动注册的入口

用户空间进程通过 fcntl(fd, F_SETFL, O_ASYNC) 或 fcntl(fd, F_SETOWN, pid) 请求驱动启用异步通知时,内核会调用驱动中注册的 fasync 文件操作。该操作的核心职责是维护一个 异步通知结构体链表(struct fasync_struct),用于记录哪些进程需要接收信号。

4.1.1 fasync 函数原型与标准实现模板

static int my_fasync(int fd, struct file *filp, int mode)
{
    struct my_device *dev = filp->private_data;
    return fasync_helper(fd, filp, mode, &dev->async_queue);
}
参数/返回值 说明
fd 文件描述符,由内核传入
filp 文件结构体指针,指向当前打开的设备文件
mode 操作模式:FASYNC_ON(启用)或 FASYNC_OFF(禁用)
dev->async_queue 设备结构体中保存的 struct fasync_struct * 指针
fasync_helper 内核提供的辅助函数,自动管理链表添加/删除
最佳实践: 驱动开发者应始终使用 fasync_helper 而非手动操作链表。该函数已处理竞态条件和内存管理,能显著降低出错概率。

4.1.2 设备结构体中的异步队列指针

每个需要支持异步通知的设备实例,必须在私有数据结构中保存一个 struct fasync_struct * 指针,并初始化为 NULL:

struct my_device {
    spinlock_t lock;
    wait_queue_head_t read_wait;
    struct fasync_struct *async_queue;  /* 异步通知队列 */
    /* 其他设备成员... */
};

4.1.3 文件操作结构体注册

将 my_fasync 赋值给 struct file_operations 中的 fasync 字段:

static const struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .release = my_release,
    .read    = my_read,
    .write   = my_write,
    .fasync  = my_fasync,   /* 注册异步通知入口 */
};
常见错误: 忘记在 release 操作中调用 my_fasync(-1, filp, 0) 来清理异步队列,会导致进程退出后驱动仍持有悬空指针,引发内核崩溃。

4.2 kill_fasync 函数调用时机:信号触发的关键

kill_fasync 是驱动主动发送 SIGIO 信号的接口。其调用时机直接决定了异步通知的实时性和有效性。错误的调用时机可能导致信号丢失或过度触发。

4.2.1 kill_fasync 函数原型

void kill_fasync(struct fasync_struct **fp, int sig, int band);
参数 含义
fp 指向 struct fasync_struct * 的二级指针(即 &dev->async_queue)
sig 发送的信号编号,通常为 SIGIO
band 事件类型:POLL_IN(可读)、POLL_OUT(可写)、POLL_ERR(错误)

4.2.2 典型调用时机分析

根据设备类型和业务场景,kill_fasync 应在以下时机被调用:

  • 数据到达时:如串口收到新数据、网络卡收到报文、传感器采集完成。
  • 缓冲区状态变化时:从满变为非满(可写),或从空变为非空(可读)。
  • 硬件中断处理函数中:在中断底半部(如 tasklet、workqueue)中调用,避免在中断上下文执行耗时操作。
  • 定时器到期时:用于周期性数据上报或超时事件通知。
核心原则: 调用 kill_fasync 的时机应与 poll 操作中唤醒等待队列的时机保持一致。即:当驱动认为设备状态已变化、可以唤醒等待进程时,也应同时发送异步信号。

4.2.3 中断驱动场景下的调用示例

/* 中断底半部处理函数 */
static void my_device_work_handler(struct work_struct *work)
{
    struct my_device *dev = container_of(work, struct my_device, work);

    spin_lock(&dev->lock);
    dev->data_ready = 1;
    /* 唤醒同步等待的进程 */
    wake_up_interruptible(&dev->read_wait);
    /* 发送异步通知信号 */
    kill_fasync(&dev->async_queue, SIGIO, POLL_IN);
    spin_unlock(&dev->lock);
}

4.3 异步通知注册与触发的完整流程

下图展示了从用户进程注册异步通知到驱动触发信号发送的完整数据流:

异步通知注册与触发流程 用户进程 fcntl(F_SETFL, O_ASYNC) VFS 调用 fasync 操作 my_fasync() fasync_helper() async_queue 链表 硬件中断 / 数据到达 kill_fasync(SIGIO) 注册阶段:用户进程通过 fcntl 触发驱动注册 触发阶段:硬件事件 驱动调用 kill_fasync

4.4 异步通知与 poll 机制的协同

异步通知和 poll 机制是驱动向用户空间报告事件状态的两种互补方式。下表对比了它们的核心差异:

特性 poll / select / epoll 异步通知 (SIGIO)
触发方式 用户进程主动调用,阻塞或轮询 驱动主动发送信号,用户进程被动接收
实时性 取决于调用频率,可能有延迟 事件发生后立即触发,实时性高
资源消耗 轮询模式下消耗 CPU,阻塞模式下无消耗 无轮询开销,但信号处理有上下文切换
适用场景 多路复用、大量文件描述符监控 低延迟、事件驱动型设备(如输入设备)
驱动实现 实现 poll 操作,调用 poll_wait 实现 fasync 操作,调用 kill_fasync
协同策略: 在驱动中,poll 和 fasync 应共享同一个条件判断逻辑。当数据到达时,同时调用 wake_up_interruptible 和 kill_fasync,确保无论用户进程使用哪种机制,都能及时得到通知。

4.5 驱动释放时的清理工作

当设备文件被关闭时,驱动必须在 release 操作中清理异步通知队列,防止内存泄漏和悬空指针:

static int my_release(struct inode *inode, struct file *filp)
{
    struct my_device *dev = filp->private_data;

    /* 将异步队列从内核链表中移除 */
    my_fasync(-1, filp, 0);

    /* 其他清理工作... */
    return 0;
}

调用 my_fasync(-1, filp, 0) 会触发 fasync_helper 将当前文件对应的异步通知条目从链表中删除,并释放相关内存。

4.6 本章小结

  • fasync 文件操作 是驱动注册异步通知的入口,通过 fasync_helper 维护异步队列链表。
  • kill_fasync 是驱动主动发送 SIGIO 信号的接口,应在数据到达、状态变化或中断处理中调用。
  • 异步通知与 poll 机制应共享条件判断逻辑,确保两种通知方式的一致性。
  • 驱动释放时必须清理异步队列,避免资源泄漏。