select、poll、epoll
前言
select、poll、epoll都是I/O多路复用的机制,目的是为了同时监听多个描述符。一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
以网络IO为例,当我们创建socket套接字并等待数据传输到来的时候,程序会被阻塞在recvfrom这里,直到有数据传输过来,该进程被唤醒,然后处理传输过来的数据。
然而在创建并维护多个socket套接字的时候,情况就变得复杂了起来。主要问题有两个:
- 操作系统如何知道网络数据对应于哪个socket?
- 如何同时监视多个socket的数据?
对于第一个问题,由于端口和socket是一一对应的关系,所以很容易由端口号找到对应的socket。
对于第二个问题,则是I/O多路复用技术需要解决的问题了。
select
select本质上是维护了一个保存有文件描述符fd的数组,当有socket上的数据需要处理的时候,进程被唤醒,这时候,进程是不知道哪个socket上是有数据的。那么只有一个办法,内核将该数组拷贝至用户区,程序对整个数组进行遍历,如果socket上有数据,那么接着处理对应的事件。
1 |
|
这种思路理解起来很简单,就是暴力遍历,所以操作的开销很大,出于效率的考量,才会规定select的最大监视数量,默认只能监视1024个socket。
poll
poll的本质和select没有太大差别,只是将保存文件描述符的数组变成了一个链表,这样解决了1024的上限问题,但是遍历的效率仍然很低。
1 | struct pollfd { |
epoll
epoll原理
为了解决上述的效率低下的问题,epoll登场了。
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
epoll的底层采用了一棵红黑树和一个双向链表。
其中红黑树代替了之前select的数组以及poll的链表,用来保存需要监视的socket。选择红黑树是因为它是一种自平衡二叉查找树,搜索、插入和删除时间复杂度都是O(log(N)),效率较好。
双向链表用来保存就绪的socket,即需要处理的事件。这样在epoll被触发的时候,直接返回该链表,用户程序不需要对监听的socket进行完整的遍历就可以知道哪些socket有待处理。
epoll操作
Linux中提供的epoll相关函数如下:
1 | int epoll_create(int size); |
epoll_create
用来创建一个epoll,初始化相关的数据结构。其中参数size
指定内核维护的队列大小,不过在2.6.8之后这个参数就没有实际价值了,因为内核维护一个动态的队列了。函数返回的是一个epoll
的fd,之后的事件操作通过这个epollfd进行。epoll_ctl
用来对epoll进行相关操作,其中epfd
是epoll_create
的返回值,op
表示操作的类型,fd
是待操作的文件描述符,event
是事件队列。epoll_wait
会阻塞直到事件发生,参数events
用来从内核得到事件的集合,maxevents
告知内核这个events有多大
epoll的ET和LT机制
- ET(Edge Triggered)边沿触发
类似于数字信号中的边沿触发模式,在数字信号中,信号从0向1跃变的时候,事件被触发。这里指的是每当有socket接收到数据的时候,epoll会通知应用程序处理该事件,之后不会再通知。所以应用程序必须在这次通知之后将事件处理完毕,否则之后不会再有机会处理事件。
- LT(Level Triggered)水平触发
还是以数字信号为例,当信号持续为1的时候,事件会一直被触发。在这里理解为,缓冲区只要有没被消耗完的数据,那么epoll会一直通知应用程序去处理。所以在LT工作模式下,事件可以不一次性处理完,分成几次去处理。但是LT模式增加了事件被触发的次数,因此效率较低。