linux 异步IO通信

一. 回顾 

  做java开发的,一定对BIO,NIO,AIO通信很了解了,现在再在下面罗列一下:

同步阻塞IO(JAVA BIO): 
  同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。 
  同步非阻塞IO(Java NIO)

  同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。用户进程也需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问。 
异步阻塞IO(Java NIO):  
  此种方式下是指应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问IO是否完成,那么为什么说是阻塞的呢?因为此时是通过select系统调用来完成的,而select函数本身的实现方式是阻塞的,而采用select函数有个好处就是它可以同时监听多个文件句柄(如果从UNP的角度看,select属于同步操作。因为select之后,进程还需要读写数据),从而提高系统的并发性!  
(Java AIO(NIO.2))异步非阻塞IO:  
  在此种模式下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。    

BIO、NIO、AIO适用场景分析: 
    BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。 
    NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。 
    AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。 

 

  针对Java开发,基本上也就上面的三种通信模型了。我们都知道Linux下的五种I/O模型

1)阻塞I/O(blocking I/O)
2)非阻塞I/O (nonblocking I/O)
3) I/O复用(select 和poll) (I/O multiplexing)
4)信号驱动I/O (signal driven I/O (SIGIO))
5)异步I/O (asynchronous I/O (the POSIX aio_functions))

  前四种都是同步,只有最后一种才是异步IO。是的,异步IO是今天介绍的重点。

 

二. 异步IO通信版本

  异步IO通信在linux下有两个版本:glibc版本,linux内核版本。

2.1 glibc版本

2.1.1 接口和实现信息

int aio_read(struct aiocb *aiocbp);  /* 提交一个异步读 */
int aio_write(struct aiocb *aiocbp); /* 提交一个异步写 */
int aio_cancel(int fildes, struct aiocb *aiocbp); /* 取消一个异步请求(或基于一个fd的所有异步请求,aiocbp==NULL) */
int aio_error(const struct aiocb *aiocbp);        /* 查看一个异步请求的状态(进行中EINPROGRESS?还是已经结束或出错?) */
ssize_t aio_return(struct aiocb *aiocbp);         /* 查看一个异步请求的返回值(跟同步读写定义的一样) */
int aio_suspend(const struct aiocb * const list[], int nent, const struct timespec *timeout); /* 阻塞等待请求完成 */

其中,struct aiocb主要包含以下字段:
int                 aio_fildes;                 /* 要被读写的fd */
void *            aio_buf;                 /* 读写操作对应的内存buffer */
__off64_t      aio_offset;           /* 读写操作对应的文件偏移 */
size_t             aio_nbytes;             /* 需要读写的字节长度 */
int                 aio_reqprio;               /* 请求的优先级 */
struct sigevent   aio_sigevent;      /* 异步事件,定义异步操作完成时的通知信号或回调函数 */

glibc的aio实现是比较通俗易懂的:
1、异步请求被提交到request_queue中;
2、request_queue实际上是一个表结构,"行"是fd、"列"是具体的请求。也就是说,同一个fd的请求会被组织在一起;
3、异步请求有优先级概念,属于同一个fd的请求会按优先级排序,并且最终被按优先级顺序处理;
4、随着异步请求的提交,一些异步处理线程被动态创建。这些线程要做的事情就是从request_queue中取出请求,然后处理之;
5、为避免异步处理线程之间的竞争,同一个fd所对应的请求只由一个线程来处理;
6、异步处理线程同步地处理每一个请求,处理完成后在对应的aiocb中填充结果,然后触发可能的信号通知或回调函数(回调函数是需要创建新线程来调用的);
7、异步处理线程在完成某个fd的所有请求后,进入闲置状态;
8、异步处理线程在闲置状态时,如果request_queue中有新的fd加入,则重新投入工作,去处理这个新fd的请求(新fd和它上一次处理的fd可以不是同一个);
9、异步处理线程处于闲置状态一段时间后(没有新的请求),则会自动退出。等到再有新的请求时,再去动态创建;

  

2.2 linux内核版本

2.2.1 接口和实现信息

下面再来看看linux版本的异步IO。它主要包含如下系统调用接口:
int io_setup(int maxevents, io_context_t *ctxp);  /* 创建一个异步IO上下文(io_context_t是一个句柄) */
int io_destroy(io_context_t ctx);  /* 销毁一个异步IO上下文(如果有正在进行的异步IO,取消并等待它们完成) */
long io_submit(aio_context_t ctx_id, long nr, struct iocb **iocbpp);  /* 提交异步IO请求 */
long io_cancel(aio_context_t ctx_id, struct iocb *iocb, struct io_event *result);  /* 取消一个异步IO请求 */
long io_getevents(aio_context_t ctx_id, long min_nr, long nr, struct io_event *events, struct timespec *timeout)  /* 等待并获取异步IO请求的事件(也就是异步请求的处理结果) */

