I/O多路复用的技术

1. 事件驱动 select() 模型

  • 用法

  • select()函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定时间后才唤醒它

    1
    2
    3
    4
    5
    6
    7
    8
    #include <sys/select.h>
    #include <sys/time.h>

    // 返回值:若有就绪描述符,则返回就绪描述符数目;若超时则返回0,出错返回-1
    int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);

    //maxfdp1: 指定待测试的描述符个数,它的值是待测试的最大描述符加1
    //readset、writeset、exceptset:指定让内核测试读、写、异常条件的描述符
  • 描述符的就绪条件:

    • 可读条件:
    • 1.该套接字接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记的当前大小
    • 2.该连接的读半部关闭(即接收了FIN的TCP连接)
    • 3.该套接字是一个监听套接字且已完成的连接数不为0
    • 4.该套接字上有一个套接字错误待处理
    • 可写条件:
    • 1.该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的当前大小
    • 2.该连接的写半部关闭
    • 3.使用非阻塞式connect的套接字已建立连接,或者connect已经以失败告终
    • 4.该套接字上有一个套接字错误待处理
    • 异常条件:该套接字存在带外数据或者仍处于带外标记
  • 优点

    • 目前几乎在所有的平台上支持
  • 缺点

    • 1.每次调用 select(),都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大,同时每次调用 select() 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大。
    • 2.单个进程能够监视的文件描述符的数量存在最大限制,32位机默认是1024个,64位机默认是2048。可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低

2 事件驱动 poll() 模型

  • 用法

    • #include <poll.h>
    • int poll(struct pollfd *fds, nfds_t nfds, int timeout);
    • 对应参数 传入一个文件描述符结构体数组,个数,等待的时间
    • pollfd结构体
      1
      2
      3
      4
      5
      struct pollfd{
      int fd; //文件描述符
      short events; //需要等待的事件
      short revents; //实际发生的事件,操作系统回写
      };
  • 事件

  • 优点

    • 采用链表的方式替换原有fd_set数据结构,poll() 没有最大文件描述符数量的限制
  • 缺点

    • poll() 和 select() 同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

3 事件驱动 epoll() 模型

  • 引用 select、poll、epoll的原理与区别 来自 https://blog.csdn.net/nanxiaotao/article/details/90612404

  • 参考 epoll原理图解 https://blog.csdn.net/qq_35433716/article/details/85345907

  • 用法

    1
    2
    3
    4
    5
    #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 *event, int maxevents, int timeout);
  • epoll的两种工作方式:1.水平触发(LT)2.边缘触发(ET)

  • ET模式只支持非阻塞的读写:为了保证数据的完整性。

  • LT模式:若就绪的事件一次没有处理完要做的事件,就会一直去处理。即就会将没有处理完的事件继续放回到就绪队列之中(即那个内核中的链表),一直进行处理。

ET模式:就绪的事件只能处理一次,若没有处理完会在下次的其它事件就绪时再进行处理。而若以后再也没有就绪的事件,那么剩余的那部分数据也会随之而丢失

  • 优点
    • 本质的改进在于epoll采用基于事件的就绪通知方式
    • epoll也需要将文件描述符先拷贝进内存,但是它只做一次
    • epoll_create()就好像先在内核中开辟出一个固定大小的空的文件描述符集合,之后再将相应的文件描述符放进去或者从中将某一个文件描述符删掉。
    • 只关心“活跃”的链接,无需遍历全部描述符集合
    • 能够处理大量的链接请求(系统可以打开的文件数目)
    • 在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。
    • epoll(市场用的最多,如果单单连接不做任何事 1G内核内存可以支持10W连接)
    • epoll 现在是线程安全的
  • 缺点
    • 只有linux支持