Linux 异步 IO 之 Native AIO

Linux Native AIO

来看看 Linux 提供的 AIO 系统调用(自行封装的头文件 native_aio.h):

#ifndef __NATIVE_AIO_H__
#define __NATIVE_AIO_H__

#define _GNU_SOURCE

#include <unistd.h>
#include <sys/syscall.h>
#include <sys/time.h>
#include <linux/aio_abi.h>

static inline int io_setup(unsigned nr_events, aio_context_t* ctx_idp)
{
    return syscall(__NR_io_setup, nr_events, ctx_idp);
}

static inline int io_destroy(aio_context_t ctx)
{
    return syscall(__NR_io_destroy, ctx);
}

static inline int io_submit(aio_context_t ctx, long nr, struct iocb** iocbpp)
{
    return syscall(__NR_io_submit, ctx, nr, iocbpp);
}

static inline int io_getevents(aio_context_t ctx, long min_nr, long nr,
                               struct io_event* events, struct timespec* timeout)
{
    return syscall(__NR_io_getevents, ctx, min_nr, nr, events, timeout);
}

static inline int io_cancel(aio_context_t ctx, struct iocb* iocb,
                            struct io_event* result)
{
    return syscall(__NR_io_cancel, ctx, iocb, result);
}

#endif

通过 man 能够查到函数的描述,但是这些函数不是跨平台的,因此 libc 没有对这些函数进行封装,这里手动封装了一下。下面是一个使用示例:

#include "native_aio.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <fcntl.h>
#include <errno.h>

#define NR_EVENT 1024
#define BUFSIZE 4096

int main(void)
{
    int fd, ret = -1;
    char* buf;
    aio_context_t ctx;
    struct io_event event;
    struct iocb cb;
    struct iocb* cblist[] = {&cb};

    fd = open(__FILE__, O_RDONLY | O_DIRECT);
    if (fd == -1) {
        fprintf(stderr, "open(%s) failed: %s.\n", __FILE__, strerror(errno));
        return -1;
    }

    buf = aligned_alloc(512, BUFSIZE);
    if (!buf) {
        fprintf(stderr, "aligned_alloc(%u) failed: %s.\n", BUFSIZE,
                strerror(errno));
        goto err1;
    }

    memset(&ctx, 0, sizeof(ctx));
    ret = io_setup(NR_EVENT, &ctx);
    if (ret != 0) {
        fprintf(stderr, "io_setup failed: %s.\n", strerror(errno));
        goto err2;
    }

    memset(&cb, 0, sizeof(cb));
    cb.aio_data = (__u64)buf;
    cb.aio_fildes = fd;
    cb.aio_lio_opcode = IOCB_CMD_PREAD;
    cb.aio_buf = (__u64)buf;
    cb.aio_offset = 0;
    cb.aio_nbytes = BUFSIZE;

    ret = io_submit(ctx, 1, cblist);
    if (ret != 1) {
        fprintf(stderr, "io_submit failed: %s.\n", strerror(errno));
        goto err3;
    }

    ret = io_getevents(ctx, 1, 1, &event, NULL);
    if (ret != 1) {
        fprintf(stderr, "io_getevents failed: %s.\n", strerror(errno));
        goto err3;
    }

    if (event.res <= 0) {
        fprintf(stderr, "io error: %s.\n", strerror(-(event.res)));
    } else {
        printf("read %lld byte(s):\n", event.res);
        write(1, (const void*)(event.data), event.res);
    }

err3:
    io_destroy(ctx);
err2:
    free(buf);
err1:
    close(fd);
    return ret;
}

程序首先打开一个只读文件,注意这里加了选项“O_DIRECT”,直接从磁盘读取文件。因为绕过了 page cache,所以一般程序都会自己来实现 cache;接着使用 aligned_alloc() 申请存放读取内容的 buffer,起始地址需要和磁盘逻辑块大小对齐(一般是 512 字节)。

准备工作完成后,调用 io_setup() 来初始化一个 aio_context_t 标识符,用于后续的 aio 操作。描述 aio 信息的主要是结构体 struct iocb(在 /usr/include/linux/aio_abi.h 中定义):

struct iocb {
    /* these are internal to the kernel/libc. */
    __u64        aio_data;        /* data to be returned in event's data */
    __u32        PADDED(aio_key, aio_reserved1);
    /* the kernel sets aio_key to the req # */

    /* common fields */
    __u16        aio_lio_opcode;        /* see IOCB_CMD_ above */
    __s16        aio_reqprio;
    __u32        aio_fildes;

    __u64        aio_buf;
    __u64        aio_nbytes;
    __s64        aio_offset;

    /* extra parameters */
    __u64        aio_reserved2;        /* TODO: use this for a (struct sigevent *) */

    /* flags for the "struct iocb" */
    __u32        aio_flags;

    /*
     * if the IOCB_FLAG_RESFD flag of "aio_flags" is set, this is an
     * eventfd to signal AIO readiness to
     */
    __u32        aio_resfd;
}; /* 64 bytes */

需要填充的字段包括:

  • aio_data:事件完成后随着 struct io_event 返回的内容,后面详细介绍;
  • aio_lio_opcode:IO 类型,可能包含以下取值:
    • IOCB_CMD_PREAD:对应系统调用 pread(),读取从指定位置开始的指定长度的内容,但是不改变文件偏移;
    • IOCB_CMD_PWRITE:对应系统调用 pwrite(),从指定位置写入指定长度的内容,但是不改变文件偏移;
    • IOCB_CMD_FSYNC:对应系统调用 fsync(),将元数据和文件内容写到磁盘;
    • IOCB_CMD_FDSYNC:对应 fdatasync(),将文件内容和必需的元数据写到磁盘;
    • IOCB_CMD_NOOP:不确定,据说还没被使用;
    • IOCB_CMD_PREADV:对应 preadv(),从当前位置读取指定数量和长度的 buffer,但不改变文件偏移;
    • IOCB_CMD_PWRITEV:对应 pwritev(),从当前位置写入指定数量和长度的 buffer,但不改变文件偏移。
  • aio_fildes:要操作的文件描述符;
  • aio_buf:要操作的 buffer 起始地址;
  • aio_nbytes:要读写的长度;
  • aio_offset:要读写的起始位置。

其它字段一定要置 0。

填充完成后就可以调用 io_submit() 提交请求了,请求提交后 io_submit() 会立即返回。接着可以调用 io_getevents() 获取在 aio_context_t 上提交的事件,函数的第二个参数是最少需要获取多少个事件,第三个参数是最多获取多少个事件,第四个参数是超时设置,如果超过指定事件还没等到最少个数的事件就返回,返回值就是实际返回的事件个数。

从 io_getevents() 返回后从 struct io_event 获取事件执行的结果:

struct io_event {
    __u64        data;        /* the data field from the iocb */
    __u64        obj;         /* what iocb this event came from */
    __s64        res;         /* result code for this event */
    __s64        res2;        /* secondary result */
};

其中的 data 字段就是 struct iocb 里的 aio_data;obj 就是 iocb 本身;res 小于等于 0 表示出错,-res 的值就是 errno,res 大于 0 表示读写成功的字节数;res2 不知道干啥用的。

最后用 io_destroy() 释放资源。

参考资料

[1] Linux Asynchronous I/O Explained

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注