深入解析select、poll、epoll:Linux网络编程的三种IO模型

文章目录

  • IO模型的分类
  • 多路复用型IO的分类
    • select
      • select系统调用
      • 缺点
    • poll
      • poll系统调用
      • 缺点
    • epoll
      • epoll系统调用
      • epoll模型
      • 优点
      • LT模式与ET模式

IO模型的分类

大家都知道,一个完整的IO操作所花费的时间在计算机中是非常多的(速度非常慢),那么这些时间都花费在哪里呢?

IO = 等待数据就绪 + 数据拷贝

而等待数据就绪所花费的时间占了整个IO时间的99%,数据拷贝所花费的时间仅占1%。

IO模型分为同步IO和异步IO。

  • 同步IO:用户发起IO,用户阻塞或轮训的查看数据是否就绪,用户进行内核态到用户态的数据拷贝。
  • 异步IO:用户发起IO,数据是否就绪以及数据拷贝的过程全交给内核,当IO完成时内核通知用户。

同步IO分为阻塞IO、非阻塞IO、信号驱动型IO、多路复用型IO。

  • 阻塞IO:用户发起IO后,若数据还未就绪,该线程就一直阻塞等待着数据的就绪。
  • 非阻塞IO:用户发起IO后,若数据还未就绪,该线程不会阻塞,而是继续向后执行代码,所以需要配合循环来一直检查数据是否就绪,而这也叫做轮询。
  • 信号驱动型IO:用户发起IO后,若数据还未就绪,该线程不会阻塞,而是继续向后执行代码,由内核来检查数据是否就绪,当数据就绪时,内核通知用户。
  • 多路复用型IO:由一个线程来监视(等待数据就绪)多个fd,当某个fd上的数据就绪时,就通知用户。

多路复用型IO和前三者的区别就是,多路复用型IO能同时监视多个fd,从而大大提高了效率。

本篇讲述多路复用型IO。

多路复用型IO的分类

select

select系统调用

函数原型:

#include 
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

参数解析:

  • nfds:需要监视的文件描述符的最大值+1(告诉操作系统要查找的文件描述符的范围);

  • readfds、writefds、exceptfds:分别都是一个位图,表示要监视的某些fd的读事件、写事件、异常事件。它们都是输入输出型参数,输入时,将需要监视的文件描述符和事件添加到位图中,输出时,就是已经就绪的文件描述符和事件。

  • timeout:

    • 取值为NULL时,表示select阻塞式调用;
    • 取值为0时,表示select非阻塞式调用;
    • 取值>0时,表示在timeout时间内阻塞式调用。
  • 返回值:

    • >0时,表示已就绪的文件描述符的数量;
    • ==0时,表示timeout时间到期返回了;
    • <0时,出错返回。
  • fd_set:

    typedef long int __fd_mask;
    
    typedef struct
    {
        __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
    } fd_set;
    
  • struct timeval:

    struct timeval {
       long    tv_sec;         /* seconds */
       long    tv_usec;        /* microseconds */
    };
    struct timespec {
       long    tv_sec;         /* seconds */
       long    tv_nsec;        /* nanoseconds */
    };
    

还有一批系统调用用来控制fd_set类型的参数。

void FD_CLR(int fd, fd_set *set);// 将fd从set中移除
int  FD_ISSET(int fd, fd_set *set);// 判断fd是否在set中
void FD_SET(int fd, fd_set *set);// 将fd设置进set中
void FD_ZERO(fd_set *set);// 将set设置为0

缺点

  • 由于中间三个参数是输入输出型参数,所以函数每次返回时,原来设置的内容就都已经被覆盖掉了,所以需要重新设置。这就导致编码难度上升;
  • 每次调用select都会把fd_set从内核态拷贝到用户态,效率较低;
  • 由于采用位图结构,所以底层选用轮询的方法来检测文件描述符是否就绪,效率较低;
  • fd_set是一个固定的类型,那么单个线程能够监视的文件描述符的数量就是固定的(centos7.6上是1024个)。

poll

poll系统调用

函数原型:

#include 
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数解析:

  • fds:是一个指针(也可以说是一个数组),它里面存放着需要监视的fd以及事件(是一个输入输出型参数);

  • nfds:表示监视的fd的数量,也就是fds的长度;

  • timeout与select一样;

  • 返回值与select一样;

  • struct pollfd:

    struct pollfd {
       int   fd;         /* file descriptor */
       short events;     /* requested events */
       short revents;    /* returned events */
    };
    
    • fd:需要监视的文件描述符;
    • events:该fd上需要监视的事件(输入型参数);
    • revents:该fd上已经就绪的事件(输出型参数)。

    这两个事件也分别都是位图结构,通过宏来设置对应事件:

    事件 描述 是否可作为输入 是否可作为输出
    POLLIN 数据(包括普通数据和优先数据)可读
    POLLRDNORM 普通数据可读
    POLLRDBAND 优先级带数据可读(Linux不支持)
    POLLPRI 高优先级数据可读,比如TCP带外数据
    POLLOUT 数据(包括普通数据和优先数据)可写
    POLLWRNORM 普通数据可写
    POLLWRBAND 优先级带数据可写
    POLLRDHUP TCP连接被对方关闭,或者对方关闭了写操作,它由GNU引入
    POLLERR 错误
    POLLHUP 挂起。比如管道的写端被关闭后,读端描述符上将收到POLLHUP事件
    POLLNVAL 文件描述符没有打开

缺点

  • 每次调用都会将fds从内核态拷贝到用户态(因为填写了revents);
  • 底层也是采用轮询的方式来检测文件描述符是否就绪,效率较低。

epoll

epoll系统调用

epoll有最基础的三个系统调用。

函数原型:

#include 
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);
  • epoll_create用来创建一个epoll模型,返回创建好的epoll模型的句柄(是一个文件描述符)。参数size在旧版本中悲剧,在新版本(2.6.8及之后的版本)被用作一个提示,提示内核为epoll实例预留size个文件描述符,建议设置成大于0的数字。

  • epoll_ctl用来控制(增删改)epoll模型中文件描述符及其事件

    • epfd:epoll模型的句柄;

    • op:控制类型

      op 效果
      EPOLL_CTL_ADD 将文件描述符及其事件添加到epoll模型中
      EPOLL_CTL_MOD 改变文件描述符对应的事件
      EPOLL_CTL_DEL 将文件描述符及其事件从epoll模型中移除
    • struct epoll_event:

      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 */
      };
      
    • 返回值:调用成功返回0,调用失败返回-1,同时errno被设置;

  • epoll_wait:等待IO事件就绪

    • epfd:同上;
    • events:一个输出型参数,需要用户提前开辟好内存,当epoll_wait返回时,数组中就已经都是就绪的fd和事件了;
    • maxevents:指定本次返回的events的最大数量,通常设置为evetns数组的大小(不能大于epoll_create的参数);
    • timeout:同上;
    • 返回值:跟select、poll一样。

epoll模型

当调用epoll_create时,内核会创建一个eventpoll结构体。

struct eventpoll {
    /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
    struct rb_root  rbr;
    /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
    struct list_head rdlist;
};

也就是创建一颗红黑树,而后每次通过epoll_ctl添加进来的fd和事件都会组织成一个结构体挂载到这颗红黑树上。

struct epitem{
    struct rb_node  rbn;//红黑树节点
    struct list_head    rdllink;//双向链表节点
    struct epoll_filefd  ffd;  //事件句柄信息
    struct eventpoll *ep;    //指向其所属的eventpoll对象
    struct epoll_event event; //期待发生的事件类型
}

epoll_ctl中的ADD就是往红黑树中添加结点、DEL就是删除红黑树中的结点、MOD就是修改红黑树中结点内部的event。

每个添加进来的事件都会与设备驱动建立回调关系,当事件触发时就会调用回调函数将其添加到就绪队列中

深入解析select、poll、epoll:Linux网络编程的三种IO模型_第1张图片

每个结点是可能同时在多个数据结构中的!

红黑树中就绪的结点同时会被组织成一个就绪队列,epoll_wait返回的就是该队列。

优点

  • 跟poll一样,解决了select中fd有上限的问题;
  • 解决了select、poll中的轮询遍历的问题;
  • 减少了用户态向内核态的拷贝(每次调用select、poll都会拷贝,而epoll仅在合适的时候用epoll_ctl添加结点)

LT模式与ET模式

  • LT(Level Triggered)水平触发模式:只要当fd上有数据时,每次调用epoll_wait都会返回该fd;
  • ET(Edge Triggered)边缘触发模式:只有当fd上的数据发生变化时(从零到有,从少到多),调用epoll_wait才会返回该fd。

select、poll、epoll都是默认处于LT模式,而epoll可以选择ET模式。

假设有一种场景:第一次从fd上读数据,没有读全,需要读第二次,如果处于LT模式下,第二次调用epoll_wait是能够继续从该fd上读数据的,而如果处于ET模式下,第二次调用epoll_wait就不能够从该fd上继续读数据了。这时就会出现问题,所以需要程序员能够一次性读全数据,无论处于LT还是ET,程序员都能够选择一次性读全数据,但是处于ET模式下,程序员就必须要一次性读全数据。

一次性读全数据的优势:能够更快速的将数据从内核拷贝到用户,从而能够拥有更大的TCP窗口,提高底层的数据发送效率,提高吞吐量。

你可能感兴趣的:(linux,网络,运维)