Linux设备驱动中的异步通知与异步I/O之异步IO

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 <aio.h>

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 <aio.h>
#include <stdio.h>
#include <strings.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>

#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 <aio.h>
#include <stdio.h>
#include <strings.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>

#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) );

/* 将aiocb填入链表*/
list[0] = &aiocb1;
list[1] = &aiocb2;
...
ret = lio_listio( LIO_WAIT, list, MAX_LIST, NULL );  /* 发起大量I/O操作*/

...

在上述代码中,因为是进行异步读操作,所以操作码为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 */
#include <stdio.h>      /* for perror() */
#include <unistd.h>     /* for syscall() */
#include <fcntl.h>      /* O_RDWR */
#include <string.h>     /* memset() */
#include <inttypes.h>   /* uint64_t */
#include <stdlib.h>
#include <libaio.h>

#define BUF_SIZE 4096

int main(int argc, char *argv[])
{
io_context_t ctx = 0;
struct iocb cb;
struct iocb *cbs[1];
unsigned char *buf;

struct io_event events[1];
int ret;
int fd;

if (argc < 2) {
printf("the command format: aior <file>\n");
exit(1);
}

fd = open(argv[1], O_RDWR | O_DIRECT);
if (fd < 0) {
perror("open error");
goto err;
}

/* Allocate aligned memory */
ret = posix_memalign((void **)&buf, 512, (BUF_SIZE + 1));
if (ret < 0) {
perror("posix_memalign failed");
goto err1;
}

memset(buf, 0, BUF_SIZE + 1);

ret = io_setup(128, &ctx); // setup
if (ret < 0) {
printf("io_setup error:%s", strerror(-ret));
goto err2;
}

/* setup I/O control block */
io_prep_pread(&cb, fd, buf, BUF_SIZE, 0); // 生成iocb的结构体

cbs[0] = &cb;
ret = io_submit(ctx, 1, cbs); // submit AIO request
if (ret != 1) {
if (ret < 0) {
            printf("io_submit error:%s", strerror(-ret));
} else {
            fprintf(stderr, "could not sumbit IOs");
}
goto err3;
}

/* get the reply */
ret = io_getevents(ctx, 1, 1, events, NULL); // 等待I/O完成事件
if (ret != 1) {
if (ret < 0) {
printf("io_getevents error:%s", strerror(-ret));
} else {
            fprintf(stderr, "could not get Events");
}
goto err3;
}
if (events[0].res2 == 0) {
printf("%s\n", buf);
} else {
printf("AIO error:%s", strerror(-events[0].res));
goto err3;
}

if ((ret = io_destroy(ctx)) < 0) { // destroy
printf("io_destroy error:%s", strerror(-ret));
goto err2;
}

free(buf);
close(fd);
return 0;

err3:
if ((ret = io_destroy(ctx)) < 0)
       printf("io_destroy error:%s", strerror(-ret));
err2:
free(buf);
err1:
close(fd);
err:
return -1;
}

编译源代码文件:gcc aio_read.c -o test -laio

执行:./test file.txt

结果:打印该文本文件前4096个字节的内容

9.4.3 AIO与设备驱动

用户空间调用io_submit()后,对应于用户传递的每一个iocb结构,内核会生成一个与之对应的kiocb结构。位于<linux/aio.h>

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相关的成员函数:

<linux/fs.h>

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支持。

猜你喜欢

转载自blog.csdn.net/xiezhi123456/article/details/80403536
今日推荐