imx6ull-驱动开发篇31——Linux异步通知

目录

异步通知

异步通知​概念​​

信号

[signal 函数](#signal 函数)

驱动中的信号处理

[fasync_struct 结构体](#fasync_struct 结构体)

[fasync 函数](#fasync 函数)

[kill_fasync 函数](#kill_fasync 函数)

应用程序对异步通知的处理


异步通知

Linux 应用程序可以通过阻塞或者非阻塞这两种方式来访问驱动设备:

  • 通过阻塞方式访问的话,应用程序会处于休眠态,等待驱动设备可以使用,
  • 非阻塞方式,会通过 poll 函数来不断的轮询,查看驱动设备文件是否可以使用。

这两种方式都需要应用程序主动地去查询设备的使用情况,这就需要用到异步通知,也就是驱动可以通过主动向应用程序发送信号

异步通知​概念​

  • 进程​​先发起I/O请求​ ​,内核在操作完成后​​主动通知进程​​(通过信号或回调),期间进程无需阻塞或轮询。

  • ​异步机制​​:操作结果通过回调/信号返回。

维度​ ​说明​
​通知方式​ 信号(SIGIO)、回调函数(aio_completion)或事件fd(eventfd
​设置函数​ fcntl(fd, F_SETOWN, pid)+ fcntl(fd, F_SETFL, O_ASYNC)
​优点​ 资源利用率最高,适合高吞吐场景
​缺点​ 编程复杂度高,需处理信号竞争
​适用场景​ 高性能服务器(如Nginx)、磁盘I/O密集型应用

阻塞、非阻塞、异步通知,这三种是针对不同的场合提出来的不同的解决方法,在实际的工作和学习中,根据自己的实际需求选择合适的处理方法即可。

信号

异步通知的核心就是信号,在 arch/xtensa/include/uapi/asm/signal.h 文件中定义了 Linux 所支持的所有信号,这些信号如下所示:

cpp 复制代码
/* 信号宏定义列表(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 函数原型如下所示:

cpp 复制代码
sighandler_t signal(int signum, sighandler_t handler)
  • signum:要设置处理函数的信号。
  • handler: 信号的处理函数。
  • 返回值: 设置成功的话返回信号的前一个处理函数,设置失败的话返回 SIG_ERR。

信号处理函数原型如下所示:

cpp 复制代码
typedef void (*sighandler_t)(int)

使用这两个函数的示例代码如下:

cpp 复制代码
#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 结构体内容如下:

cpp 复制代码
/**
 * 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 结构体指针变量,添加定义到设备结构体中:

cpp 复制代码
struct fasync_struct *async_queue; /* 异步相关结构体 */

fasync 函数

要使用异步通知,需要在设备驱动中实现 file_operations 操作集中的 fasync 函数,此函数格式如下所示:

cpp 复制代码
int (*fasync) (int fd, struct file *filp, int on)

fasync 函数里面一般通过调用 fasync_helper 函数来初始化前面定义的 fasync_struct 结构体指针。

fasync_helper 函数原型如下:

cpp 复制代码
/**
 * 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

示例代码如下:

cpp 复制代码
#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 函数原型如下所示:

cpp 复制代码
void kill_fasync(struct fasync_struct **fp, int sig, int band)

函数参数和返回值含义如下:

  • fp:要操作的 fasync_struct。
  • sig: 要发送的信号。
  • band: 可读时设置为 POLL_IN,可写时设置为 POLL_OUT。

应用程序对异步通知的处理

应用程序对异步通知的处理包括以下三步:

  • 注册信号处理函数
  • 将应用程序的进程号告诉给内核
  • 开启异步通知

1、注册信号处理函数

应用程序根据驱动程序所使用的信号来设置信号的处理函数,应用程序使用 signal 函数来设置信号的处理函数。

2、将应用程序的进程号告诉给内核

使用如下命令将本应用程序的进程号告诉给内核:

cpp 复制代码
fcntl(fd, F_SETOWN, getpid())

3、开启异步通知

使用如下两行程序开启异步通知:

cpp 复制代码
flags = fcntl(fd, F_GETFL); /* 获取当前的进程状态 */
fcntl(fd, F_SETFL, flags | FASYNC); /* 开启当前进程异步通知功能 */

通过 fcntl 函数设置进程状态为 FASYNC,经过这一步,驱动程序中的 fasync 函数就会执行。

用户空间配置流程大概如图:

下一讲实验,我们编写相应的驱动和测试代码。

相关推荐
yuanpan1 小时前
ubuntu系统上的conda虚拟环境导出方便下次安装
linux·ubuntu·conda
AOwhisky2 小时前
Linux 文本处理三剑客:awk、grep、sed 完全指南
linux·运维·服务器·网络·云计算·运维开发
Gavin_9152 小时前
从零开始部署经典开源项目管理系统最新版redmine6-Linux Debian12
linux·ruby on rails·开源·debian·ruby·redmine
shelutai3 小时前
ubuntu 编译ffmpeg6.1 增加drawtext,libx264,libx265等
linux·ubuntu·ffmpeg
runfarther3 小时前
搭建LLaMA-Factory环境
linux·运维·服务器·python·自然语言处理·ai编程·llama-factory
hello_ world.3 小时前
RHCA10NUMA
linux
神秘人X7074 小时前
Linux高效备份:rsync + inotify实时同步
linux·服务器·rsync
轻松Ai享生活4 小时前
一步步学习Linux initrd/initramfs
linux
轻松Ai享生活4 小时前
一步步深入学习Linux Process Scheduling
linux