摘要
IO多路复用是指用一个进程监控多个socket的IO事件,以便于在socket就绪时,执行对应的操作。
select
- 首先为需要监听的每个socket设置一个文件描述符,从而得到所有的socket的文件描述符数组;
- 根据文件描述符数组,使用一个bitmap记录需要监听的事件,例如文件描述符数组是[3, 5, 8],那么对应的bitmap是0001010010000...,bitmap长度为1024;
为什么要使用bitmap?因为二进制占用内存小,位运算速度快! 为什么最大长度是1024?因为进程文件描述符上限是1024,此外过长的bitmap会影响性能,就设置了1024作为最大限制,个人认为主要是历史遗留问题,select是20世纪的技术,那时的计算机能同时监视1024个socket已经完全够用了。
- 调用select函数,会将bitmap拷贝到内核,由内核循环遍历bitmap对应的需要监听的事件,当有事件触发时,将触发了事件的位置1,其他位置全部置零,将bitmap返回给用户进程。
- 用户进程拿到bitmap后,还需要再遍历一遍bitmap才能知道是哪个事件被触发了。
select性能已经不错了,因为需要从用户和内核态拷贝的数据是bitmap,非常小。主要的缺点是需要不断的遍历bitmap,时间复杂度高。且存在1024的容量限制。另外bitmap无法重用,需要不断初始化。
Poll
- 定义了一个结构体如下,使用结构体数组记录文件描述符以及对应的事件;
struct pollfd{
int fd; // 文件描述符
short events; // 注册的事件
short revents; // 事件是否发生的标志位
};
- 初始化,将所有要监听的事件以及该事件对应的文件描述符存入结构体数组中;
- 执行poll,将数组复制到内核,由内核遍历,监听事件是否发生,当有事件发生时,对应的revents置1;
- 遍历结构体数组,寻找触发事件的文件描述符,以便于执行相应操作。
仍然需要用户内核态之间的值拷贝,且这poll拷贝的数组远远大于bitmap;空间复杂度仍然是O(n)
Epoll
- 使用epoll_create在内核中创建event_poll,用于存储需要监听的socket,event_poll包括了三个字段:
- rdyList:已就绪socket的文件描述符链表
- rbr:监听中socket的文件描述符,使用红黑树存储的,以便于迅速的查找;
- wq:等待队列,当某个进程关注的事件为就绪时,会将该进程放入等待队列中
- 使用epoll_ctl添加或删除要监听的socket文件描述符在红黑树中,当Socket收到数据后,中断程序会操作eventpoll对象,而不是直接操作进程;
- 进程使用epoll_wait等待监听的socket就绪,该进程此时会阻塞挂起在event_poll的等待队列中,当socket就绪时,中断程序会一边将对应socket的引用添加至就绪队列,一边唤醒等待队列中的进程;