其中,struct iocb主要包含以下字段:
__u16     aio_lio_opcode;     /* 请求类型(如:IOCB_CMD_PREAD=读、IOCB_CMD_PWRITE=写、等) */
__u32     aio_fildes;         /* 要被操作的fd */
__u64     aio_buf;            /* 读写操作对应的内存buffer */
__u64     aio_nbytes;         /* 需要读写的字节长度 */
__s64     aio_offset;         /* 读写操作对应的文件偏移 */
__u64     aio_data;           /* 请求可携带的私有数据(在io_getevents时能够从io_event结果中取得) */
__u32     aio_flags;          /* 可选IOCB_FLAG_RESFD标记,表示异步请求处理完成时使用eventfd进行通知(百度一下) */
__u32     aio_resfd;          /* 有IOCB_FLAG_RESFD标记时,接收通知的eventfd */

其中,struct io_event主要包含以下字段:
__u64     data;               /* 对应iocb的aio_data的值 */
__u64     obj;                /* 指向对应iocb的指针 */
__s64     res;                /* 对应IO请求的结果(>=0: 相当于对应的同步调用的返回值;<0: -errno) */

  io_context_t句柄在内核中对应一个struct kioctx结构,用来给一组异步IO请求提供一个上下文。其主要包含以下字段:
struct mm_struct*     mm;             /* 调用者进程对应的内存管理结构(代表了调用者的虚拟地址空间) */
unsigned long         user_id;        /* 上下文ID,也就是io_context_t句柄的值(等于ring_info.mmap_base) */
struct hlist_node     list;           /* 属于同一地址空间的所有kioctx结构通过这个list串连起来,链表头是mm->ioctx_list */
wait_queue_head_t     wait;           /* 等待队列(io_getevents系统调用可能需要等待,调用者就在该等待队列上睡眠) */
int                   reqs_active;    /* 进行中的请求数目 */
struct list_head      active_reqs;    /* 进行中的请求队列 */
unsigned              max_reqs;       /* 最大请求数(对应io_setup调用的int maxevents参数) */
struct list_head      run_list;       /* 需要aio线程处理的请求列表(某些情况下,IO请求可能交给aio线程来提交) */
struct delayed_work   wq;             /* 延迟任务队列(当需要aio线程处理请求时,将wq挂入aio线程对应的请求队列) */
struct aio_ring_info  ring_info;      /* 存放请求结果io_event结构的ring buffer */

其中,这个aio_ring_info结构比较值得一提,它是用于存放请求结果io_event结构的ring buffer。它主要包含了如下字段:
unsigned long   mmap_base;       /* ring buffer的地始地址 */
unsigned long   mmap_size;       /* ring buffer分配空间的大小 */
struct page**   ring_pages;      /* ring buffer对应的page数组 */
long            nr_pages;        /* 分配空间对应的页面数目(nr_pages * PAGE_SIZE = mmap_size) */
unsigned        nr, tail;        /* 包含io_event的数目及存取游标 */

 

2.3 例子

#include <stdio.h>
#include <libaio.h>
#include <sys/eventfd.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>  
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string>
using std::string;

#define TEST_FILE "./aiotestfile.txt"
#define ALIGN_SIZE 512
#define RD_SIZE 1024
#define RD_EVENT_NUM 5

bool create_test_file(const string &a_strFileName)
{
    FILE *file = fopen(a_strFileName.c_str(), "w");
    if(NULL == file)
    {
        perror("fopen");
        return false;
    }
    fprintf(file, "my name is \n");
    fprintf(file, "stars\n");
    fprintf(file, "what's your name?\n");
    fprintf(file, "what's your name?\n");
    fprintf(file, "what's your name?\n");
    fprintf(file, "what's your name?\n");
    fprintf(file, "what's your name?\n");
    fprintf(file, "what's your name?\n");
    fprintf(file, "what's your name?\n");
    fprintf(file, "what's your name?\n");
    fprintf(file, "what's your name?\n");
    fprintf(file, "stars\n");
    fprintf(file, "stars\n");
    fprintf(file, "stars\n");
    fprintf(file, "stars\n");
    fprintf(file, "stars\n");

    fclose(file);

    return true;
}

void aio_call_back(io_context_t ctx, struct iocb *iocb, long res, long res2)
{
    printf("can read %d bytes, and in fact has read %d bytes\n", iocb->u.c.nbytes, res);  
    printf("the content is :\n%s", iocb->u.c.buf);
}

