9.4 Linux异步I/O
9.4.1 AIO概念与GNU C库AIO
Linux中最常用的输入/输出(I/O)模型是同步I/O。在同步IO中,当请求发出之后,应用程序就会阻塞,直到请求满足为止。这是一种很好的解决方案,调用应用程序在等待I/O请求完成时不需要占用CPU。但是在许多应用场景中,I/O请求可能需要与CPU消耗产生交叠,以充分利用CPU和I/O提高吞吐率。
图9.3描绘了异步I/O的时序,应用程序发起I/O动作后,直接开始执行,并不等待I/O结束,它要么过一段时间来查询之前的I/O请求完成情况,要么I/O请求完成了会自动被调用与I/O完成绑定的回调函数。
图9.3 异步I/O的时序
Linux的AIO有多种实现,其中一种实现是在用户空间的glibc库中实现的,它本质上是借用了多线程模型,用开启新的线程以同步的方法来做I/O,新的AIO辅助线程与发起AIO的线程以pthread_cond_signal()的形式进行线程间的同步。
glibc的AIO主要包括如下函数。
#include
1.aio_read()
aio_read()函数请求对一个有效的文件描述符进行异步读操作。这个文件描述符可以表示一个文件、套接字,甚至管道。
aio_read函数的原型:
int aio_read(struct aiocb *aiocbp);
aio_read()函数在请求进行排队之后会立即返回(尽管读操作并未完成)。如果执行成功,返回值就为0;如果出现错误,返回值就为-1,并设置errno的值。
参数aiocb(AIO I/O Control Block)结构体包含了传输的所有信息,以及为AIO操作准备的用户空间缓冲区。在产生I/O完成通知时,aiocb结构就被用来唯一标识所完成的I/O操作。
2.aio_write()
aio_write()函数用来请求一个异步写操作。其函数原型:
int aio_write(struct aiocb *aiocbp);
aio_write()函数会立即返回,并且它的请求已经被排队(成功时返回值为0,失败时返回值为-1,并相应地设置errno)。
3.aio_error()
aio_error()函数被用来确定请求的状态。其原型:
int aio_error( struct aiocb *aiocbp );
这个函数可以返回以下内容。
EINPROGRESS:说明请求尚未完成。
ECANCELED:说明请求被应用程序取消了。
-1:说明发生了错误,具体错误原因由errno记录。
4.aio_return()
异步I/O和同步阻塞I/O方式之间的一个区别是不能立即访问这个函数的返回状态,因为异步I/O并没有阻塞在read()调用上。在标准的同步阻塞read()调用中,返回状态是在该函数返回时提供的。但是在异步I/O中,要使用aio_return()函数。这个函数的原型:
ssize_t aio_return(struct aiocb *aiocbp);
只有在aio_error()调用确定请求已经完成(可能成功,也可能发生了错误)之后,才会调用这个函数。aio_return()的返回值就等价于同步情况中read()或write()系统调用的返回值(所传输的字节数如果发生错误,返回值为负数)。
代码清单9.12 用户空间异步读例程
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define BUFSIZE 4096
// gcc one.c -o test -lrt
int main(int argc, char *argv[])
{
int fd, ret;
struct aiocb aiocb;
fd = open("file.txt", O_RDONLY);
if (fd < 0) {
perror("open");
_exit(1);
}
/* 清零aiocb结构体 */
bzero(&aiocb, sizeof(struct aiocb));
/* 为aiocb请求分配数据缓冲区 */
aiocb.aio_buf = malloc(BUFSIZE + 1);
if (!aiocb.aio_buf) {
perror("malloc");
_exit(1);
}
/* 初始化aiocb的成员 */
aiocb.aio_fildes = fd; // file desriptor
aiocb.aio_nbytes = BUFSIZE; // length of transfer
aiocb.aio_offset = 0; // 偏移量
ret = aio_read(&aiocb); // 异步读请求操作
if (ret < 0) {
perror("aio_read");
_exit(1);
}
while (aio_error(&aiocb) == EINPROGRESS) // 确定请求的状态
continue;
if ((ret = aio_return(&aiocb)) > 0) {
/* 获得异步读的返回值 */
printf("%s\n", aiocb.aio_buf);
} else {
/* 读失败,分析errorno */
printf("%d\n", aiocb.__error_code);
}
return 0;
}
5.aio_suspend()
用户可以使用aio_suspend()函数来阻塞调用进程,直到异步请求完成为止。调用者提供了一个aiocb引用列表,其中任何一个完成都会导致aio_suspend()返回。aio_suspend()的函数原型:
int aio_suspend(const struct aiocb * const aiocb_list[], int nitems, const struct timespec *timeout);
代码清单9.13 用户空间异步I/O aio_suspend()函数使用例程
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define BUFSIZE 4096
#define MAX_LIST 3
// gcc two.c -o test -lrt
int main(int argc, char *argv[])
{
int fd, ret;
struct aiocb aiocb;
struct aioct *cblist[MAX_LIST];
fd = open("file.txt", O_RDONLY);
if (fd < 0) {
perror("open");
_exit(1);
}
/* 清零aiocb结构体 */
bzero(&aiocb, sizeof(struct aiocb));
/* 清零aioct结构体链表 */
bzero((char *)cblist, sizeof(cblist) );
/* 为aiocb请求分配数据缓冲区 */
aiocb.aio_buf = malloc(BUFSIZE + 1);
if (!aiocb.aio_buf) {
perror("malloc");
_exit(1);
}
/* 初始化aiocb的成员 */
aiocb.aio_fildes = fd; // file desriptor
aiocb.aio_nbytes = BUFSIZE; // length of transfer
aiocb.aio_offset = 0; // 偏移量
cblist[0] = &aiocb;
ret = aio_read(&aiocb); // 异步读请求操作
if (ret < 0) {
perror("aio_read");
_exit(1);
}
ret = aio_suspend(cblist, MAX_LIST, NULL);// 阻塞调用进程,直到异步请求完成为止
if (ret < 0) {
perror("aio_suspend");
_exit(1);
}
while (aio_error(&aiocb) == EINPROGRESS) // 请求的状态:请求尚未完成
continue;
if ((ret = aio_return(&aiocb)) > 0) {
/* 获得异步读的返回值 */
printf("%s\n", aiocb.aio_buf);
} else {
/* 读失败,分析errorno */
printf("%d\n", aiocb.__error_code);
}
return 0;
}
在glibc实现的AIO中,除了同步的等待方式以外,也可以使用信号或者回调机制来异步地标明异步IO(AIO)的完成。
6.aio_cancel()
允许用户取消对某个文件描述符执行的一个或所有I/O请求。其原型:
int aio_cancel(int fd, struct aiocb *aiocbp);
要取消一个请求,用户需提供文件描述符和aiocb指针。如果这个请求被成功取消了,那么这个函数就会返回AIO_CANCELED。如果请求完成了,这个函数就会返回AIO_NOTCANCELED。
要取消对某个给定文件描述符的所有请求,用户需要提供这个文件的描述符,并将aiocbp参数设置为NULL。如果所有的请求都取消了,这个函数就会返回AIO_CANCELED;如果至少有一个请求没有被取消,那么这个函数就会返AIO_NOT_CANCELED;如果没有一个请求可以被取消,那么这个函数就会返回AIO_ALLDONE。可以使用aio_error()来验证每个AIO请求,如果某请求已经被取消了,那么aio_error()就会返回-1,并且errno会被设置为ECANCELED。
7.lio_listio()
该函数可用于同时发起多个传输。它使得用户可以在一个系统调用中启动大量的I/O操作。
lio_listio API函数的原型如下:
int lio_listio(int mode, struct aiocb *const aiocb_list[],int nitems, struct sigevent *sevp);
代码清单9.14 用户空间异步I/O lio_listio()函数使用例程
#define MAX_LIST 4
#define BUFSIZE 4096
struct aiocb aiocb1, aiocb2;
struct aiocb *list[MAX_LIST];
...
/* 准备第一个aiocb */
aiocb1.aio_fildes = fd;
aiocb1.aio_buf = malloc( BUFSIZE+1 );
aiocb1.aio_nbytes = BUFSIZE;
aiocb1.aio_offset = next_offset;
aiocb1.aio_lio_opcode = LIO_READ; /* 异步读操作*/
...
/* 准备多个aiocb */
bzero( (char *)list, sizeof(list) );...
在上述代码中,因为是进行异步读操作,所以操作码为LIO_READ,对于写操作来说,应该使用LIO_WRITE作为操作码,而LIO_NOP意味着空操作。
9.4.2 Linux内核AIO与libaio
Linux AIO也可以由内核空间实现,异步I/O是Linux 2.6以后版本内核的一个标准特性。对于块设备,AIO可以一次性发出大量的read/write调用并且通过通用块层的I/O调度来获得更好的性能,用户程序也可以减少过多的同步负载,还可以在业务逻辑中更灵活地进行并发控制和负载均衡。相较于glibc的用户空间多线程同步等实现也减少了线程的负载和上下文切换等。对于网络设备,在socket层面上,也可以使用AIO,让CPU和网卡的收发动作充分交叠以改善吞吐性能。选择正确的I/O模型对系统性能的影响很
大。
在用户空间中,一般要结合libaio来进行内核AIO的系统调用。内核AIO提供的系统调用主要包括:
int io_setup(int maxevents, io_context_t *ctxp);
int io_destroy(io_context_t ctx);
int io_submit(io_context_t ctx, long nr, struct iocb *ios[]);
int io_cancel(io_context_t ctx, struct iocb *iocb, struct io_event *evt);
int io_getevents(io_context_t ctx_id, long min_nr, long nr, struct io_event *events,
struct timespec *timeout);
void io_set_callback(struct iocb *iocb, io_callback_t cb);
void io_prep_pwrite(struct iocb *iocb, int fd, void *buf, size_t count, long long offset);
void io_prep_pread(struct iocb *iocb, int fd, void *buf, size_t count, long long offset);
void io_prep_pwritev(struct iocb *iocb, int fd, const struct iovec *iov, int iovcnt,
long long offset);
void io_prep_preadv(struct iocb *iocb, int fd, const struct iovec *iov, int iovcnt,
long long offset);
代码清单9.15 使用libaio调用内核AIO的范例
//gcc aior.c -o aior –laio
// 安装libaio包 sudo apt-get install libaio-dev
#define _GNU_SOURCE /* O_DIRECT is not POSIX */memset(buf, 0, BUF_SIZE + 1);
ret = io_setup(128, &ctx); // setup编译源代码文件:gcc aio_read.c -o test -laio
执行:./test file.txt
结果:打印该文本文件前4096个字节的内容
9.4.3 AIO与设备驱动
用户空间调用io_submit()后,对应于用户传递的每一个iocb结构,内核会生成一个与之对应的kiocb结构。位于
struct kiocb {
struct file *ki_filp;
struct kioctx *ki_ctx; /* NULL for sync ops */
kiocb_cancel_fn *ki_cancel;
void *private;
union {
void __user *user;
struct task_struct *tsk;
} ki_obj;
__u64 ki_user_data; /* user's data for completion */
loff_t ki_pos;
size_t ki_nbytes; /* copy of iocb->aio_nbytes */
struct list_head ki_list; /* the aio core uses this
* for cancellation */
/*
* If the aio_resfd field of the userspace iocb is not zero,
* this is the underlying eventfd context to deliver events to.
*/
struct eventfd_ctx *ki_eventfd;
};
file_operations包含3个与AIO相关的成员函数:
ssize_t (*aio_read) (struct kiocb *iocb, const struct iovec *iov, unsigned long nr_segs, loff_t pos);
ssize_t (*aio_write) (struct kiocb *iocb, const struct iovec *iov, unsigned long nr_segs, loff_t pos);
int (*aio_fsync) (struct kiocb *iocb, int datasync);
io_submit()/*下发/提交io请求*/系统调用间接引起了file_operations中的aio_read()和aio_write()的调用。
AIO一般由内核空间的通用代码处理,对于块设备和网络设备而言,一般在Linux核心层的代码已经解决。字符设备驱动一般不需要实现AIO支持。
9.5 总结
Linux中的异步I/O,异步I/O使得应用程序在等待I/O操作的同时进行其他操作。
使用信号可以实现设备驱动与用户程序之间的异步通知,设备驱动和用户空间要分别完成3项对应的工作,用户空间设置文件的拥有者、FASYNC标志及捕获信号,内核空间响应对文件的拥有者、FASYNC标志的设置并在资源可获得时释放信号。
Linux 2.6以后的内核包含对AIO的支持,它为用户空间提供了统一的异步I/O接口。glibc也提供了一个不依赖于内核的用户空间的AIO支持。