本文主要从缺点和一步步改善为角度说明IO多路复用中select,poll, 以及epoll方法的不同。
I、select
1.1、select有三个主要缺点:
1)每次调用select,都需要将fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;
2)同时每次调用select都需要在内核遍历所有的fd,这个开销在fd很多时也很大;
3)select支持的文件描述符数量太少了,默认是1024(可以通过修改宏,但是会影响性能);
II、poll(关键词,构造一个pollfd结构的数组)
poll与select一样都需要大量的复制(用户态与内核态之间的大量复制),其主要优点在于没有最大连接数的限制,因为其实基于链表存储的。
III、epoll
3.1、对于fd拷贝问题的解决:
对于I/O多路复用,有两件事必须要做:一是准备好需要监控的fd集合;二是探测并返回fd集合中哪些已经OK了。
对于select与poll,每次调用select或poll都在重复地准备(集中处理)整个需要监控的fd集合。然而对于频繁调用的select与poll而言,fd集合的变化频率要低得多,没必要每次都重新准备(集中处理)整个fd集合。
于是epoll引入了epoll_ctl
系统调用,将高频调用的epoll_wait
和低频的epoll_ctl
隔离开。同时,epoll_ctl
通过(add,mod,del)三个操作来分散对需要监控的fd集合的修改(使用红黑树监控fd集合,确定增删改操作),做到了有变化才变更,将select或poll的集中处理变成了分散处理。
同时,epoll通过内核与用户空间共享同一块内存来解决fd拷贝问题。mmap(内存映射)将用户空间的一块地址和内核空间的一块地址同时映射到相同的一块物理内存地址,使得这块地址对内核和用户都可见,减少用户态和内核态之间的数据交换。
3.2对于全部遍历fd集合的解决:
epoll能按需遍历就绪的fd集合(只遍历已经就绪的fd)。
为此,epoll引入了一个中间层、一个双向链表(ready_list),一个单独的睡眠队列(single_epoll_wait_list);
epoll在中间层为每个监控的socket准备了一个单独的回调函数epoll_callback_sk,而对于select/poll,所有socket都共用一个相同的回调函数。正是这个单独的回调函数使得每个socket都能单独处理自身,当自己就绪的时候就将自身socket挂入epoll的ready_list。
同时,epoll引入了一个睡眠队列single_epoll_wait_list,分割了两类睡眠(将等待epoll的休眠进程与其他休眠进程分开)。process不再睡眠在所有的socket的睡眠队列上,而是睡眠在epoll的单独睡眠队列上,在等待“任意一个socket可读就绪”事件。
3.3 epoll的边沿触发与水平触发:
1、 水平触发:
LT(level triggered) 是默认/缺省的工作方式,同时支持 block和no_block socket。这种工作方式下,内核会通知你一个fd是否就绪,然后才可以对这个就绪的fd进行I/O操作。就算你没有任何操作,系统还是会继续提示fd已经就绪,不过这种工作方式出错会比较小,传统的select/poll就是这种工作方式的代表。
2、边沿触发ET:
ET(edge-triggered) 是高速工作方式,仅支持no_block socket,这种工作方式下,当fd从未就绪变为就绪时,内核会通知fd已经就绪,并且内核认为你知道该fd已经就绪,不会再次通知了,除非因为某些操作导致fd就绪状态发生变化。如果一直不对这个fd进行I/O操作,导致fd变为未就绪时,内核同样不会发送更多的通知,因为only once。所以这种方式下,出错率比较高,需要增加一些检测程序。
3、LT可以理解为水平触发,只要有数据可以读,不管怎样都会通知。而ET为边缘触发,只有状态发生变化时才会通知,可以理解为电平变化。
IV、 epoll 与 select的选取
表面上看epoll
的性能最好,但是在只有少量连接活跃的情况下,select
和poll
可能比epoll
好,因为epoll
需要很多不同的回调函数。
【参考】
[1] 大话 Select、Poll、Epoll
欢迎转载,转载请注明出处wenmingxing select & poll & epoll
网友评论