IO模型:IO多路复用

I/O Multiplexing

I/O多路复用:同时监视多个I/O流(如Socket)的状态并处理可读、可写和异常事件。它允许在单个线程中同时处理多个I/O操作

select

int select(int nfds, fd_set*readfds, fd_set*writefds, fd_set*exceptfds, struct timeval*timeout)
  • nfds:要监听的文件描述符总数;
  • fd_set:要监听的文件描述符集合; 分为:readfds、writefds和exceptfds:指可读、可写、异常等事件对应的文件描述符集合 通过这三个参数传入应用程序感兴趣的fd;返回时,内核修改对应的就绪文件描述符来告知应用程序,哪些fd就绪了;
  • timeout:函数超时时间,当设置为-1,则为非阻塞,立即返回;

poll

int poll(struct pollfd*fds, nfds_t nfds, int timeout)
  • nfds:要监听的文件描述符总数;
  • fds:一个结构体,指定应用程序要监听的文件描述符;包括可读、可写、异常等 与select类似;
  • timeout:函数超时时间,当设置为-1,则为非阻塞,立即返回;

epoll

epoll函数为Linux特有的IO复用函数 epoll接口的核心是在内核创建一个epoll数据结构:一个包含两个列表的容器; 能够处理的最大请求,取决与系统可打开的文件描述符个数;:

cat /proc/sys/fs/file-max

1. epoll_create

创建epoll实例(eventpoll对象),返回一个文件描述符指向创建的epoll实例(一个包含两个列表的socket容器)

实例:

  • 兴趣列表(红黑树):带有事件的socket集合;当执行epoll_ctl()时,将对应的socket加入到兴趣列表;
    • 兴趣列表是一个红黑树,可以高效插入、查找、删除的复杂度为O(logN)
  • 就绪列表(双向链表):事件就绪的socket集合;由内核在socket事件触发就绪时,将其加入到该集合内;
int epoll_create(int size);
  • size:需要监听的socket数量;
  • size内核会动态调整,但在调用时,只需要传递大于0的一个数;

此fd用于后续所有对epoll接口的调用,不需要时则删除此fd,内核则会销毁epoll实例,释放所有关联的资源;

2. epoll_ctl

操作已经创建完成的epoll实例中的节点:

  • 添加:添加Socket到红黑树中;
  • 修改:遍历红黑树,找到Socket,修改监听事件;
  • 删除:遍历红黑树,找到socket删除;

当socket触发的事件是关注的事件,则由内核将fd添加到就绪队列;

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • epfd:指向epoll实例的fd,由create创建;
  • op:需要对fd执行的动作:
    • EPOLL_CTL_ADD:将此fd添加进红黑树,并绑定event;
    • EPOLL_CTL_MOD:将event绑定到fd上;
    • EPOLL_CTL_DEL:注销、删除fd,此时event参数被忽略,可以为null;
  • fd:需要执行以上动作的文件描述符(红黑树中的一颗节点)
  • epoll_event:想要关联到fd上的事件;是一个位掩码,可以绑定多个事件:
    EPOLLIN | EPOLLOUT
    • EPOLLIN:对fd绑定读事件,可以执行read()系统调用;
    • EPOLLOUT:对fd绑写读事件,可以执行write()系统调用;

3. epoll_wait

等待事件就绪,可以是阻塞的,也可以立即返回; 将就绪队列的触发的事件拷贝到用户空间;

拿到就绪事件,遍历,执行就绪事件中的回调函数,即可对socket上绑定的事件进行处理(读、写、连接、断开连接事件等);

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • epfd:监听等待的epoll实例;
  • events:是一个epoll_event空列表,有就绪fd时,内核会将对应就绪的事件,放入数组中,返回后,用户线程进行处理;只包含就绪事件,效率高
  • maxevents:大于等于events的大小;(因为create的size不限制大小,这里max也无意义了)
  • timeout:超时时间;
    • timeout=-1,永久阻塞,直到有时间返回;
    • timeout=0,非阻塞,立即返回,无论有无就绪IO;
    • timeout>0,阻塞指定时间后返回;

水平触发LT、边沿触发ET

数字电路中由电平、边沿触发

  • 电平触发(水平触发):当处于高电平时,则保持触发,处理完成降为低电平;
  • 边沿触发:只有从低电平到高电平变化的瞬间,才会输出,输出完成,不会再次输出;

epoll中则是:

  • 水平触发:数据未处理,则下次调用wait,仍然可以返回未处理的数据;容错高
  • 边沿触发:只会返回一次,无论是否处理;效率高

epoll实现

// 创建epoll
int epfd = epoll_create(1000);
// 添加事件
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &listen_event);
// 循环监听
while(1){
    int active_cnt = epoll_wait(epfd, events, 1000, -1);
    for(i=0; i<active_cnt; i++){
        if (event[i].data.fd & EPOLLIN){
            // read
        ) else if (event[i].data.fd & EPOLLOUT) {
            // write
        }
    }
}

对比select/poll/epoll

select

  • 监听的socket fd数量受限;默认1024;
  • fd_set集合有三个:可读、可写、异常集合,需要不断在用户和内核间复制;随着socket数量增加,效率下降;
  • select每次返回全量fd_set,需要用户再遍历,找到就绪的fd,进行处理,O(n)复杂度;
  • 处理完需要重置fd事件,重新设置需要监听的事件,才能再次select调用;
  • 仅工作在水平触发LT模式;未处理的事件,下次依然返回;

poll

  • 监听的socket fd数量不受限;取决与系统可打开的文件描述符个数:
    cat /proc/sys/fs/file-max
  • fd_set集合只有一个,仍需要不断在用户和内核间复制;随着socket数量增加,效率下降;
  • poll每次返回全量fd_set,需要用户再遍历,找到就绪的fd,进行处理,O(n)复杂度;
  • 仅工作在水平触发LT模式;未处理的事件,下次依然返回;

epoll

  • 监听的socket fd数量不受限;取决与系统可打开的文件描述符个数:
    cat /proc/sys/fs/file-max
  • 不使用fd_set,而是直接在内核创建epoll红黑树实例,只有事件epoll_wait返回时,将就绪fd从内核复制到用户空间,每次仅复制一次;
  • epoll只关注就绪事件,处理的时间复杂度为O(1)
  • 可选边沿触发ET,更高效;