最新消息:

异步I/O之native AIO篇

IO admin 3874浏览 0评论

本文介绍Linux下另外一种异步I/O,即由Linux内核实现提供的native AIO机制,要使用这一套机制,可以利用libaio库,也可以手动利用syscall做一层自己的封装,不过这并无大碍,libaio库本身也很简单。
AIO的使用场景在哪里?为什么使用AIO的文件打开都带有O_DIRECT标记?其实这并不意味着AIO在普通(buffered)模式下就会报错或执行失败,比如在下面示例中,去掉O_DIRECT打开标记,程序一样可以执行,但此时就不再是纯粹的AIO,而“退化”等效为同步IO(参考1),因此一般应用AIO的推荐场景为“ Currently, the AIO interface is best for O_DIRECT access to a raw block device like a disk, flash drive or storage array. ”(参考2、3)。

先看一个利用libaio库实现的AIO示例:

/**
 * gcc libaio_test.c -o libaio_test -laio
 * ref: http://www.fsl.cs.sunysb.edu/~vass/linux-aio.txt
 * modified by: http://lenky.info/
 */
#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;
    struct iocb cb;
    struct iocb *cbs[1];
    unsigned char *buf;
    struct io_event events[1];
    int ret;
    int fd;
    fd = open("./libaio_test.c", O_RDWR | O_DIRECT);
    if (fd < 0) {
        perror("open error");
        goto err;
    }
    ret = posix_memalign((void **)&buf, 512, (BUF_SIZE + 1));
    if (ret < 0) {
        perror("posix_memalign failed");
        goto err1;
    }
    ctx = 0;
    ret = io_setup(128, &ctx);
    if (ret < 0) {
        printf("io_setup error:%s", strerror(-ret));
        goto err2;
    }
    memset(buf, 0, BUF_SIZE + 1);
    /* setup I/O control block */
    io_prep_pread(&cb, fd, buf, BUF_SIZE, 0);
    cb.data = main;
    cbs[0] = &cb;
    ret = io_submit(ctx, 1, cbs);
    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);
    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("data:%p, obj:%p, res:%u, res2:%un", events[0].data, events[0].obj,
            events[0].res, events[0].res2);
        printf("%pn", main);
        printf("%pn", &cb);
        printf("%pn", cb.data);
    } else {
        printf("AIO error:%s", strerror(-events[0].res));
        goto err3;
    }
    //printf("%sn", buf);
    if ((ret = io_destroy(ctx)) < 0) {
        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;
}

编译上面这个源文件,需要一个名为libaio的库,如果对应机器上没装,则需要安装上:

[root@localhost ~]# rpm -q libaio
libaio-0.3.106-3.2
[root@localhost ~]# cat /etc/issue
CentOS release 5.4 (Final)
Kernel r on an m
[root@localhost t]# ls /usr/src/kernels/
2.6.18-164.el5-x86_64

http://rpm.pbone.net/搜索关键字“libaio-0.3.106-3.2”,下载对应的库,比如我这里的CentOS 5.4系统,如果无法下载到已编译好的库,则可以直接下载源代码包:libaio-0.3.106-3.2.src.rpm,然后在本地用7z解压后,手动编译获得:

[root@localhost t]# cd libaio-0.3.106
[root@localhost libaio-0.3.106]# make
[root@localhost libaio-0.3.106]# ls src/libaio.so.1.0.1
src/libaio.so.1.0.1
[root@localhost libaio-0.3.106]# make install

编译并运行示例:

[root@localhost libaio-0.3.106]# cd ..
[root@localhost t]# gcc libaio_test.c -o libaio_test -laio
[root@localhost t]# ./libaio_test

一切OK。

CentOS 5.4是比较陈旧的系统,默认内核版本为2.6.18,不支持后面文章将讲到的eventfd
需要2.6.22以后的内核),换个CentOS 6.0的系统试试,默认是2.6.32的内核,被我升级到2.6.38了:

[root@www aio]# rpm -q libaio
libaio-0.3.107-10.el6.x86_64
[root@www aio]# cat /etc/issue
CentOS Linux release 6.0 (Final)
Kernel r on an m
[root@www aio]# ls /usr/src/kernels/
2.6.32-71.el6.x86_64
[root@www aio]# uname -a
Linux www.t1.com 2.6.38.8 #2 SMP Wed Nov 2 07:52:53 CST 2011 x86_64 x86_64 x86_64 GNU/Linux
[root@www aio]# gcc libaio_test.c -o libaio_test -laio
[root@www aio]# ./libaio_test

