GoDm@'s Blog

Linux异步通知

版权信息

warning

本文章为博主原创文章。遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。


Linux 异步通知机制(Asynchronous Notification)是 Linux 设备驱动中一个非常经典且实用的概念。

简单来说,它的核心思想是 “被动接收,而非主动查询”

1. 什么是异步通知机制

想象一下你在钓鱼,这时候你有两种方式知道鱼上钩了:

  1. 轮询(Polling):你每隔 5 秒钟拉起鱼竿看一看有没有鱼。这很累,而且如果你在看书,看书会被不断打断。

  2. 异步通知(Asynchronous):你在鱼竿上挂个铃铛,然后你就安心看书。当鱼上钩拉动鱼竿时,铃铛响了(信号),你再放下书去收杆。

在 Linux 中,异步通知机制是基于 信号(Signal) 实现的。

2. 异步通知的优缺点

2.1. 优点

2.2. 缺点

3. 理解异步通知机制

我们从用户态开始入手,看在使用异步通知时,内核里实际发生了什么。
首先我们需要了解三个核心API:

3.1. fcntl()

它定义在 <linux/fcntl.h>。是用户态用来“控制已打开文件描述符行为”的系统调用。
不负责读写数据,而是用来:

函数原型:

#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */);

fcntl主要是操作 struct file 这个结构体,如下:

用户态 fcntl()
   ↓
sys_fcntl()
   ↓
VFS
   ↓
struct file
 ├─ f_flags   ← O_NONBLOCK / O_ASYNC
 ├─ f_owner   ← SIGIO 接收者
 └─ f_op

四大类用法:

1. F_GETFL / F_SETFL

🟢作用:读取 / 修改 file->f_flags

🟢常见标志位:

标志 作用
O_NONBLOCK 非阻塞 I/O
O_ASYNC 启用异步通知(SIGIO)
O_APPEND 追加写
O_DIRECT 直接 I/O

🟢用法:

int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_NONBLOCK | O_ASYNC);

不能直接 F_SETFL 一个新值,否则你会把原有 flags 覆盖掉。

2. F_SETOWN / F_GETOWN
👉 异步通知必须用

🟢作用:指定信号发给谁 修改 file->f_owner.pid = pid

🟢用法:

fcntl(fd, F_SETOWN, getpid());

3. F_DUPFD / F_DUPFD_CLOEXEC

🟢作用:复制文件描述符

🟢用法:

int newfd = fcntl(fd, F_DUPFD, 0);

4. 文件锁

用于防止多个进程同时操作资源。现在不深入。

3.2. fasync_helper()

这是在驱动层使用的。fasync_helper() 用来维护“需要被异步通知的进程列表”。
它做的事只有一个核心目标——
哪个进程打开了设备并设置了 O_ASYNC,就把那个进程记下来。

函数原型:

int fasync_helper(int fd,
                  struct file *file,
                  int on,
                  struct fasync_struct **fapp);

函数逻辑伪代码:

if (on) {
    // 开启异步
    if (file 不在 fapp 链表中) {
        分配 fasync_struct
        设置 file / f_owner
        插入 fapp 链表
    }
} else {
    // 关闭异步
    if (file 在 fapp 链表中) {
        从链表删除
        释放 fasync_struct
    }
}

3.3. kill_fasync()

在驱动层使用。向应用程序发出信号的核心函数。

原型:

void kill_fasync(struct fasync_struct **fp, int sig, int band)

3.4. 工作流程

  1. 在用户态打开设备:
int fd = open("/dev/key", O_RDWR);

此步在 VFS 中创建 struct file,没启用异步。

  1. 用户态登记进程PID:
fcntl(fd, F_SETOWN, getpid());

内核找到 struct file,把 file->f_owner.pid 设置为当前进程 PID,以后这个 file 触发信号,就发给这个进程。

  1. 用户态请求开启异步通知:
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_ASYNC);

此步会触发驱动的 fasync 回调函数。

  1. 驱动必须实现 fasync 这个fop:
static int key_fasync(int fd, struct file *file, int on)
{
    struct key_dev *kdev = file->private_data;
    return fasync_helper(fd, file, on, &kdev->async_queue);
}

fasync_helper:把当前进程封装成 struct fasync_struct 并加入 kdev->async_queue

just like this:

key_dev -> 设备资源结构体
 └── async_queue
      ├── 进程A (PID xxx)
      ├── 进程B
  1. 事件发生:
    例如,按键中断触发
irq_handler()
{
    key_value = 1;
    kill_fasync(&kdev->async_queue, SIGIO, POLL_IN);
}

kill_fasync:通知 async_queue 里的所有进程:有 I/O 事件发生了。

just like this:

kill_fasync
 └── 遍历 async_queue
      └── send_sigio()
           └── send_sig_info()
                └── 把 SIGIO 放入目标进程的 signal queue

注意:此函数只是把只是把信号“挂”到目标进程上。

  1. 用户态必须注册信号处理函数:
signal(SIGIO, sigio_handler);

当内核发现:

current->pending signal == SIGIO


→ 保存当前上下文
→ 跳转到 sigio_handler()

4. 使用异步机制

4.1. 应用程序端 (User Space)

应用程序需要做三件事(简称“三板斧”):

  1. 绑定信号处理函数:使用 signal() 让应用知道收到 SIGIO 信号后该干什么。
  2. 设置属主:使用 fcntl(fd, F_SETOWN, getpid()) 告诉内核:“这个设备文件的信号要发给当前进程”。
  3. 开启异步标志:使用 fcntl(fd, F_SETFL, flags | O_ASYNC) 启用异步通知功能。

4.2. 驱动程序端 (Kernel Space)

驱动需要处理数据结构 fasync_struct 并实现三个部分:

  1. 定义结构体:在设备结构体中定义 struct fasync_struct *async_queue;
  2. 实现 .fasync 接口:当应用调用 fcntl 设置 O_ASYNC 时,内核会调用驱动的 .fasync 函数。驱动里只需调用内核辅助函数 fasync_helper
  3. 发送信号:当设备数据就绪(如中断来了),调用 kill_fasync 发送信号。
  4. 清理:在 .release 函数中把节点从异步队列中删除。

4.3. 番外:嵌入还是指针?

在设备资源结构体中,
struct fasync_struct async_queue 用嵌入还是指针?

用指针。因为它是“外部管理对象”,你只是引用。它由 fasync_helper 管理。

—END—


共计约1.8k字。于2025/12/19首次发布,最后更新于2025/12/20。

本文章为博主原创文章。遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

  1. 1. 什么是异步通知机制
  2. 2. 异步通知的优缺点
    1. 2.1. 优点
    2. 2.2. 缺点
  3. 3. 理解异步通知机制
    1. 3.1. fcntl()
    2. 3.2. fasync_helper()
    3. 3.3. kill_fasync()
    4. 3.4. 工作流程
  4. 4. 使用异步机制
    1. 4.1. 应用程序端 (User Space)
    2. 4.2. 驱动程序端 (Kernel Space)
    3. 4.3. 番外:嵌入还是指针?