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 所支持的所有信号,这些信号如下所示:
#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 函数原型如下所示:
sighandler_t signal(int signum, sighandler_t handler)-signum:要设置处理函数的信号。
-handler: 信号的处理函数。若为SIGIGN,表示忽略该信号;若为SIGDFL,表示采用系统默认方式处理信号;若为用户自定义的函数,则信号被捕获到后,该函数将被执行。
-返回值: 设置成功的话返回信号的前一个处理函数handler,设置失败的话返回 SIG_ERR。
信号处理函数原型如下所示:
typedef void (*sighandler_t)(int)
先来看一个使用信号实现异步通知的例子,它通过signal(SIGIO,input_handler)对标准输入文件描述符STDIN_FILENO启动信号机制。用户输人后,应用程序将接收到SIGIO信号其处理函数input_handler()将被调用,如代码清单 9.2所示。
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#define MAX_LEN 100void 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、开启异步通知
使用如下两行程序开启异步通知:
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
结构体:
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
结构体指针放在设备结构体中仍然是最佳选择,支持异步通知的设备结构体模板,如下所示:
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; /* 异步结构体指针 */
)如何维护异步通知队列
- 私有数据结构:设备驱动程序通常会在其私有数据结构中维护一个指向
fasync_struct
的指针。这个私有数据结构可能是struct inode
的一部分,或者是设备驱动程序定义的其他结构体。fasync_helper
函数:当需要处理异步通知时,内核会调用fasync_helper
函数。这个函数会检查设备驱动程序的私有数据结构中是否有指向fasync_struct
的指针,并据此决定是否需要创建或修改异步通知队列。kill_fasync
函数:当设备驱动程序需要通知应用程序文件状态发生变化时(如数据可读或可写),它会调用kill_fasync
函数。这个函数会遍历由fasync_struct
结构体组成的链表,并向每个注册的进程发送信号。
两个函数分别是:
1)处理 FASYNC 标志变更的函数。
int (*fasync) (int fd, struct file *filp, int on);
int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **fa);
2)释放信号用的函数。
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()
函数的模板。
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。下面给出了释放信号的范例。
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() 函数的模板。
static int xxx_release(struct inode *inode, struct file *filp)
{/* 将文件从异步通知列表中删除 */xxx_fasync(-1, filp, 0);...return 0;
}
调用前面代码中的
xxx_fasync
函数来完成fasync_struct
的释放工作,但是,其最终还是通过fasync_helper
函数完成释放工作。