很顺利,本文下面的介绍将以CentOS 6.0/2.6.38系统环境为准。回过头来看示例源代码,用到了几个接口,分别如下(这几个接口的更多信息可以查看man手册,比如:http://www.kernel.org/doc/man-pages/online/pages/man2/io_getevents.2.html):

int io_setup(int maxevents, io_context_t *ctxp);
创建一个AIO上下文环境,因为native AIO是由内核实现并支持的,因此当应用程序需要使用native AIO时,需先通知内核,让内核做好相关准备工作,比如一系列的数据维护结构。该函数的第二个参数ctxp可以理解为类似于句柄的东东;而第一个参数maxevents表示这个待创建的AIO上下文环境可同时维持的最大请求数目。注意:在调用该函数之前必须将*ctxp初始化为0。
这里还有一个注意点(其它系列函数类似):该函数执行成功返回0,如果失败则返回对应错误码的负值。只有libaio库才会这样,如果是我们自己通过syscall进行封装,则不会有此问题:http://www.kernel.org/doc/man-pages/online/pages/man2/io_setup.2.html

RETURN VALUE top

On success, io_setup() returns 0. For the failure return, see NOTES.

NOTES top

Glibc does not provide a wrapper function for this system call.

The wrapper provided in libaio for io_setup() does not follow the usual C
library conventions for indicating error: on error it returns a negated error
number (the negative of one of the values listed in ERRORS). If the system
call is invoked via syscall(2), then the return value follows the usual
conventions for indicating an error: -1, with errno set to a (positive) value
that indicates the error.

void io_prep_pread(struct iocb *iocb, int fd, void *buf, size_t count, long long offset);
这个函数定义在libaio库的头文件libaio.h内,是个内联函数,功能很简单,就是对代表一个请求的iocb结构体进行填充和设置:

static inline void io_prep_pread(struct iocb *iocb, int fd, void *buf, size_t count, long long offset)
{
memset(iocb, 0, sizeof(*iocb));
iocb->aio_fildes = fd;
iocb->aio_lio_opcode = IO_CMD_PREAD;
iocb->aio_reqprio = 0;
iocb->u.c.buf = buf;
iocb->u.c.nbytes = count;
iocb->u.c.offset = offset;
}

结构体iocb用于描述一个AIO请求,为什么需要这样的结构体(以及后面的io_event)?因为AIO是异步的,发起IO请求的逻辑和处理请求结果的逻辑多半不在同一处,并且还需在应用层与内核层进行请求传递,所以需要这样的封装来组织、描述和维护对应的IO请求。结构体iocb的具体定义如下:

struct iocb {
    PADDEDptr(void *data, __pad1);  /* Return in the io completion event */
    PADDED(unsigned key, __pad2);   /* For use in identifying io requests */
    short       aio_lio_opcode;
    short       aio_reqprio;
    int     aio_fildes;
    union {
        struct io_iocb_common       c;
        struct io_iocb_vector       v;
        struct io_iocb_poll     poll;
        struct io_iocb_sockaddr saddr;
    } u;
};
struct io_iocb_common {
    PADDEDptr(void  *buf, __pad1);
    PADDEDul(nbytes, __pad2);
    long long   offset;
    long long   __pad3, __pad4;
};  /* result code is the amount read or -'ve errno */

字段data用于传递自定义数据到请求结束后的处理环境里,因为异步IO应用的设置IO请求和IO请求结束后的处理可能在不同的业务环境里,如果要在这两个环境里传递一些额外数据,则可以利用该字段。
字段aio_lio_opcode表示AIO的操作类型,可以是IO_CMD_PREAD和IO_CMD_PWRITE等。
字段aio_fildes表示AIO的对应文件描述符。
结构体io_iocb_common的buf、nbytes、offset分别表示存放数据的缓存区地址以及大小、偏移。

int io_submit(io_context_t ctx, long nr, struct iocb *ios[]);
请求设置好后,即可利用io_submit()函数将它们提交给内核进行处理,可一次提交多个请求,所有请求以数组的形式存放到ios内,而总请求个数通过参数nr进行传递;第一个参数ctx为函数io_setup()创建的AIO上下文环境。
该函数的返回值ret有几种情况:
ret = nr:所有请求成功提交处理。
0 < ret < nr:前ret个请求成功提交处理。函数io_submit()逐个处理数组内的请求,当处理到第ret个时失败,此时返回对应的数组下标值。此种情况无法获取具体的错误信息。
ret < 0:所有请求都提交失败,这可能是提供的AIO上下文环境无效,也可能是在处理第0个请求时失败返回。在此种情况,可以获取具体的错误信息,如示例代码里一样,可以通过strerror()函数获取错误信息。

int io_getevents(io_context_t ctx_id, long min_nr, long nr, struct io_event *events, struct timespec *timeout);
该函数用于等待IO请求结束(成功或失败)并获取结束请求对应的描述请求结果的io_event结构体。
io_event定义如下:

struct io_event {
        PADDEDptr(void *data, __pad1);
        PADDEDptr(struct iocb *obj,  __pad2);
        PADDEDul(res,  __pad3);
        PADDEDul(res2, __pad4);
};

主要是四个字段data、obj、res和res2,另外的pad*字段用于填充,无实际意义。
字段data:指向请求对象结构体iocb的的data字段,在前面提到过,利用该字段可以给每个请求外挂额外的数据。libaio提供有一个io_set_callback()的函数用于设置回调函数就是利用的这个字段:

static inline void io_set_callback(struct iocb *iocb, io_callback_t cb)
{
    iocb->data = (void *)cb;
}

字段obj:指向请求对象结构体iocb。
字段res:读到的字节数;如果为负数,则-res是对应的错误码errno。
字段res2:0表示成功,否则表示失败。

从示例代码的执行结果对照来理解:

[root@www aio]# gcc libaio_test.c -o libaio_test -laio
[root@www aio]# ./libaio_test
data:0x400949, obj:0x7ffffb0726c0, res:2592, res2:0
0x400949
0x7ffffb0726c0
0x400949
[root@www aio]# ls -l
total 16
-rwxr-xr-x. 1 root root 9635 Jul 21 09:36 libaio_test
-rw-r--r--. 1 root root 2592 Jan  6  2013 libaio_test.c

再来看函数io_getevents()几个参数的含义:
ctx_id:AIO上下文环境句柄。
min_nr:最少需等待结束的IO请求数目,如果少于这个数目则继续等待。
nr:最多需等待结束的IO请求数目,一般情况也就是存储结果的events数组可容纳的个数。
events:存储请求结果的events数组。
timeout:为NULL表示无限等待;结构体timespec两个字段秒和纳秒都为0,表示不管有没有请求结束都立即返回;其他值则表示等待的时间,到了这个时间点,不管有没有请求结束都返回。

返回值:
ret = nr:最大容量的请求结束返回,此时在内核里可能还有其它已结束的请求没有获取回来,可以利用0等待继续获取。
min_nr <= ret <= nr:结束的请求都已经返回,内核里当前应该没有已结束的请求。
0 < ret < min_nr:结束的请求都已经返回,等待已超时。
ret = 0:等待超时,无结束请求。
ret < 0:发生错误:EFAULT表示events或timeout参数非法;EINVAL表示ctx_id无效或min_nr超限或nr超限;EINTR表示信号中断;

int io_destroy(io_context_t ctx);
该函数无需多说,用于销毁一个AIO上下文环境。

这几个接口在对应的头文件里有定义,比如我这里是:

[root@www aio]# ls /usr/include/aio.h
/usr/include/aio.h
[root@www aio]# ls /usr/include/libaio.h
/usr/include/libaio.h

使用Linux native AIO机制的另外一种简单办法就是利用syscall手动构造一层封装,而无需使用libaio库(虽然该库本身也很简单),这样做的好处是只要内核支持即可,无需安装libaio库,所以相对比较简洁方便,nginx就是这样用的,直接看重写的示例代码:

/**
 * gcc native_aio_test.c -o native_aio_test
 * ref: http://www.fsl.cs.sunysb.edu/~vass/linux-aio.txt
 * modified by: http://lenky.info/
 */
#define _GNU_SOURCE           /* O_DIRECT and syscall() is not POSIX */
#include <stdio.h>            /* for perror() */
#include <unistd.h>           /* for syscall() */
#include <sys/syscall.h>      /* for __NR_* definitions */
#include <linux/aio_abi.h>    /* for AIO types and constants */
#include <fcntl.h>            /* O_RDWR */
#include <string.h>           /* memset() */
#include <inttypes.h>         /* uint64_t */
#include <stdlib.h>
#define BUF_SIZE (4096)
inline int io_setup(unsigned nr, aio_context_t *ctxp)
{
    return syscall(__NR_io_setup, nr, ctxp);
}
inline int io_submit(aio_context_t ctx, long nr,  struct iocb **iocbpp)
{
    return syscall(__NR_io_submit, ctx, nr, iocbpp);
}
inline int io_getevents(aio_context_t ctx, long min_nr, long max_nr,
        struct io_event *events, struct timespec *timeout)
{
    return syscall(__NR_io_getevents, ctx, min_nr, max_nr, events, timeout);
}
inline int io_destroy(aio_context_t ctx)
{
    return syscall(__NR_io_destroy, ctx);
}
int main(int argc, char **argv)
{
    aio_context_t ctx;
    struct iocb cb;
    struct iocb *cbs[1];
    unsigned char *buf;
    struct io_event events[1];
    int ret;
    int fd;
    fd = open("./native_aio_test.c", O_RDWR | O_DIRECT);
    if (fd < 0) {
        perror("open error");
        goto err;
    }
    ret = posix_memalign((void **)&buf, 512, (BUF_SIZE + 1));
    if (ret < 0) {
        perror("posix_memalign failed");
        goto err1;
    }
    ctx = 0;
    ret = io_setup(128, &ctx);
    if (ret < 0) {
        perror("io_setup error");
        goto err2;
    }
    /* setup I/O control block */
    memset(&cb, 0, sizeof(cb));
    cb.aio_fildes = fd;
    cb.aio_lio_opcode = IOCB_CMD_PREAD;
    /* command-specific options */
    memset(buf, 0, BUF_SIZE + 1);
    cb.aio_buf = (uint64_t)buf;
    cb.aio_offset = 0;
    cb.aio_nbytes = BUF_SIZE;
    cb.aio_data = (__u64)main;
    cbs[0] = &cb;
    ret = io_submit(ctx, 1, cbs);
    if (ret != 1) {
        if (ret < 0) {
            perror("io_submit error");
        } else {
            fprintf(stderr, "could not sumbit IOs");
        }
        goto err3;
    }
    /* get the reply */
    ret = io_getevents(ctx, 1, 1, events, NULL);
    if (ret != 1) {
        if (ret < 0) {
            perror("io_getevents error");
        } else {
            fprintf(stderr, "could not get Events");
        }
        goto err3;
    }
    if (events[0].res2 == 0) {
        printf("data:%p, obj:%p, res:%u, res2:%un", events[0].data, events[0].obj,
            events[0].res, events[0].res2);
        printf("%pn", main);
        printf("%pn", &cb);
        printf("%pn", cb.aio_data);
    } else {
        printf("AIO error:%s", strerror(-events[0].res));
        goto err3;
    }
    //printf("%sn", buf);
    if (io_destroy(ctx) < 0) {
        perror("io_destroy error");
        goto err2;
    }
    free(buf);
    close(fd);
    return 0;
err3:
    if (io_destroy(ctx) < 0)
        perror("io_destroy error");
err2:
    free(buf);
err1:
    close(fd);
err:   
    return -1;
}

示例源代码最前面给出的几个接口的参数和功能与前面介绍的一致,但返回值已符合C库标准约定,所以可以直接使用perror()函数显示错误信息。
示例代码包含了内核头文件,因为用到的几个变量类型,比如aio_context_t、iocb、io_event定义在内核内(具体是头文件:include/linux/aio_abi.h)。

编译执行:

[root@www aio]# uname -a
Linux www.t1.com 2.6.38.8 #2 SMP Wed Nov 2 07:52:53 CST 2011 x86_64 x86_64 x86_64 GNU/Linux
[root@www aio]# cat /etc/issue
CentOS Linux release 6.0 (Final)
Kernel r on an m
[root@www aio]# gcc native_aio_test.c -o native_aio_test
[root@www aio]# ./native_aio_test
data:0x400865, obj:0x7fff8f091f90, res:3348, res2:0
0x400865
0x7fff8f091f90
0x400865
[root@www aio]# ls -l
total 16
-rwxr-xr-x. 1 root root 9558 Jul 21 10:55 native_aio_test
-rw-r--r--. 1 root root 3348 Jan  6  2013 native_aio_test.c

一切OK。

编程注意:AIO的异步意味着IO请求过程中使用的一些变量要是持久的,不能是函数内局部变量,这就要特别注意一些传指针获取结果的参数(即输出参数),你得清楚的知道,当你获得AIO结果时,参数对应的那块内存是否还在你当前流程的控制之中;而像我这里的示例代码,就一个main()函数,所以使用局部变量倒也无所大碍,但仅仅只是示例而已。

完全参考:
1,http://code.google.com/p/kernel/wiki/AIOUserGuide
2,http://hi.baidu.com/_kouu/item/2b3cfecd49c17d10515058d9
3,http://stackoverflow.com/questions/8768083/difference-between-posix-aio-and-libaio-on-linux
4,http://www.fsl.cs.sunysb.edu/~vass/linux-aio.txt
5,http://bert-hubert.blogspot.com/2012/05/on-linux-asynchronous-file-io.html

 

转载请注明:爱开源 » 异步I/O之native AIO篇

您必须 登录 才能发表评论!