Linux驱动开发--异步通知与异步I/O

3、异步通知与异步I/O

3.1 Linux信号

阻塞与非阻塞访问、poll()函数提供了较好的解决设备访问的机制,但是如果有了异步通知,整套机制则更加完整了。

异步通知的意思是:一旦设备就绪,则主动通知应用程序,这样应用程序根本就不需要查询设备状态,这一点非常类似于硬件上"中断"的概念,比较准确的称谓是"信号驱动的异步I/O"。信号是在软件层次上对中断机制的一种模拟 ,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的信号是异步的 ,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。

阻塞I/O意味着一直等待设备可访问后再访问,非阻塞I/O中使用poll()意味着查询设备是否可访问,而异步通知则意味着设备通知用户自身可访问,之后用户再进行I/O处理。由此可见,这几种I/O方式可以相互补充。

图9.1呈现了阻塞I/O、结合轮询的非阻塞I/O及基于SIGIO的异步通知在时间先后顺序上的不同。

这里要强调的是:阻塞、非阻塞I/O、异步通知本身没有优劣,应该根据不同的应用场景合理选择。

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

c 复制代码
#define SIGHUP 		1 /* 终端挂起或控制进程终止 */
#define SIGINT 		2 /* 终端中断(Ctrl+C 组合键) */
#define SIGQUIT 	3 /* 终端退出(Ctrl+\组合键) */
#define SIGILL 		4 /* 非法指令 */
#define SIGTRAP 	5 /* debug 使用,有断点指令产生 */
#define SIGABRT 	6 /* 由 abort(3)发出的退出指令 */
#define SIGIOT 		6 /* IOT 指令 */
#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 /* 闹钟 */
#define SIGTERM 	15 /* 软件终止 */
#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 /* 可以进行输入/输出操作 */
#define SIGPOLL 	SIGIO
/* #define SIGLOS 29 */
#define SIGPWR 		30 /* 断点重启 */
#define SIGSYS 		31 /* 非法的系统调用 */
#define SIGUNUSED 	31 /* 未使用信号 */


/* These should not be considered constants from userland.  */
#define SIGRTMIN	32
#define SIGRTMAX	(_NSIG-1)

除了 SIGKILL(9)和 SIGSTOP(19)这两个信号不能被忽略外,进程能够忽略或捕获其他的全部信号。一个信号被捕获的意思是当一个信号到达时有相应的代码处理 它。如果一个信号没有被这个进程所捕获,内核将采用默认行为处理。

3.2 信号的接收--应用端

我们使用中断的时候需要设置中断处理函数,同样的,如果要在应用程序中使用信号,那么就必须设置信号所使用的信号处理函数,在应用程序中使用 signal 函数来设置指定信号的处理函数, signal 函数原型如下所示:

c 复制代码
sighandler_t signal(int signum, sighandler_t handler)

-signum:要设置处理函数的信号。
-handler: 信号的处理函数。
    若为SIGIGN,表示忽略该信号;
	若为SIGDFL,表示采用系统默认方式处理信号;
	若为用户自定义的函数,则信号被捕获到后,该函数将被执行。
-返回值: 设置成功的话返回信号的前一个处理函数handler,设置失败的话返回 SIG_ERR。

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

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

先来看一个使用信号实现异步通知的例子,它通过signal(SIGIO,input_handler)对标准输入文件描述符STDIN_FILENO启动信号机制。用户输人后,应用程序将接收到SIGIO信号其处理函数input_handler()将被调用,如代码清单 9.2所示。

c 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#define MAX_LEN 100

void input_handler(int num)
{
    char data[MAX_LEN];
    int len;

    /* 读取并输出 STDIN_FILENO 上的输入 */
    len = read(STDIN_FILENO, &data, MAX_LEN);
    data[len] = 0;
    printf("input available:%s\n", data);
}

int main(void)
{
    int oflags;
    /* 启动信号驱动机制 */
    signal(SIGIO, input_handler);
    fcntl(STDIN_FILENO, F_SETOWN, getpid());
    oflags = fcntl(STDIN_FILENO, F_GETFL);
    fcntl(STDIN_FILENO, F_SETFL, oflags | FASYNC);

    /* 最后进入一个死循环,仅为保持进程不终止。如果程序中没有这个死循环会立即执行完毕 */
    while (1);
}

上述代码 24 行为 SIGIO 信号安装 input_handler() 作为处理函数,第 25 行设置本进程为 STDIN_FILENO 文件的拥有者,没有这一步,内核不会知道应该将信号发给哪个进程。而为了启用异步通知机制,还需对设备设置 FASYNC 标志,第 26 行、27 行代码可实现此目的。

整个程序的执行效果如下:

复制代码
[root@localhost driver_study1# ./signal_test
I am Chinese.
input available: I am Chinese.
-> signal_test 程序打印

I love Linux driver.
input available: I love Linux driver.
-> signal_test 程序打印

从中可以看出,当用户输入一串字符串后,标准输入设备释放 SIGIO 信号,这个信号"中断"与驱使对应的应用程序中的 input_handler() 得以执行,并将用户输入显示出来。

**由此可见,为了能在用户空间中处理一个设备释放的信号,它必须完成 3 项工作。**应用程序对异步通知的处理包括以下三步:
1、注册信号处理函数

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

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

使用 fcntl(fd, F_SETOWN, getpid())将本应用程序的进程号告诉给内核。

3、开启异步通知

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

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

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

3.3 信号的释放--设备驱动端

在设备驱动和应用程序的异步通知交互中,仅仅在应用程序端捕获信号是不够的,因为信号的源头在设备驱动端 。因此,应该在合适的时机让设备驱动释放信号,在设备驱动程序中增加信号释放的相关代码。

为了使设备支持异步通知机制,驱动程序中涉及3项工作。

1)支持 F_SETOWN 命令:

  • 能在这个控制命令处理中设置 filp->f_owner 为对应进程 ID。不过此项工作已由内核完成,设备驱动无须处理。

2)支持 F_SETFL 命令的处理:

  • 每当 FASYNC 标志改变时,驱动程序中的 fasync() 函数将得以执行。因此,驱动中应该实现 fasync() 函数。

3)在设备资源可获得时,调用 kill_fasync() 函数激发相应的信号。

驱动中的上述3项工作和应用程序中的3项工作是一一对应的:

设备驱动中异步通知编程比较简单,主要用到一项数据结构和两个函数。

数据结构是 fasync_struct 结构体:

c 复制代码
struct fasync_struct {
    spinlock_t fa_lock;       		// 保护结构体的自旋锁
    int magic;               		// 用于验证结构体的魔术数字
    int fa_fd;              	 	// 文件描述符
    struct fasync_struct *fa_next; 	// 指向下一个结构体的指针,形成单向链表
    struct file *fa_file;     		// 指向文件结构体的指针
    struct rcu_head fa_rcu;   		// RCU机制使用的头结构
};

和其他的设备驱动一样,fasync_struct 结构体指针放在设备结构体中仍然是最佳选择,支持异步通知的设备结构体模板,如下所示:

c 复制代码
struct xxx_dev {
    struct cdev cdev; /* cdev 结构体 */
    ...
    struct fasync_struct *async_queue; /* 异步结构体指针 */
};
  • 当一个进程对某个文件调用 fcntl() 系统调用并设置 FASYNC 标志时,内核会创建一个 fasync_struct 实例,并将其加入到文件的异步通知队列中。
  • 当文件状态发生变化时(如数据可读或可写),内核会遍历这个队列----上图中的fasync_struct列表,找到相关的 fasync_struct,并通过文件描述符通知对应的进程。

引出了**"异步通知队列"**:接下来再进一步分析

