• I/O多路复用


    概念引入

    I/O多路复用的本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。

    Linux中基于socket的通信本质也是一种I/O,使用socket()函数创建的套接字默认都是阻塞的,这意味着当sockets API的调用不能立即完成时,线程一直处于等待状态,直到操作完成获得结果或者超时出错。会引起阻塞的socket API分为以下四种:

    •输入操作:recv()、recvfrom()。以阻塞套接字为参数调用该函数接收数据时,如果套接字缓冲区内没有数据可读,则调用线程在数据到来前一直睡眠。

    •输出操作:send()、sendto()。以阻塞套接字为参数调用该函数发送数据时,如果套接字缓冲区没有可用空间,线程会一直睡眠,直到有空间。

    •接受连接:accept()。以阻塞套接字为参数调用该函数,等待接受对方的连接请求。如果此时没有连接请求,线程就会进入睡眠状态。

    •外出连接:connect()。对于TCP连接,客户端以阻塞套接字为参数,调用该函数向服务器发起连接。该函数在收到服务器的应答前,不会返回。这意味着TCP连接总会等待至少服务器的一次往返时间。

    使用阻塞模式的套接字编写网络程序比较简单,容易实现。但是在服务器端,通常要处理大量的套接字通信请求,如果线程阻塞于上述的某一个输入或输出调用时,将无法处理其他任何运算或响应其他网络请求,这么做无疑是十分低效的,当然可以采用多线程,但大量的线程占用很大的内存空间,并且线程切换会带来很大的开销。而I/O多路复用模型能处理多个connection的优点就使其能支持更多的并发连接请求。

    Linux支持I/O多路复用的系统调用有select、poll、epoll,这些调用都是内核级别的。但select、poll、epoll本质上都是同步I/O,先是block住等待就绪的socket,再block住将数据从内核拷贝到用户内存空间。

    2.1 select详解

    Linux提供的select相关函数接口如下:

    #include <sys/select.h>

    #include <sys/time.h>

    int select(int max_fd, fd_set *readset, fd_set *writeset, fd_set *exceptset, struct timeval *timeout)

    FD_ZERO(int fd, fd_set* fds)   //清空集合

    FD_SET(int fd, fd_set* fds)    //将给定的描述符加入集合

    FD_ISSET(int fd, fd_set* fds)  //判断指定描述符是否在集合中

    FD_CLR(int fd, fd_set* fds)    //将给定的描述符从文件中删除  

    1.select函数的返回值就绪描述符的数目,超时时返回0,出错返回-1。

    2.第一个参数max_fd指待测试的fd个数,它的值是待测试的最大文件描述符加1,文件描述符从0开始到max_fd-1都将被测试。

    3.中间三个参数readset、writeset和exceptset指定要让内核测试读、写和异常条件的fd集合,如果不需要测试的可以设置为NULL。

    select的缺点:

    1.单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;(在linux内核头文件中,有这样的定义:#define __FD_SETSIZE    1024)

    2.内核/用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销;

    3.select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;

    4.select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。

    2.2 poll详解

    poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。

    Linux提供的poll函数接口如下:

    #include <poll.h>

    int poll(struct pollfd fds[], nfds_t nfds, int timeout);

    typedef struct pollfd {

            int fd;                         //需要被检测或选择的文件描述符

            short events;                   //对文件描述符fd上感兴趣的事件

            short revents;                  //文件描述符fd上当前实际发生的事件*/

    } pollfd_t;

    1.poll()函数返回fds集合中就绪的读、写,或出错的描述符数量,返回0表示超时,返回-1表示出错;

    2.fds是一个struct pollfd类型的数组,用于存放需要检测其状态的socket描述符,并且调用poll函数之后fds数组不会被清空;

    3.nfds记录数组fds中描述符的总数量;

    4.timeout是调用poll函数阻塞的超时时间,单位毫秒;

    5.一个pollfd结构体表示一个被监视的文件描述符,通过传递fds[]指示poll()监视多个文件描述符。其中,结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域,结构体的revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。events域中请求的任何事件都可能在revents域中返回。

    合法的事件如下:

    POLLIN            有数据可读

    POLLRDNORM        有普通数据可读

    POLLRDBAND        有优先数据可读

    POLLPRI           有紧迫数据可读

    POLLOUT           写数据不会导致阻塞

    POLLWRNORM        写普通数据不会导致阻塞       

    POLLWRBAND              写优先数据不会导致阻塞    

    POLLMSGSIGPOLL        消息可用

    当需要监听多个事件时,使用POLLIN | POLLRDNORM设置events域;当poll调用之后检测某事件是否发生时,fds[i].revents & POLLIN进行判断。

    相比select模型,poll使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他三个缺点依然存在。

    拿select模型为例,假设我们的服务器需要支持100万的并发连接,则在__FD_SETSIZE为1024的情况下,则我们至少需要开辟1k个进程才能实现100万的并发连接。除了进程间上下文切换的时间消耗外,从内核/用户空间大量的无脑内存拷贝、数组轮询等,是系统难以承受的。因此,基于select模型的服务器程序,要达到10万级别的并发访问,是一个很难完成的任务。

    2.3 epoll详解

    epoll在Linux2.6内核正式提出,是基于事件驱动的I/O方式,相对于select和poll来说,epoll没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。优点如下:

    1.没有最大并发连接的限制,能打开的fd上限远大于1024(1G的内存能监听约10万个端口)

    2.采用回调的方式,效率提升。只有活跃可用的fd才会调用callback函数,也就是说epoll只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会远远高于select和poll。

    3.内存拷贝。使用mmap()文件映射内存来加速与内核空间的消息传递,减少复制开销。

    epoll对文件描述符的操作有两种模式:LT(level trigger,水平触发)和ET(edge trigger,边缘触发)。

    水平触发:默认工作模式,即当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序可以不立即处理该事件;下次调用epoll_wait时,会再次通知此事件。

    边缘触发:当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次通知此事件。(直到你做了某些操作导致该描述符变成未就绪状态了,也就是说边缘触发只在状态由未就绪变为就绪时通知一次)。

    ET模式很大程度上减少了epoll事件的触发次数,因此效率比LT模式下高。

    Linux中提供的epoll相关函数接口如下:

    #include <sys/epoll.h>

    int epoll_create(int size);

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

    int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

    1.epoll_create函数创建一个epoll句柄,参数size表明内核要监听的描述符数量。调用成功时返回一个epoll句柄描述符,失败时返回-1。

    2.epoll_ctl函数注册要监听的事件类型。四个参数解释如下:

      epfd表示epoll句柄;

      op表示fd操作类型:EPOLL_CTL_ADD(注册新的fd到epfd中),EPOLL_CTL_MOD(修改已注册的fd的监听事件),EPOLL_CTL_DEL(从epfd中删除一个fd)

     fd是要监听的描述符;

     event表示要监听的事件

    epoll_event结构体定义如下:

    struct epoll_event {

        __uint32_t events;  /* Epoll events */

        epoll_data_t data;  /* User data variable */

    };

    typedef union epoll_data {

        void *ptr;

        int fd;

        __uint32_t u32;

        __uint64_t u64;

    } epoll_data_t;

    3.epoll_wait函数等待事件的就绪,成功时返回就绪的事件数目,调用失败时返回-1,等待超时返回0。

     epfd是epoll句柄

      events表示从内核得到的就绪事件集合

      maxevents告诉内核events的大小

      timeout表示等待的超时事件

    转载地址:https://zhuanlan.zhihu.com/p/22834126  https://blog.csdn.net/davidsguo008/article/details/73556811

  • 相关阅读:
    JAVA基础——编程练习(二)
    JAVA基础——面向对象三大特性:封装、继承、多态
    JVM内存
    50. Pow(x, n) (JAVA)
    47. Permutations II (JAVA)
    46. Permutations (JAVA)
    45. Jump Game II (JAVA)
    43. Multiply Strings (JAVA)
    42. Trapping Rain Water (JAVA)
    41. First Missing Positive (JAVA)
  • 原文地址:https://www.cnblogs.com/marry215464/p/10533064.html
Copyright © 2020-2023  润新知