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,更高效;