在 Linux 内核中,异步通知队列(由 fasync_struct 结构体组成的链表)是通过设备驱动程序的私有数据结构 来维护的(也就是前面的struct fasync_struct *async_queue; /* 异步结构体指针 */

如何维护异步通知队列

  1. 私有数据结构 :设备驱动程序通常会在其私有数据结构中维护一个指向 fasync_struct 的指针。这个私有数据结构可能是 struct inode 的一部分,或者是设备驱动程序定义的其他结构体。
  2. fasync_helper 函数 :当需要处理异步通知时,内核会调用 fasync_helper 函数。这个函数会检查设备驱动程序的私有数据结构中是否有指向 fasync_struct 的指针,并据此决定是否需要创建或修改异步通知队列。
  3. kill_fasync 函数 :当设备驱动程序需要通知应用程序文件状态发生变化时(如数据可读或可写),它会调用 kill_fasync 函数。这个函数会遍历由 fasync_struct 结构体组成的链表,并向每个注册的进程发送信号。

两个函数分别是:

1)处理 FASYNC 标志变更的函数。

c 复制代码
int (*fasync) (int fd, struct file *filp, int on);
int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **fa);

2)释放信号用的函数。

c 复制代码
void kill_fasync(struct fasync_struct **fa, int sig, int band);

如果要使用异步通知,需要在设备驱动中实现 file_operations 操作集中的 fasync 函数,fasync 函数里面一般通过调用 fasync_helper 函数来初始化前面定义的 fasync_struct 结构体指针

当应用程序通过"fcntl(fd, F_SETFL, flags | FASYNC)"改变fasync 标记的时候,驱动程序 file_operations 操作集中的 fasync 函数就会执行。 在设备驱动的 fasync() 函数中,只需要简单地将该函数的3个参数以及 fasync_struct 结构体指针的指针作为第4个参数传入 fasync_helper() 函数即可。下面给出了支持异步通知的设备驱动程序 fasync() 函数的模板。

c 复制代码
struct xxx_dev {
    ......
    struct fasync_struct *async_queue; /* 异步相关结构体 */
};

static int xxx_fasync(int fd, struct file *filp, int on)
{
    struct xxx_dev *dev = (xxx_dev *)filp->private_data;

    if (fasync_helper(fd, filp, on, &dev->async_queue) < 0)
        return -EIO;
    return 0;
}

static struct file_operations xxx_ops = {
    ......
    .fasync = xxx_fasync,
    ......
};

在设备资源可以获得时,应该调用 kill_fasync() 释放 SIGIO 信号。在可读时,第3个参数设置为 POLL_IN,在可写时,第3个参数设置为 POLL_OUT。下面给出了释放信号的范例。

c 复制代码
static ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
    struct xxx_dev *dev = filp->private_data;
    ...
    /* 产生异步读信号 */
    if (dev->async_queue)
        kill_fasync(&dev->async_queue, SIGIO, POLL_IN);
    ...
}

最后,在文件关闭时,即在设备驱动的 release() 函数中,应调用设备驱动的 fasync() 函数将文件从异步通知的列表中删除。给出了支持异步通知的设备驱动 release() 函数的模板。

c 复制代码
static int xxx_release(struct inode *inode, struct file *filp)
{
    /* 将文件从异步通知列表中删除 */
    xxx_fasync(-1, filp, 0);
    ...
    return 0;
}

调用前面代码中的 xxx_fasync 函数来完成 fasync_struct 的释放工作,但是,其最终还是通过 fasync_helper 函数完成释放工作。

相关推荐
大刘讲IT3 分钟前
数据治理体系的“三驾马车”:质量、安全与价值挖掘
大数据·运维·经验分享·学习·安全·制造·零售
SuperW1 小时前
Linux学习——UDP
linux·学习·udp
szxinmai主板定制专家1 小时前
国产RK3568+FPGA以 ‌“实时控制+高精度采集+灵活扩展”‌ 为核心的解决方案
大数据·运维·网络·人工智能·fpga开发·机器人
xixingzhe22 小时前
docker转移镜像
运维·docker·容器
SuperW2 小时前
Linux学习——IO多路复用知识
linux·服务器·学习
CopyLower3 小时前
Spring Boot的优点:赋能现代Java开发的利器
java·linux·spring boot
终身学习基地3 小时前
第七篇:linux之基本权限、进程管理、系统服务
linux·运维·服务器
安顾里3 小时前
LInux平均负载
linux·服务器·php
unlockjy3 小时前
Linux——进程优先级/切换/调度
linux·运维·服务器