• 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 
    ");
        fprintf(file, "stars
    ");
        fprintf(file, "what's your name?
    ");
        fprintf(file, "what's your name?
    ");
        fprintf(file, "what's your name?
    ");
        fprintf(file, "what's your name?
    ");
        fprintf(file, "what's your name?
    ");
        fprintf(file, "what's your name?
    ");
        fprintf(file, "what's your name?
    ");
        fprintf(file, "what's your name?
    ");
        fprintf(file, "what's your name?
    ");
        fprintf(file, "stars
    ");
        fprintf(file, "stars
    ");
        fprintf(file, "stars
    ");
        fprintf(file, "stars
    ");
        fprintf(file, "stars
    ");
    
        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
    ", iocb->u.c.nbytes, res);  
        printf("the content is :
    %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
    ", 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讲的很细

  • 相关阅读:
    数据结构 B/B+树
    Hadoop的目录结构
    安装JDK
    OSTEP-projects concurrency-webserver
    第二十四章(制作HTTP服务器端)学习笔记
    day4_生成小数的程序
    day4_用集合生成8位密码的程序
    day4_集合操作
    day3_homework
    day6_random模块的用法、break和continue
  • 原文地址:https://www.cnblogs.com/foreverstars/p/6507146.html
Copyright © 2020-2023  润新知