异步 I/O
在 I/O 多路复用中,进程通过系统调用 select()或 poll()来主动查询文件描述符上是否可以执行 I/O 操作。 而在异步 I/O 中,当文件描述符上可以执行 I/O 操作时,进程可以请求内核为自己发送一个信号。之后进程就可以执行任何其它的任务直到文件描述符可以执行 I/O 操作为止,此时内核会发送信号给进程。所以要使用异步 I/O,还得结合前面所学习的信号相关的内容,所以异步 I/O 通常也称为信号驱动 I/O。
要使用异步 I/O,程序需要按照如下步骤来执行:
- 通过指定 O_NONBLOCK 标志使能非阻塞 I/O。
- 通过指定 O_ASYNC 标志使能异步 I/O。
- 设置异步 I/O 事件的接收进程。也就是当文件描述符上可执行 I/O 操作时会发送信号通知该进程, 通常将调用进程设置为异步 I/O 事件的接收进程。
- 为内核发送的通知信号注册一个信号处理函数。默认情况下,异步 I/O 的通知信号是 SIGIO,所以内核会给进程发送信号 SIGIO。
- 以上步骤完成之后,进程就可以执行其它任务了,当 I/O 操作就绪时,内核会向进程发送一个 SIGIO 信号,当进程接收到信号时,会执行预先注册好的信号处理函数,我们就可以在信号处理函数中进行 I/O 操作。
O_ASYNC 标志
O_ASYNC 标志可用于使能文件描述符的异步 I/O 事件,当文件描述符可执行 I/O 操作时,内核会向异步 I/O 事件的接收进程发送 SIGIO 信号(默认情况下),该标志主要用于异步 I/O。 需要注意的是:在调用 open()时无法通过指定 O_ASYNC 标志来使能异步 I/O,但可以使用 fcntl()函数 添加 O_ASYNC 标志使能异步 I/O,譬如:
int flag;
flag = fcntl(0, F_GETFL); //先获取原来的 flag
flag |= O_ASYNC; //将 O_ASYNC 标志添加到 flag
fcntl(fd, F_SETFL, flag); //重新设置 flag
设置异步 I/O 事件的接收进程
为文件描述符设置异步 I/O 事件的接收进程,也就是设置异步 I/O 的所有者。同样也是通过 fcntl()函数进行设置,操作命令 cmd 设置为 F_SETOWN,第三个参数传入接收进程的进程 ID(PID),通常将调用进程的 PID 传入,譬如:
fcntl(fd, F_SETOWN, getpid());
注册 SIGIO 信号的处理函数
通过 signal()或 sigaction()函数为 SIGIO 信号注册一个信号处理函数,当进程接收到内核发送过来的 SIGIO 信号时,会执行该处理函数,所以我们应该在处理函数当中执行相应的 I/O 操作。
使用示例
代码演示了以异步 I/O 方式读取鼠标,当进程接收到 SIGIO 信号时,执行信号处理函数 sigio_handler(),在该函数中调用 read()读取鼠标数据。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#define MOUSE "/dev/input/event3"
static int fd;
static void sigio_handler(int sig){
static int loops = 5;
char buf[100] = {0};
int ret;
if(SIGIO != sig) {
return ;
}
ret = read(fd, buf,sizeof(buf));
if( 0 < ret)
printf("鼠标:成功读取<%d>个字节数据\n",ret);
loops--;
if(0 >= ret){
close(fd);
exit(0);
}
}
int main (void){
int flag;
/* 打开鼠标设备文件<使能非阻塞 I/O> */
fd = open(MOUSE, O_RDONLY | O_NONBLOCK);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 使能异步 I/O */
flag = fcntl(fd, F_GETFL);
flag |= O_ASYNC;
fcntl(fd, F_SETFL, flag);
/* 设置异步 I/O 的所有者 */
fcntl(fd, F_SETOWN, getpid());
/* 为 SIGIO 信号注册信号处理函数 */
signal(SIGIO, sigio_handler);
for ( ; ; )
sleep(1);
}
编译测试:
优化异步 I/O
上一小节介绍了异步 I/O 的原理以及使用方法,在一个需要同时检查大量文件描述符(譬如数千个)的应用程序中,例如某种类型的网络服务端程序,与 select()和 poll()相比,异步 I/O 能够提供显著的性能优势。 之所以如此,原因在于:对于异步 I/O,内核可以“记住”要检查的文件描述符,且仅当这些文件描述符上 可执行 I/O 操作时,内核才会向应用程序发送信号。
而对于 select()或 poll()函数来说,内部实现原理其实是通过轮训的方式来检查多个文件描述符是否可执行 I/O 操作,所以,当需要检查的文件描述符数量较多时,随之也将会消耗大量的 CPU 资源来实现轮训检查操作。当需要检查的文件描述符并不是很多时,使用 select()或 poll()是一种非常不错的方案!
Tips:当需要检查大量文件描述符时,可以使用 epoll 解决 select()或 poll()性能低的问题。在性能表现上,epoll 与异步 I/O 方式相似, 但是 epoll 有一些胜过异步 I/O 的优点。
不管是异步 I/O、还是 epoll,在需要检查大量文件描述符的应用程序当中,在这种情况下,它们的性能相比于 select()或 poll()有着显著的优势!
本小节将对上一小节所讲述的异步 I/O 进行优化,既然要对其进行优化,那必然存在着一些缺陷,如下所示:
- 默认的异步 I/O 通知信号 SIGIO 是非排队信号。SIGIO 信号是标准信号(非实时信号、不可靠信号),所以它不支持信号排队机制,譬如当前正在执行 SIGIO 信号的处理函数,此时内核又发送多次 SIGIO 信号给进程,这些信号将会被阻塞,只有当信号处理函数执行完毕之后才会传递给进程,并且只能传递一次,而其它后续的信号都会丢失。
- 无法得知文件描述符发生了什么事件。譬如在上文代码信号处理函数 sigio_handler()中,直接调用了 read()函数读取鼠标,而并未判断文件描述符是否处于可读就绪态,事实上,示例代码 中这种异步 I/O 方式并未告知应用程序文件描述符上发生了什么事件,是可读取还是可写入亦或者发生异常等。
所以本小节我们将会针对以上列举出的两个缺陷进行优化。
使用实时信号替换默认信号 SIGIO
SIGIO 作为异步 I/O 通知的默认信号,是一个非实时信号,我们可以设置不使用默认信号,指定一个实时信号作为异步 I/O 通知信号,如何指定呢?同样也是使用 fcntl()函数进行设置,调用函数时将操作命令 cmd 参数设置为 F_SETSIG,第三个参数 arg 指定一个实时信号编号即可,表示将该信号作为异步 I/O 通知信号,譬如:
fcntl(fd, F_SETSIG, SIGRTMIN);
使用 sigaction()函数注册信号处理函数
在应用程序当中需要为实时信号注册信号处理函数,使用 sigaction 函数进行注册,并为 sa_flags 参数指定 SA_SIGINFO,表示使用 sa_sigaction 指向的函数作为信号处理函数,而不使用 sa_handler 指向的函数。 因为 sa_sigaction 指向的函数作为信号处理函数提供了更多的参数,可以获取到更多信息,函数定义参考 Linux信号基础(3)——更泛化的使用方式 中示例代码关于 struct sigaction 结构体的描述。
函数参数中包括一个 siginfo_t 指针,指向 siginfo_t 类型对象,当触发信号时该对象由内核构建。siginfo_t 结构体中提供了很多信息,我们可以在信号处理函数中使用这些信息,就对于异步 I/O 事件而言,传递给信号处理函数的 siginfo_t 结构体中与之相关的字段如下:
- si_signo:引发处理函数被调用的信号。这个值与信号处理函数的第一个参数一致。
- si_fd:表示发生异步 I/O 事件的文件描述符;
- si_code:表示文件描述符 si_fd 发生了什么事件,读就绪态、写就绪态或者是异常事件等。该字段 中可能出现的值以及它们对应的描述信息参见下表。
- si_band:是一个位掩码,其中包含的值与系统调用 poll()中返回的 revents 字段中的值相同。
如表所示,si_code 中可能出现的值与 si_band 中的位掩码有着一一对应关系。
所以,由此可知,可以在信号处理函数中通过对比 siginfo_t 结构体的 si_code 变量来检查文件描述符发生了什么事件,以采取相应的 I/O 操作。
使用示例
通过前文的学习,我们已经知道了如何针对本节开头提出的异步 I/O 存在的两个缺陷进行优化。下示例代码对上文代码进行了优化,使用实时信号+sigaction 解决:默认 异步 I/O 通知信号 SIGIO 可能存在丢失以及信号处理函数中无法判断文件描述符所发生的 I/O 事件这两个问题。
调用 sigaction()注册信号处理函数时,sa_flags 指定了 SA_SIGINFO,所以将使用 sa_sigaction 指向的函数 io_handler 作为信号处理函数,io_handler 共有 3 个参数,参数 sig 等于引发信号处理函数被调用的信号值。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#define MOUSE "/dev/input/event3"
static int fd;
static void io_handler(int sig, siginfo_t*info, void *context){
static int loops = 5;
char buf[100] = {0};
int ret;
if(SIGRTMIN != sig) {
return ;
}
/* 判断鼠标是否可读 */
if (POLL_IN == info->si_code) {
ret = read(fd, buf, sizeof(buf));
if (0 < ret)
printf("鼠标: 成功读取<%d>个字节数据\n", ret);
loops--;
if (0 >= loops) {
close(fd);
exit(0);
}
}
}
int main (void){
struct sigaction act;
int flag;
/* 打开鼠标设备文件<使能非阻塞 I/O> */
fd = open(MOUSE, O_RDONLY | O_NONBLOCK);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 使能异步 I/O */
flag = fcntl(fd, F_GETFL);
flag |= O_ASYNC;
fcntl(fd, F_SETFL, flag);
/* 指定实时信号 SIGRTMIN 作为异步 I/O 通知信号 */
fcntl(fd, F_SETSIG, SIGRTMIN);
/* 为实时信号 SIGRTMIN 注册信号处理函数 */
act.sa_sigaction = io_handler;
act.sa_flags = SA_SIGINFO;
sigemptyset(&act.sa_mask);
sigaction(SIGRTMIN, &act, NULL);
for ( ; ; )
sleep(1);
}
对上述示例代码进行编译时,出现了一些报错信息,如下所示:
报错提示没有定义F_SETSIG,确实如此,我们需要定义了_GNU_SOURCE宏之后才能使用F_SETSIG。
这里笔者选择直接在源文件中使用#define 定义_GNU_SOURCE 宏,如下所示:
#define _GNU_SOURCE //在源文件开头定义_GNU_SOURCE 宏
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
再次进行编译测试即可