int main(int argc, char* argv[])
{
    if(!create_test_file(TEST_FILE))
    {
        perror("create_test_file");
        return -1;
    }

    int efd = eventfd(0, EFD_NONBLOCK|EFD_CLOEXEC);
    if(efd == -1)
    {
        perror("eventfd");
        return -1;
    }
    int epollfd = epoll_create(1);
    if(epollfd == -1)
    {
        perror("epoll_create");
        return -1;
    }
    struct epoll_event epevent;
    epevent.events = EPOLLIN|EPOLLET;
    epevent.data.ptr = NULL;
    if(epoll_ctl(epollfd, EPOLL_CTL_ADD, efd, &epevent) != 0)
    {
        perror("epoll_ctr");
        return -1;
    }

    int fd = open(TEST_FILE, O_RDWR|O_DIRECT);
    if(fd < 0)
    {
        perror("open");
        return -1;
    }

    void *buf;
    if(posix_memalign(&buf, ALIGN_SIZE, RD_SIZE))
    {
        perror("posix_memalign");
        return -1;
    }

    struct iocb *ocbs[RD_EVENT_NUM];
    for(int i = 0; i < RD_EVENT_NUM; ++i)
    {
        ocbs[i] = (struct iocb*)malloc(sizeof(struct iocb)); 
        io_prep_pread(ocbs[i], fd, buf, RD_SIZE, RD_SIZE * i);
        io_set_eventfd(ocbs[i], efd);
        io_set_callback(ocbs[i], aio_call_back);
    }

    io_context_t ctx = NULL;
    if(io_setup(8192, &ctx))
    {
        perror("io_setup");
        return -1;
    }
    if(io_submit(ctx, RD_EVENT_NUM, ocbs) != RD_EVENT_NUM)
    {
        perror("io_submit");
        return -1;
    }

    struct epoll_event *events_list = (struct epoll_event *)malloc(sizeof(struct epoll_event) * 32);
    int retnum = 0;
    while(retnum < RD_EVENT_NUM)
    {
        int epnum = epoll_wait(epollfd, events_list, 32, -1);  
        if(epnum <= 0){  
            if(errno != EINTR){  
                perror("epoll_wait");  
                exit(9);  
            }  
        }  
        else  
        {
            int ready;
            int n = read(efd, &ready, sizeof(ready));  
            if(n != 8){  
                perror("read error");  
                exit(10);  
            }  
            printf("finished io number: %d\n", ready);
            
            while(ready > 0)
            {
                struct timespec tms;
                tms.tv_sec = 0;
                tms.tv_nsec = 0;
                struct io_event *events = (struct io_event *)malloc(sizeof(struct io_event) * RD_EVENT_NUM);
                int ret = io_getevents(ctx, 1, RD_EVENT_NUM, events, &tms);
                if(ret > 0)
                {
                    for(int v = 0; v < ret; ++v)
                    {
                        ((io_callback_t)(events[v].data))(ctx, events[v].obj, events[v].res, events[v].res2);
                    }
                    retnum += ret;
                    ready -= ret;
                }
            }
        }
    }

    close(epollfd);
    free(buf);
    close(fd);
    io_destroy(ctx);
    close(efd);
    remove(TEST_FILE);

    for(int m = 0; m < RD_EVENT_NUM; m++)
    {
        free(ocbs[m]);
    }

    return 0;
}

 

2.4 比较

  从上面的流程可以看出,linux版本的异步IO实际上只是利用了CPU和IO设备可以异步工作的特性(IO请求提交的过程主要还是在调用者线程上同步完成的,请求提交后由于CPU与IO设备可以并行工作,所以调用流程可以返回,调用者可以继续做其他事情)。相比同步IO,并不会占用额外的CPU资源。
而glibc版本的异步IO则是利用了线程与线程之间可以异步工作的特性,使用了新的线程来完成IO请求,这种做法会额外占用CPU资源(对线程的创建、销毁、调度都存在CPU开销,并且调用者线程和异步处理线程之间还存在线程间通信的开销)。不过,IO请求提交的过程都由异步处理线程来完成了(而linux版本是调用者来完成的请求提交),调用者线程可以更快地响应其他事情。如果CPU资源很富足,这种实现倒也还不错。

  还有一点,当调用者连续调用异步IO接口,提交多个异步IO请求时。在glibc版本的异步IO中,同一个fd的读写请求由同一个异步处理线程来完成。而异步处理线程又是同步地、一个一个地去处理这些请求。所以,对于底层的IO调度器来说,它一次只能看到一个请求。处理完这个请求,异步处理线程才会提交下一个。而内核实现的异步IO,则是直接将所有请求都提交给了IO调度器,IO调度器能看到所有的请求。请求多了,IO调度器使用的类电梯算法就能发挥更大的功效。请求少了,极端情况下(比如系统中的IO请求都集中在同一个fd上,并且不使用预读),IO调度器总是只能看到一个请求,那么电梯算法将退化成先来先服务算法,可能会极大的增加碰头移动的开销。

  最后,glibc版本的异步IO支持非direct-io,可以利用内核提供的page cache来提高效率。而linux版本只支持direct-io,cache的工作就只能靠用户程序来实现了。

 

参考地址:

http://blog.csdn.net/u012398613/article/details/22897279 参考

http://lse.sourceforge.net/io/aio.html   使用帮助

http://www.ibm.com/developerworks/cn/linux/l-async/ 专业技术文档

http://www.jiangmiao.org/blog/2290.html API整理

http://blog.sina.com.cn/s/blog_87c80c500100yol3.html 有例子

http://hedengcheng.com/?p=98 innodb中的异步IO
http://www.pagefault.info/?p=76 nginx中的异步IO
http://stackoverflow.com/questions/6497217/c-how-to-use-both-aio-read-and-aio-write 与网络编程结合

http://blog.sina.com.cn/s/blog_3e3fcadd0100grgk.html 例子和API讲的很细

 

posted on 2017-03-05 22:23  霏霏暮雨  阅读(628)  评论(0编辑  收藏  举报

导航