上一篇文章讲到了Unix的I/O模型,以及在java中的具体实现,其中在java中我们最为关注的就是 I/O 复用了,这篇主要总结下I/O多路复用器。
Linux的内核将所有外部设备都可以看做一个文件来操作。那么我们对与外部设备的操作都可以看做对文件进行操作。我们对一个文件的读写,都通过调用内核提供的系统调用;内核给我们返回一个filede scriptor(fd,文件描述符)。而对一个socket的读写也会有相应的描述符,称为socketfd(socket描述符)。描述符就是一个数字,指向内核中一个结构体(文件路径,数据区,等一些属性)。那么我们的应用程序对文件的读写就通过对描述符的读写完成。
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
int poll(struct pollfd *fdarray, unsigned nfds,int timeout);
fdarray是一个链表的结构,数据结构如下:
struct pollfd{
int fd; // 需要关注的文件描述符
short events;// 关注的事件类型
short revents;// 发生的事件
}
poll相对于select的改进主要是在这个结构体上,由数组到链表,解决了描述符有上限的问题,并且将结果和参数分离,只需要重置revents就可以了,而不需要重新申请整个结构;
经过select->poll的改进,还剩下select的2个缺点、
epoll在poll和select的缺点之上做了重大改进,但是逻辑也更为复杂。它有三个函数:
int epoll_create(int size)
直接在内核创建保存文件描述符的空间。epoll的结构采用红黑树保存,epoll_create可视为初始化root节点。调用epoll_create所创建的文件描述符保存空间称为“epoll例程”。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):
与select函数类似,等待文件描述符发生变化;
存储FD的数据结构直接改由在内核的维护,我们便不再需要重复多次从用户态copy到内核态,只需要实时维护发生变化的FD到内核就可以了;用events来存储发生事件的fd,则无需再遍历整个被关注的描述符集合。
epoll对文件描述符的操作有2种模式,默认模式是水平触发。
select | poll | epoll | |
---|---|---|---|
操作方式 | 遍历 | 遍历 | 回调 |
底层实现 | 数组 | 链表 | 红黑树 |
IO效率 | 每次调用都进行线性遍历,时间复杂度为O(n) | 每次调用都进行线性遍历,时间复杂度为O(n) | 事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面,时间复杂度O(1) |
最大连接数 | 1024(x86)或2048(x64) | 无上限 | 无上限 |
fd拷贝 | 每次调用select,都需要把fd集合从用户态拷贝到内核态 | 每次调用poll,都需要把fd集合从用户态拷贝到内核态 | 调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝 |