io复用_io复用模型 - CSDN
精华内容
参与话题
  • 在之前我们实现的并发服务端时通过床将多个...因此我们可以考虑通过另一种方式来实现服务端的并发服务——IO复用。复用:复用在通讯领域很常见,一般常见”频分复用”,”时分复用”等名词。其实复用就是在一个通信频

    在之前我们实现的并发服务端时通过床将多个进程来实现的,这种并实现并发的方式简单方便,但是进程的创建和销毁是很消耗系统资源的,在访问量大时服务器很容易出现资源不够用的情况。除此之外,由于每个进程有独立的内存空间,所以进程间的通讯也相对比较复杂。因此我们可以考虑通过另一种方式来实现服务端的并发服务——IO复用。

    复用:

    复用在通讯领域很常见,一般常见”频分复用”,”时分复用”等名词。其实复用就是在一个通信频道内传递多个数据(信号)的技术。以频分复用为例:其实就是在一个通信信道内,发送端通过把信息加载在不同频率的波段上进行发送,而接受端在接受到波时通过滤波装置把各中频率的波进行分离,以此达到提高通信信道利用率的目的。

    IO复用:

    IO复用其实也是通过对IO描述符的复用来减少进程的创建,使得服务端始终只有一个进程,从而节省了系统资源,提高效率。


    select()函数是最具有代表性的实现复用服务端的方法,它可以将多个文件描述符集中到一起进行统一监视,当监视到有文件描述符需要输入或者是输出时就选择该接口进行通讯,通讯完成之后就回到之前监视的状态。

    监视内容:是否存在套接字接受数据?无需阻塞传输数据的套接字有哪些?哪些套接字发生了异常?

    int select(int maxfd,fd_set *read_set, *write_set,fd_set *except_set, const struct timeval *timeout)选择描述符进行通讯:

    • maxfd(监视数量):监视对象文件描述符数量

    • read_set(读取文件描述符集合的地址):将所有关注”是否存在待读取数据”的文件描述符注册到fd_set集合中,并传递地址值。也就是说select()函数会监视这个集合里边的文件描述符是是否有待读取的数据,没有要监听的描述符时传0

    • write_set(写入文件描述符集合的地址):将所有关注”是否可传输无阻塞数据”的文件描述符注册到fd_set集合中,并传递地址值。也就是说select()函数会监视这个集合里边的文件描述符是否能发送无阻塞数据,没有要监听的描述符时传0

    • except_set(发生异常文件描述符集合的地址):将所有关注”是否可发生异常”的文件描述符注册到fd_set集合中,并传递地址值。也就是说select()函数会监视这个集合里边的文件描述符是否发生异常,没有要监听的描述符时传0

    • timeout(超时):位防止无限进入阻塞状态,设置一个超时信息

    发生错误时返回-1,超时时返回0,当所关注的事件发生时,返回所发生事件的文件描述符数量

    select()函数的使用比较复杂,大体分为三步:

    1. 参数设置:

      • 设置文件描述符:使用select()函数能同时监听多个文件描述符,首先要使用fd_set类型将这些文件描述符按照分类(接收,传输,异常)集中起来。

        fd_set是一个存有0和1的位数组。从下标0开始,一直到下标为当前文件描述符的最大序号为止,依次表示该文件描述符是否被监听,例如fd_set 变量fds[0]中的值为1时表示文件描述符0(标准的输入流)被监听。
        针对fd_set的操作都是以位为单位的,为此专门编写了用于fd_set读写的宏定义:

        1. FD_ZERO(fd_set *fdset):将fd_set的所有位初始化为0

        2. FD_SET(int fd,fd_set *fdset):在fd_set中注册文件描述符fd的信息

        3. FD_CLR(int fd,fd_set *fdset):从fd_set中清除文件描述符fd的信息

        4. FD_ISSET(int fd,fd_set *fdset):查询fd_set中是否包含文件描述符fd的信息

      • 指定监听范围:指定监听文件描述符的范围,其实也就是fd_set中的文件描述符数量,由于每次新创建一个文件描述符时都会自动加1,所以要传入的值为最大的文件描述符+1(加一是由于文件描述符的标号从0开始)。

      • 设置超时:由于当文件描述符没有状态的改变时select()函数会始终处于阻塞状态,设置超时时间就是为了防止无限制的等待。即使文件描述符没有发生变化,只要过了指定时间,函数会返回0。这样在函数调用时能知道当前的状态。

        结构体timeval用于保存设置的超时时间,每次在调用select()函数之前都要重新设置超时时间,其结构体如下:

        struct timeval{
            long tv_sec;//秒数
            long tv_usec://毫秒数
        }
    2. 调用select()函数:监听注册的文件描述符的状态,当有状态发生变化,或者时超时时返回结果。

    3. 查看调用结果:当select()函数返回值是大于0的整数时说明是所监听的文件描述符的状态发生了变化,这时我们可以通过之前的fd_set变量来查看变化的结果。

      当select()函数调用完之后向其传入的fd_set变量将发生变化,原来为1的所有位均变为0,但是发生变化的文件描述符对应位除外,因此可以认为值仍为1的位置上的文件描述符发生了变化。


    至此关于select()函数的介绍就结束了,用起来比较复杂,我们梳理一遍使用过程:

    1. 准备工作:

      • 为select()设置要监视的文件描述符集合,使用函数库提供的关于fd_set的宏定义设置fd_set

      • 为select()设置监视范围,即当前最大文件描述符+1

      • 位select(0设置超时时间,把秒数填入timeval结构体的tv_sec成员中,把毫秒数填入timeval结构体的tv_usec成员中,每次在调用select()函数之前都要重新设置超时时间。

    2. 调用select()函数

    3. 查看调用结果:根据fd_set调用前后的变化来确定发生变化的文件描述符,调用之后fd_set中值为1的位所对应的文件描述符状态发生了变化

    4. 调用发生变化的文件描述符进行相应的操作


    在大体了解了select()函数的使用过程之后我们就可以尝试着进行一下简单的应用:

    #include<stdio.h>
    #include<unistd.h>
    #include<sys/time.h>
    #include<sys/select.h>
    
    #define BUFF_SIZE 30
    
    int main(){
        //声明文件描述符集合
        fd_set read_set;
        fd_set temp_set;
        //保存函数的返回结果
        int select_res;
        //字符串长度
        int str_len;
        //字符缓冲
        char buff[BUFF_SIZE];
        //超时时间结构体
        struct timeval time_out;
        //初始化fd_set,所有位都置0
        FD_ZERO(&read_set);
        //设置fd_set,使其监视文件描述符为0的文件描述符(系统的标准输入流)
        FD_SET(0,&read_set);
    
        while(1){
            temp_set = read_set;
            //设置超时时间
            time_out.tv_sec = 5;
            time_out.tv_usec = 0;
            //调用select()函数
            select_res = select(1,&temp_set,0,0,&time_out);
            //根据返回值来判断是否变化
            if(select_res == -1){
                puts("select() error");
                break;
            }else if(select_res == 0){
                puts("select() timeout");
            }else{
                //检查是否含有要查询的描述符
                if(FD_ISSET(0,&temp_set)){
                    //从文件描述符为0的流中读取数据
                    str_len = read(0,buff,BUFF_SIZE);
                    buff[str_len] = 0;
                    printf("message from console : %s ",buff);
                }
            }
    
    
        }
    
    return 0;
    }

    IO复用的服务端:

    /*************************************************************************
        > File Name: echo_select_server.c
        > Author: xjhznick
        > Mail: xjhznick@gmail.com 
        > Created Time: 2015年03月26日 星期四 14时03分40秒
      > Description:使用select函数实现I/O复用服务器端
     ************************************************************************/
    
    #include<stdio.h>
    #include<stdlib.h>
    #include<string.h>
    #include<unistd.h>
    #include<arpa/inet.h>
    #include<sys/socket.h>
    #include<sys/time.h>
    #include<sys/select.h>
    
    void error_handling(char *message);
    
    #define BUFF_SIZE 32
    
    int main(int argc, char *argv[])
    {
        int server_sock;
        int client_sock;
    
        struct sockaddr_in server_addr;
        struct sockaddr_in client_addr;
        socklen_t client_addr_size;
    
        char buff[BUFF_SIZE];
        fd_set reads, reads_init;
        struct timeval timeout, timeout_init;
    
        int str_len, i, fd_max, fd_num;
    
        if(argc!=2){ //命令行中启动服务程序仅限一个参数:端口号
            printf("Usage : %s <port>\n", argv[0]);
            exit(1);
        }
    
        //调用socket函数创建套接字
        server_sock = socket(PF_INET, SOCK_STREAM, 0);
        if(-1 == server_sock){
            error_handling("socket() error.");
        }
    
        memset(&server_addr, 0, sizeof(server_addr));
        server_addr.sin_family = AF_INET;
        server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
        server_addr.sin_port = htons(atoi(argv[1]));
    
        //调用bind函数分配IP地址和端口号
        if( -1 == bind( server_sock, (struct sockaddr*)&server_addr, 
                    sizeof(server_addr)) ){
            error_handling("bind() error");
        }
    
        //监听端口的连接请求,连接请求等待队列size为5
        if( -1 == listen(server_sock, 5) ){
            error_handling("listen() error");
        }
    
        //register fd_set var
        FD_ZERO(&reads_init);
        FD_SET(server_sock, &reads_init);//monitor socket: server_sock
        FD_SET(0, &reads_init);// stdin also works
        fd_max = server_sock;
        //
        timeout_init.tv_sec = 5;
        timeout_init.tv_usec= 0;
    
        while(1){
            //调用select之后,除发生变化的文件描述符对应的bit,其他所有位置0,所以需用保存初值,通过复制使用
            reads = reads_init;
            //调用select之后,timeval成员值被置为超时前剩余的时间,因此使用时也需要每次用初值重新初始化
            timeout = timeout_init;
            fd_num = select(fd_max+1, &reads, NULL, NULL, &timeout);
            if(fd_num < 0){
                fputs("Error select()!", stderr);
                break;
            }else if(fd_num == 0){
                puts("Time-out!");
                continue;
            }
            for(i=0; i<=fd_max; i++){
                if(FD_ISSET(i, &reads)){
                    if(i == server_sock){//connection request!
                        //接受连接请求
                        client_addr_size = sizeof(client_addr);
                        client_sock = accept( server_sock, (struct sockaddr*)&client_addr, &client_addr_size );
                        //accept函数自动创建数据I/0 socket
                        if(-1 == client_sock){
                            error_handling("accept() error");
                            //健壮性不佳,程序崩溃退出
                        } else{
                            //注册与客户端连接的套接字文件描述符
                            FD_SET(client_sock, &reads_init);
                            if(fd_max < client_sock) fd_max = client_sock;
                            printf("Connected client : %d\n", client_sock);
                        }
                    }else{//read message!
                        str_len = read(i, buff, BUFF_SIZE);
                        if(str_len){//echo to client
                            buff[str_len] = 0;
                            printf("Message from client %d: %s", i, buff);
                            write(i, buff, str_len);
                        }else{ //close connection
                            FD_CLR(i, &reads_init);
                            close(i);
                            printf("Disconnected client %d!\n", i);
                        }
                    }//end of i==|!=server_sock
                }//end of if(FD_ISSET)
            }//end of while
    
        }//end of for
    
        //断开连接,关闭套接字
        close(server_sock);
    
        return 0;
    }
    
    void error_handling(char *message)
    {
        fputs(message, stderr);
        fputc('\n', stderr);
        exit(EXIT_FAILURE);
    }
    展开全文
  • I/O复用的理解

    千次阅读 多人点赞 2018-05-12 00:31:27
    I/O复用就是单个线程通过记录跟踪每一个Sock(I/O流)的状态来同时管理多个I/O流.假设你是一个机场的空管, 你需要管理到你机场的所有的航线, 包括进港,出港, 有些航班需要放到停机坪等待,有些航班需要去登机口接...

    I/O复用就是单个线程通过记录跟踪每一个Sock(I/O流)的状态来同时管理多个I/O.

    假设你是一个机场的空管, 你需要管理到你机场的所有的航线, 包括进港,出港, 有些航班需要放到停机坪等待,有些航班需要去登机口接乘客。

    你会怎么做?
    最简单的做法,就是你去招一大批空管员,然后每人盯一架飞机, 从进港,接客,排位,出港,航线监控,直至交接给下一个空港,全程监控。
    那么问题就来了:

    · 很快你就发现空管塔里面聚集起来一大票的空管员,交通稍微繁忙一点,新的空管员就已经挤不进来了。 

    · 空管员之间需要协调,屋子里面就1, 2个人的时候还好,几十号人以后 ,基本上就成菜市场了。

    · 空管员经常需要更新一些公用的东西,比如起飞显示屏,比如下一个小时后的出港排期,最后你会很惊奇的发现,每个人的时间最后都花在了抢这些资源上。 
    现实上我们的空管同时管几十架飞机稀松平常的事情, 他们怎么做的呢?
    他们用这个东西

    · 

    · 这个东西叫flight progress strip. 每一个块代表一个航班,不同的槽代表不同的状态,然后一个空管员可以管理一组这样的块(一组航班),而他的工作,就是在航班信息有新的更新的时候,把对应的块放到不同的槽子里面。
    这个东西现在还没有淘汰哦,只是变成电子的了而已。。
    是不是觉得一下子效率高了很多,一个空管塔里可以调度的航线可以是前一种方法的几倍到几十倍。
    如果你把每一个航线当成一个Sock(I/O ), 空管当成你的服务端Sock管理代码的话.

    第一种方法就是最传统的多进程并发模型 (每进来一个新的I/O流会分配一个新的进程管理。)
    第二种方法就是I/O多路复用 (单个线程,通过记录跟踪每个I/O(sock)的状态,来同时管理多个I/O流 。)


    其实“I/O多路复用这个坑爹翻译可能是这个概念在中文里面如此难理解的原因。所谓的I/O多路复用在英文中其实叫 I/O multiplexing. 如果你搜索multiplexing啥意思,基本上都会出这个图:


    · 于是大部分人都直接联想到"一根网线,多个sock复用" 这个概念,包括上面的几个回答, 其实不管你用多进程还是I/O多路复用, 网线都只有一根好伐。多个Sock复用一根网线这个功能是在内核+驱动层实现的

    重要的事情再说一遍: I/O multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O)的状态(对应空管塔里面的Fight progress strip)来同时管理多个I/O. 发明它的原因,是尽量多的提高服务器的吞吐能力。
    是不是听起来好拗口,看个图就懂了.


    在同一个线程里面, 通过拨开关的方式,来同时传输多个I/O流, (学过EE的人现在可以站出来义正严辞说这个叫时分复用了)。
    什么,你还没有搞懂一个请求到来了,nginx使用epoll接收请求的过程是怎样的, 多看看这个图就了解了。提醒下,ngnix会有很多链接进来, epoll会把他们都监视起来,然后像拨开关一样,谁有数据就拨向谁,然后调用相应的代码处理。
    了解这个基本的概念以后,其他的就很好解释了。

    select, poll, epoll 都是I/O多路复用的具体的实现,之所以有这三个鬼存在,其实是他们出现是有先后顺序的。

    I/O多路复用这个概念被提出来以后, select是第一个实现 (1983 左右在BSD里面实现的)

    select 被实现以后,很快就暴露出了很多问题。

    · select 会修改传入的参数数组,这个对于一个需要调用很多次的函数,是非常不友好的。

    · select 如果任何一个sock(I/O stream)出现了数据,select 仅仅会返回,但是并不会告诉你是那个sock上有数据,于是你只能自己一个一个的找,10几个sock可能还好,要是几万的sock每次都找一遍。

    · select 只能监视1024个链接,linux 定义在头文件中的,参见FD_SETSIZE

    · select 不是线程安全的,如果你把一个sock加入到select, 然后突然另外一个线程发现,尼玛,这个sock不用,要收回。对不起,这个select 不支持的,如果你丧心病狂的竟然关掉这个sock, select的标准行为是。。呃。。不可预测的, 这个可是写在文档中的哦.

    “If a file descriptor being monitored by select() is closed in another thread, the result is unspecified”
    霸不霸气

    于是14年以后(1997年)一帮人又实现了poll, poll 修复了select的很多问题,比如

    · poll 去掉了1024个链接的限制,于是要多少链接呢, 主人你开心就好。

    · poll 从设计上来说,不再修改传入数组,不过这个要看你的平台了,所以行走江湖,还是小心为妙。

    其实拖14年那么久也不是效率问题, 而是那个时代的硬件实在太弱,一台服务器处理1千多个链接简直就是神一样的存在了,select很长段时间已经满足需求。

    但是poll仍然不是线程安全的, 这就意味着,不管服务器有多强悍,你也只能在一个线程里面处理一组I/O流。你当然可以那多进程来配合了,不过然后你就有了多进程的各种问题。

    于是5年以后, 2002, 大神 Davide Libenzi 实现了epoll.

    epoll 可以说是I/O 多路复用最新的一个实现,epoll 修复了poll select绝大部分问题, 比如:

    · epoll 现在是线程安全的。 

    · epoll 现在不仅告诉你sock组里面数据,还会告诉你具体哪个sock有数据,你不用自己去找了。 
    epoll 当年的patch,现在还在,下面链接可以看得到:
    /dev/epoll Home Page
    可是epoll 有个致命的缺点。。只有linux支持。比如BSD上面对应的实现是kqueue
    ngnix 的设计原则里面, 它会使用目标平台上面最高效的I/O多路复用模型咯,所以才会有这个设置。一般情况下,如果可能的话,尽量都用epoll/kqueue吧。
    详细的在这里:

    PS: 上面所有这些比较分析,都建立在大并发下面,如果你的并发数太少,用哪个,其实都没有区别。 如果像是在欧朋数据中心里面的转码服务器那种动不动就是几万几十万的并发,不用epoll我可以直接去撞墙了

    Linux下实现I/O复用的系统调用有select、poll、epoll。

    select系统调用的用途:在一段时间内,监听用户感兴趣的文件描述符上的可读,可写和异常等事件

    poll系统调用:在制定时间内轮询一定数量的文件描述符,已测试其中是否有就绪者

    epoll系列系统调用:

    epolllinux特有的I/O复用函数

    文件描述符(用来标识内核中的事件表)的创建有epoll_creat来完成


     

    展开全文
  • IO多路复用机制详解

    万次阅读 多人点赞 2015-09-24 11:06:54
    高性能IO模型浅析   服务器端编程经常需要构造高性能的IO模型,常见的IO模型有四种: (1)同步阻塞IO(Blocking IO):即传统的IO模型。 (2)同步非阻塞IO(Non-blocking IO):默认创建的...

    高性能IO模型浅析

     

    服务器端编程经常需要构造高性能的IO模型,常见的IO模型有四种:

    (1)同步阻塞IO(Blocking IO):即传统的IO模型。

    (2)同步非阻塞IO(Non-blocking IO):默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK。注意这里所说的NIO并非Java的NIO(New IO)库。

    (3)IO多路复用(IO Multiplexing):即经典的Reactor设计模式,有时也称为异步阻塞IO,Java中的Selector和Linux中的epoll都是这种模型。

    (4)异步IO(Asynchronous IO):即经典的Proactor设计模式,也称为异步非阻塞IO。

    同步和异步的概念描述的是用户线程与内核的交互方式:同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;而异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。

    阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式:阻塞是指IO操作需要彻底完成后才返回到用户空间;而非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。

    另外,Richard Stevens 在《Unix 网络编程》卷1中提到的基于信号驱动的IO(Signal Driven IO)模型,由于该模型并不常用,本文不作涉及。接下来,我们详细分析四种常见的IO模型的实现原理。为了方便描述,我们统一使用IO的读操作作为示例。

    一、同步阻塞IO

    同步阻塞IO模型是最简单的IO模型,用户线程在内核进行IO操作时被阻塞。

    1 同步阻塞IO

    如图1所示,用户线程通过系统调用read发起IO读操作,由用户空间转到内核空间。内核等到数据包到达后,然后将接收的数据拷贝到用户空间,完成read操作。

    用户线程使用同步阻塞IO模型的伪代码描述为:

    {

    read(socket, buffer);

    process(buffer);

    }

    即用户需要等待read将socket中的数据读取到buffer后,才继续处理接收的数据。整个IO请求的过程中,用户线程是被阻塞的,这导致用户在发起IO请求时,不能做任何事情,对CPU的资源利用率不够。

    二、同步非阻塞IO

    同步非阻塞IO是在同步阻塞IO的基础上,将socket设置为NONBLOCK。这样做用户线程可以在发起IO请求后可以立即返回。

     

    图2 同步非阻塞IO

    如图2所示,由于socket是非阻塞的方式,因此用户线程发起IO请求时立即返回。但并未读取到任何数据,用户线程需要不断地发起IO请求,直到数据到达后,才真正读取到数据,继续执行。

    用户线程使用同步非阻塞IO模型的伪代码描述为:

    {

    while(read(socket, buffer) != SUCCESS)

    ;

    process(buffer);

    }

    即用户需要不断地调用read,尝试读取socket中的数据,直到读取成功后,才继续处理接收的数据。整个IO请求的过程中,虽然用户线程每次发起IO请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源。一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO这一特性。

    三、IO多路复用

    IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。

    图3 多路分离函数select

    如图3所示,用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。

    从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

    用户线程使用select函数的伪代码描述为:

    {

    select(socket);

    while(1) {

    sockets = select();

    for(socket in sockets) {

    if(can_read(socket)) {

    read(socket, buffer);

    process(buffer);

    }

    }

    }

    }

    其中while循环前将socket添加到select监视中,然后在while内一直调用select获取被激活的socket,一旦socket可读,便调用read函数将socket中的数据读取出来。

    然而,使用select函数的优点并不仅限于此。虽然上述方式允许单线程内处理多个IO请求,但是每个IO请求的过程还是阻塞的(在select函数上阻塞),平均时间甚至比同步阻塞IO模型还要长。如果用户线程只注册自己感兴趣的socket或者IO请求,然后去做自己的事情,等到数据到来时再进行处理,则可以提高CPU的利用率。

    IO多路复用模型使用了Reactor设计模式实现了这一机制。

    图4 Reactor设计模式

    如图4所示,EventHandler抽象类表示IO事件处理器,它拥有IO文件句柄Handle(通过get_handle获取),以及对Handle的操作handle_event(读/写等)。继承于EventHandler的子类可以对事件处理器的行为进行定制。Reactor类用于管理EventHandler(注册、删除等),并使用handle_events实现事件循环,不断调用同步事件多路分离器(一般是内核)的多路分离函数select,只要某个文件句柄被激活(可读/写等),select就返回(阻塞),handle_events就会调用与文件句柄关联的事件处理器的handle_event进行相关操作。

    5 IO多路复用

    如图5所示,通过Reactor的方式,可以将用户线程轮询IO操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作(异步),而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时,则通知相应的用户线程(或执行用户线程的回调函数),执行handle_event进行数据读取、处理的工作。由于select函数是阻塞的,因此多路IO复用模型也被称为异步阻塞IO模型。注意,这里的所说的阻塞是指select函数执行时线程被阻塞,而不是指socket。一般在使用IO多路复用模型时,socket都是设置为NONBLOCK的,不过这并不会产生影响,因为用户发起IO请求时,数据已经到达了,用户线程一定不会被阻塞。

    用户线程使用IO多路复用模型的伪代码描述为:

    void UserEventHandler::handle_event() {

    if(can_read(socket)) {

    read(socket, buffer);

    process(buffer);

    }

    }

    {

    Reactor.register(new UserEventHandler(socket));

    }

    用户需要重写EventHandler的handle_event函数进行读取数据、处理数据的工作,用户线程只需要将自己的EventHandler注册到Reactor即可。Reactor中handle_events事件循环的伪代码大致如下。

    Reactor::handle_events() {

    while(1) {

    sockets = select();

    for(socket in sockets) {

    get_event_handler(socket).handle_event();

    }

    }

    }

    事件循环不断地调用select获取被激活的socket,然后根据获取socket对应的EventHandler,执行器handle_event函数即可。

    IO多路复用是最常使用的IO模型,但是其异步程度还不够“彻底”,因为它使用了会阻塞线程的select系统调用。因此IO多路复用只能称为异步阻塞IO,而非真正的异步IO。

    四、异步IO

    “真正”的异步IO需要操作系统更强的支持。在IO多路复用模型中,事件循环将文件句柄的状态事件通知给用户线程,由用户线程自行读取数据、处理数据。而在异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可。

    异步IO模型使用了Proactor设计模式实现了这一机制。

    图6 Proactor设计模式

    如图6,Proactor模式和Reactor模式在结构上比较相似,不过在用户(Client)使用方式上差别较大。Reactor模式中,用户线程通过向Reactor对象注册感兴趣的事件监听,然后事件触发时调用事件处理函数。而Proactor模式中,用户线程将AsynchronousOperation(读/写等)、Proactor以及操作完成时的CompletionHandler注册到AsynchronousOperationProcessor。AsynchronousOperationProcessor使用Facade模式提供了一组异步操作API(读/写等)供用户使用,当用户线程调用异步API后,便继续执行自己的任务。AsynchronousOperationProcessor 会开启独立的内核线程执行异步操作,实现真正的异步。当异步IO操作完成时,AsynchronousOperationProcessor将用户线程与AsynchronousOperation一起注册的Proactor和CompletionHandler取出,然后将CompletionHandler与IO操作的结果数据一起转发给Proactor,Proactor负责回调每一个异步操作的事件完成处理函数handle_event。虽然Proactor模式中每个异步操作都可以绑定一个Proactor对象,但是一般在操作系统中,Proactor被实现为Singleton模式,以便于集中化分发操作完成事件。

    7 异步IO

    如图7所示,异步IO模型中,用户线程直接使用内核提供的异步IO API发起read请求,且发起后立即返回,继续执行用户线程代码。不过此时用户线程已经将调用的AsynchronousOperation和CompletionHandler注册到内核,然后操作系统开启独立的内核线程去处理IO操作。当read请求的数据到达时,由内核负责读取socket中的数据,并写入用户指定的缓冲区中。最后内核将read的数据和用户线程注册的CompletionHandler分发给内部Proactor,Proactor将IO完成的信息通知给用户线程(一般通过调用用户线程注册的完成事件处理函数),完成异步IO。

    用户线程使用异步IO模型的伪代码描述为:

    void UserCompletionHandler::handle_event(buffer) {

    process(buffer);

    }

    {

    aio_read(socket, new UserCompletionHandler);

    }

    用户需要重写CompletionHandler的handle_event函数进行处理数据的工作,参数buffer表示Proactor已经准备好的数据,用户线程直接调用内核提供的异步IO API,并将重写的CompletionHandler注册即可。

    相比于IO多路复用模型,异步IO并不十分常用,不少高性能并发服务程序使用IO多路复用模型+多线程任务处理的架构基本可以满足需求。况且目前操作系统对异步IO的支持并非特别完善,更多的是采用IO多路复用模型模拟异步IO的方式(IO事件触发时不直接通知用户线程,而是将数据读写完毕后放到用户指定的缓冲区中)。Java7之后已经支持了异步IO,感兴趣的读者可以尝试使用。

    展开全文
  • Linux IO 多路复用理解

    千次阅读 2018-09-14 14:41:26
    1、复用的意思时不用每个进程/线程来操控单独的一个IO,只需一个进程/线程来操控多个IO. 2、内核空间不能直接解引用用户态的指针。     select 与 poll select 传递 fd_set* 的指针,仍然需要将fd_set从...

    1、复用的意思时不用每个进程/线程来操控单独的一个IO,只需一个进程/线程来操控多个IO.

    2、内核空间不能直接解引用用户态的指针。

     

     

    select 与 poll

    select 传递 fd_set* 的指针,仍然需要将fd_set从用户态拷贝到内核态。poll 传递的 pollfd* 指针一样需要从用户态拷贝所有 pollfd 到内核态。( copy_from_user 方法)

    fd_set 只是一个包装成 struct 的数组,就是一个 1024bit 的bitmap 而已。由于传入时需要用来标记监控的文件描述符,返回时也要用其标记是否有事件发生,所以每次调用前需要初始化。fd_set 是一个静态的数组,所以 select 支持的文件描述符数量有限,而 poll 传入的相当于一个动态数组(指针 + 元素个数),所以支持的文件描述符数量没有限制。

    pollfd 将文件描述符和事件用不同的字段来分离表示,绑定到一个结构体当中。传入时用 events 表示监控的事件,传出时用 revents 表示返回的事件,所以不用 select 一样每次调用初始化一下。

    struct pollfd
      {
        int fd;			/* File descriptor to poll.  */
        short int events;		/* Types of events poller cares about.  */
        short int revents;		/* Types of events that actually occurred.  */
      };

    select 和 polled 的问题在于感兴趣的文件描述符一直由用户态记录,而 epoll 则交个内核来管理了。select 和 poll 传入到内核的结构,内核需要遍历所有传入的文件描述符,依次检查每个文件描述符是否有监控的事件发生。在检查之前,会将当前进程加入到文件描述符fd 的 wait queue 当中。

    1、在 select 和 poll 调用之前,如果有事件发生,网卡通过中断和中断联合(interrupt coalescing)来通知内核读写,此时由于检测到事件,内核将 fd_set/ pollfd 结果拷贝到用户态,同时用户态调用返回。用户态遍历所有监控的文件描述符,检查返回结果,作对应处理

    2、如果在 select 和 poll 调用之前,如果没有事件发生。select/poll将阻塞,进程休眠,知道超时或者被中断。新的事件来临,内核按文件描述符 fd 为依托来处理。此时该 fd 上的 wait queue 会被依次唤醒。通常的实现下,select/poll 并不知道是自己是被哪个 fd 唤醒,所以又需要再去遍历一遍所有传入的fd,然后同 1 一样在用户态返回和处理。

    epoll

    epoll 将所有需要监控的文件描述符同一交给内核来管理,所以不需要在每次调用时拷贝。步骤细化,涉及到 3 个调用。

    typedef union epoll_data
    {
      void *ptr;
      int fd;
      uint32_t u32;
      uint64_t u64;
    } epoll_data_t;
    
    struct epoll_event
    {
      uint32_t events;	/* Epoll events */
      epoll_data_t data;	/* User data variable */
    } __EPOLL_PACKED;
    
    
    /* Creates an epoll instance.  Returns an fd for the new instance.
       The "size" parameter is a hint specifying the number of file
       descriptors to be associated with the new instance.  The fd
       returned by epoll_create() should be closed with close().  */
    extern int epoll_create (int __size) __THROW;
    
    /* Manipulate an epoll instance "epfd". Returns 0 in case of success,
       -1 in case of error ( the "errno" variable will contain the
       specific error code ) The "op" parameter is one of the EPOLL_CTL_*
       constants defined above. The "fd" parameter is the target of the
       operation. The "event" parameter describes which events the caller
       is interested in and any associated user data.  */
    extern int epoll_ctl (int __epfd, int __op, int __fd,
    		      struct epoll_event *__event) __THROW;
    
    
    /* Wait for events on an epoll instance "epfd". Returns the number of
       triggered events returned in "events" buffer. Or -1 in case of
       error with the "errno" variable set to the specific error code. The
       "events" parameter is a buffer that will contain triggered
       events. The "maxevents" is the maximum number of events to be
       returned ( usually size of "events" ). The "timeout" parameter
       specifies the maximum wait time in milliseconds (-1 == infinite).
    
       This function is a cancellation point and therefore not marked with
       __THROW.  */
    extern int epoll_wait (int __epfd, struct epoll_event *__events,
    		       int __maxevents, int __timeout);

    epoll 传入的和 poll 相似,也是一个动态数组,所以数量也没有限制。内核使用红黑树来快速的添加删除需要监控的文件描述符,同时基于事件驱动,文件描述符 fd 有事件发生时,内核的回调函数会将该 fd 加入到内核维护的 ready list 内。所以调用 epoll_ctl 时,内核只需要去检查 ready list 并拷贝结果到用户态即可。及时 epoll_ctl 调用时,ready list 为空,进程休眠。在进程挂在 fd 上的 wait queue 被唤醒之前,内核已经将事件添加到 ready list 了,所以这个时候仍然只要简单的将 ready list 的结果返回给用户态而已。就是说,在用户态,返回的结果只包含产生了事件的文件描述符。

    最后,select 和 poll 实际上是水平触发模式,而 epoll 不仅支持水平触发,而且可以设置为边沿触发。

    实践经历

    无论对端正常还是异常关闭,在本端调用 close 之前,水平触发将一直有 EPOLLIN 事件。因为 TCP 允许只关闭发送或者接收,通过 shutdown 实现。close 调用将同时关闭发送和接收。对端可能 FIN 再 RST,或者直接RST,或者只有FIN。

    1、对端close 发送 FIN,如果本段继续发送,对端将发送 RST.

    2、接收缓冲区还有数据时,linger 的数值是缺省的 0,此时 close 将直接发送 RST 给对端。( 版本 Linux localhost.localdomain 3.10.0-514.el7.x86_64,不知道是否和版本有关系)。发送缓冲区有数据,开关参数 l_onoff 非 0,超时参数 l_linger 为 0,此时发送缓冲区还有数据,调用 close 也是直接发送 RST 对对端。

     

    1、如果对端直接 RST 关闭,本端的第一次事件为 EPOLLIN + EPOLLERR + EPOLLHUP (19),此时 read 返回 -1,错误码为 Connection reset by peer。之后事件为 EPOLLIN + EPOLLHUP (11),read 返回 0.

    2、对端 FIN 之后再 RST 或正常关闭,本端的事件一直是 EPOLLIN,read 返回 0.

    close 之后,自动从内核的监控数据结构中去除,再无事件。而 poll 由于兴趣文件描述符需要自己维护,所以需要手动从 pollfd 列表中去除,select 也需要从 fd_set 中去除。否则再次 poll 和 select 获取结果时,poll 直接返回(不阻塞),对应的 pollfd 的事件为 POLLNVAL,而 select 直接返回(不阻塞)-1,错误码为 EBADF,文件描述符错误。

    对端正常关闭,本端未调用 close 之前,read 将一直返回 0,所以用 read 返回判断对端是否已经关闭。EPOLLRDHUP 需要注册作为输入,在输出中才能看到。

    本端 close 之后,read,write 返回错误,Bad file descripter。对端关闭(本端无法知道是关闭读还是同时都关闭了)。通常对端同时都关闭了,本端第一次 write 可以成功,不过对端返回 RST,之后本端再 write 将收到 SIGPIPE 信号,如果不处理该信号,默认的操作是终止进程。捕获或者忽略该信号,从中断上下文(信号相当于软中断)回到进程上下文(中断,进程,内核三种),write 将立马返回错误 Broken Pipe.

     

    展开全文
  • IO多路复用

    千次阅读 2019-06-23 14:48:40
    IO多路复用 同步IO、异步IOIO多路复用 IO两个阶段 IO过程分两阶段: 数据准备阶段。从设备读取数据到内核空间的缓冲区 内核空间复制回用户空间进程缓冲区阶段 发生IO的时候: 内核从IO设备读数据(例如:淘米...
  • 什么是IO多路复用,理解IO多路复用

    千次阅读 多人点赞 2016-12-07 09:16:00
    一、什么是socket? 我们都知道unix(like)世界里,一切皆文件,而文件是什么呢?文件就是一串二进制流而已,不管socket,还是FIFO、管道、终端,对我们来说,一切都是文件,一切都是流。在信息 交换的过程中,我们都...
  • IO 多路复用介绍

    千次阅读 2018-03-14 14:44:48
    原文地址 https://www.cnblogs.com/sunhao96/p/7873842.htmlIO模式 对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。...
  • IO 多路复用是什么意思?

    千次阅读 2019-01-03 00:36:26
    1 IO 多路复用是什么意思? - 罗志宇的回答 - 知乎 https://www.zhihu.com/question/32163005/answer/55772739 这个还是很好说清楚的。 假设你是一个机场的空管, 你需要管理到你机场的所有的航线, 包括进港,...
  • 学习之前,我们先来了解一下IO模型: ①同步阻塞IO(BlockingIO):即传统的IO模型。 ②同步非阻塞IO(Non-blockingIO):默认创建的socket都是阻塞的,非阻塞IO要求... ③多路复用IOIOMultiplexing):即...
  • Netty教程-IO多路复用机制详解

    万次阅读 2017-04-20 18:33:51
    高性能IO模型浅析   服务器端编程经常需要构造高性能的IO模型,常见的IO模型有四种: (1)同步阻塞IO(Blocking IO):即传统的IO模型。 (2)同步非阻塞IO(Non-blocking IO):默认创建的socket都是阻塞...
  • STM32F4——GPIO基本应用及复用

    千次阅读 2015-07-27 14:44:52
    IO基本应用 IO基本结构:  针对STM32F407有7组IO,分别为GPIOA~GPIOG,每组IO有16个IO口,则有112个IO口。其中IO口的基本结构如下:  工作方式:  STM32F4工作模式有8种,其中4中输入模式,4种输出模式,分别...
  • Java IO多路复用技术详解

    千次阅读 2017-04-28 09:49:58
    服务器端编程经常需要构造高性能的IO模型,常见的IO模型有四种: (1)同步阻塞IO(Blocking IO):即传统的IO模型。 (2)同步非阻塞IO(Non-blocking IO):默认创建的...(3)IO多路复用IO Multiplexi
  • 文章目录IOTCP通信阻塞IO非阻塞IOIO多路复用 IO 即Input Stream与Output Stream TCP通信 在介绍IO之前我们首先我们先了解一下TCP协议,对于TCP通信来说,每个TCP的scoket内核里面都有一个接受与发送缓冲区。 数据在...
  • 【stm32f407】IO引脚复用和映射

    万次阅读 2017-06-05 15:06:57
    一. 什么叫管脚复用 STM32F4有很多的内置外设,这些外设的外部引脚都是与GPIO复用的。...该复用器一次只允许一个外设的复用功能(AF)连接到对应的IO口。这样可以确保共用同一个IO引脚的外设之间不会发生冲突。
  • STM32的IO复用的一些说明

    千次阅读 2017-02-15 14:36:31
    http://www.51hei.com/bbs/dpj-40985-1.html
  • 什么叫做引脚复用

    千次阅读 2019-02-25 14:30:52
    问:什么叫做引脚复用? 答:STM32芯片内部有很多外设,这些外设的引脚都是与IO复用的。也就是说,一个IO口如果可以复用为内置外设的功能引脚,那么当这个IO口作为内外设使用的时候,就叫做复用。 ...
  • IO多路复用与异步IO区别

    千次阅读 2020-02-28 10:43:01
    一、IO多路复用(Reactor) 图3 多路分离函数select 如图3所示, 用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程...
  •  常见的IO模型有阻塞、非阻塞、IO多路复用,异步。以一个生动形象的例子来说明这四个概念。周末我和女友去逛街,中午饿了,我们准备去吃饭。周末人多,吃饭需要排队,我和女友有以下几种方案:  (1)我和女友点...
  • IO多路复用—由Redis的IO多路复用yinch

    万次阅读 多人点赞 2019-05-08 15:41:32
    linux IO多路复用有epoll, poll, select,epoll性能比其他几者要好。 名词比较绕口,理解涵义就好。一个epoll场景:一个酒吧服务员(一个线程),前面趴了一群醉汉,突然一个吼一声“倒酒”(事件),你小跑过去...
  • 常见JAVA IO/NIO模型

    万次阅读 2019-07-09 12:33:28
    我们常见的IO模型有:阻塞 IO 模型、非阻塞 IO 模型、多路复用 IO 模型、信号驱动 IO 模型、异步 IO 模型;下面我们就简单介绍一下以上IO模型。 1、阻塞 IO 模型 最传统的一种IO 模型,即在读写数据过程中会发生...
1 2 3 4 5 ... 20
收藏数 110,727
精华内容 44,290
关键字:

io复用