这篇文章主要是承接上一篇 - 《Socket编程基础3 - 之I/O多路复用》未完结的内容。上一篇文章主要讲了select()
以及多路复用的概念,这一篇文章将详细讲解poll()
和epoll()
函数以及这3个函数的对比。本篇内容主要来自网络以及《Linux高性能服务器编程》。
poll()函数
select()
函数有一个问题,就是FD_SETSIZE
的大小有限,一般是1024。如果我们想要连接更多的socket,那么就需要修改内核(简单调整FD_SETSIZE
是不管用的),很容易造成意想不到的问题。poll()
函数解决了这个问题,poll()
函数支持65535个socket连接,可以满足大部分的需求。
函数API
poll()
函数的原型如下:
1 | int poll(struct pollfd *fds, nfds_t nfds, int timeout); |
函数成功返回正数,正数表示有多少个socket描述符就绪;fds
是struct pollfd
类型的指针,fds
就是我们需要关注的socket描述符,我们先来看看这个结构体是什么样的:
1 | struct pollfd { |
events
是我们请求的事件,revents
是根据请求返回的事件。请求事件就是我们需要观察是否发生的事件,如果发生了,那么返回事件将不再是0。图1是events
和revents
的汇总:
对于读,我们一般使用POLLIN
或者POLLRDNORM
;对于写,我们一般用POOLOUT
和POLLWRNORM
。哪些操作返回读事件哪些返回写事件?
- TCP/UDP所有数据都是普通数据;
- 读半部分关闭(比如收到对端的
FIN
),是普通数据;- Linux 2.6.17后,可以使用POLLRDHUP
来接收对方断开连接或者对方写半部分关闭。 - TCP连接错误,可以是普通数据也可以是
POLLERR
; - 有新连接一般都视为普通数据;
- 非阻塞
connect
完成被认为socket可写。
nfds
用于告诉内核我们有多少个fds
,timeout
指明多久没有响应就超时,设置-1
是一直等待,设置0
是立刻返回。
poll()函数的优缺点
poll()
其实和select()
类似,因此这里说的优点是相对于select()
的优点,而缺点是相对于epoll()
的。
优点:
- 前面说过,和
select()
相比,支持更多的socket描述符; - 请求和返回的事件是相互独立的,不需要自己做额外的操作区分这两者;
缺点:
- 和
select()
一样,需要将关注的所有socket描述符传给内核,然后内核处理好后再传回来,耗时; - 和
select()
一样,都需要遍历所有的socket描述符才知道哪些描述符就绪;对于少量活跃socket的情况,时间复杂度依然是O(n);
epoll()函数
epoll()
函数相对于select()
和poll()
而言,做出了重大的改进。epoll()
不仅可以支持无上限的socket描述符(实际数量和内存大小有关),而且仅需要O(1)的时间就可以找到就绪的socket描述符。因此在很多情况下,epoll()
的效率都是远远高于select()
和poll()
的。
函数API
首先我们来看看如何创建epoll
对象:
1 | int epoll_create(int size); |
函数的参数现在已经没有意义的,为了和旧版本兼容,我们只需要输入一个正数即可。这个函数返回一个文件描述符,该文件描述符就是epoll
对象。
有了epoll
对象,我们就需要对该对象进行各种设置,要设置epoll
对象,我们需要调用:
1 | int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); |
epfd
是epoll
对象;op
是操作类型,有EPOLL_CTL_ADD
, EPOLL_CTL_MOD
和EPOLL_CTL_DEL
。它们分别表示添加/修改、删除一个epoll
中的socket描述符;fd
是我们需要操作的socket描述符;event
是一个epoll_event
的结构体:
1 | typedef union epoll_data { |
其中events
是我们等待的事件,data
是一个联合体,可以是socket描述符,也可以是其他,一般我们用作socket描述符。我们可以用epoll_ctl()
设置多个socket描述符,然后用函数epoll_wait()
等待事件就绪。该函数的原型如下:
1 | int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); |
epfd
是epoll
对象;events
是指向epoll_event
数组的指针;maxevents
是最多返回多少个socket描述符的请求;timeout
是多长时间超时。如果成功,函数将返回有几个就绪的描述符;如果没有就绪的,返回0;错误返回-1。
LT和ET模式
LT是Level Trigger,ET是Edge Trigger(让我想起了DFF)。它们的主要区别在于:LT模式下,如果对就绪的描述符不处理(或者说没有处理完),那么下次调用epoll_wait()
时,它还是会通知该描述符就绪;而在ET模式下,我们必须对就绪的描述符进行处理,因为epoll_wait()
不会再次对该描述符进行通知。因此,在ET模式下,我们将大大降低调用epoll_wait()
的次数。
这里的处理指的是是否读出buffer中的数据,是否向buffer写入数据等。
根据彻底学会使用epoll(二)——ET和LT的触发方式一文作者地总结,有以下几种情况会在ET模式下唤醒epoll_wait()
函数。
对于读操作:
- buffer由空变成不空;
- buffer写入新的数据;
- buffer不为空,用户使用
EPOLL_CTL_MOD
修改数据可读。
对于写操作:
- buffer有满变成不满;
- buffer数据减少;
- buffer不满,用户使用
EPOLL_CTL_MOD
修改数据可写。
在LT模式下,epoll_wait()
被唤醒的条件就完全不同。
对于读操作:
- buffer中数据没读完。
对于写操作:
- buffer没有写满。大多数情况下buffer是不满的,因此在用LT模式时:写操作只有在需要时候才添加到wait list中,不用时候要从wait list中删除,避免事件写事件总是就绪(
select()
和poll()
是LT模式,也要注意写操作)。
根据之前select()
函数的知识,可写是buffer剩余空间一般大于2048字节才满足可写,这里应该也是遵循相同的逻辑。
ET和FT代码示例
下面我们通过几个简单的例子来看看ET和LT的区别,以下例子均来自博文彻底学会使用epoll(三)——ET的读操作实例分析。
1 |
|
其他例子可以访问彻底学会使用epoll(四)——ET的写操作实例分析。
EPOLLONESHOT事件
如果我们只希望一个线程(or processes, if child processes have inherited the epoll file descriptor across fork(2))访问socket,我们可以设置socket描述符的EPOLLONESHOT事件。对于设置了该事件的socket描述符,这个socket描述符只能就绪一次,因此即使发生了线程(进程)切换,我们也不用担心这个描述符再次就绪。我们需要手动解除这个事件才能让其他线程(进程)可以接收到该描述符的就绪事件。
epoll()问题总结
ET模式下如何正确读写
ET模式下,每次事件只会被响应一次,因此我们需要在这一次响应中完成读写操作。一般做法就是将socket描述符设置为non-blocking
,然后通过while()
循环来读/写,直到返回0或者EAGAIN
(在non-blocking
的设置下表示buffer读完了或者buffer满了),或者EWOULDBLOCK
或者EINPROGRESS
。注意,如果读/写长度大于buffer,那么可能出现buffer为空/满的时候数据其实还没传输完毕。这种情况下,我们需要额外逻辑来处理。
为什么要将socket描述符设置为non-blocking
?
如果不是non-blocking
,那么读/写完后就会被read/write
函数阻塞,这样其他的socket描述符也被阻塞了。我们不希望在read/write
被阻塞,而是在epoll_wait()
处阻塞,这样其他就绪的socket描述符就可以进行自己的工作了。
ET模式下accept()函数的正确使用
如果有多个socket连接同时到来,ET模式下的epoll_wait()
只响应一次,那么accept()
函数只能处理一个连接,其他处于就绪队列的连接就不会被处理了。正确的做法是使用while()
循环来accept()
连接,直到所有连接都被处理。
多路复用下accept()的正确用法
accept()
要用non-blocking
,因为要确保进程不能被accept()
阻塞,我们需要进程被select(), poll()
或者epoll_wait()
阻塞。如果被accept()
阻塞,那么多路复用就失效了,因为其他socket描述符根本不会被响应。
三种多路复用的比较
select()
和poll()
在活动socket描述符少但总的socket描述符多时,效率低;epoll_wait()
在这种情况下效率高。在活动描述符多的情况下,epoll_wait()
则并不一定比select()
和poll()
强。图2是这三个函数的对比: