高性能网络编程IO多路复用之epoll
epoll
的性能是最好的,即使在监听多达 10000 个文件描述符的情况下,其性能和监听 10 个文件描述符相比,差别也不大。而随着文件描述符的增大,select
和 poll
的性能逐渐变得很差。
1. epoll
的使用
首先,通过编写一个聊天室服务器的例子认识一下epoll
。
使用epoll
编写网络程序需要三个步骤:分别是epoll_create``epoll_ctl
和epoll_wait
。接下来,我们详细讲解一下这三个 API。
1.1 epoll_create()
epoll_create()
函数会创建一个 epoll 实例,并返回一个文件描述符指向该 epoll 实例。
1 |
|
参数说明
size
: 自Linux 2.6.8,参数size
将被忽略,但是仍需传入一个大于 0 的整数。
这其实是一个历史包袱:在早期的
epoll_create
实现中,size
参数表示应用程序期望监控的文件描述符的数量,然后内核会根据size
来初始化内核的数据结构。但在新的实现中,内核可以动态分配所需的数据结构,因此就不再需要这个参数了。我们只需注意,将size
设置成一个大于 0 的整数就可以了。
1.2 epoll_ctl
epoll_ctl()
函数是 epoll 的控制接口,它可以往 epoll 实例中添加一个文件描述符,从epoll 实例中删除一个文件描述符,或者修改和某个文件描述符关联的事件。
1 |
|
参数
epfd
: 指向 epoll 实例的文件描述符。
op
: 操作,可取值有EPOLL_CTL_ADD``EPOLL_CTL_MOD``EPOLL_CTL_DEL
。
fd
: 要添加、修改或删除的文件描述符。
event
: 与文件描述符fd
关联的事件。如果op
的值为EPOLL_CTL_DEL
,event
参数则被忽略。struct epoll_event
的定义如下:
1 | struct epoll_event { |
首先,我们来看一下struct epoll_event
的events
成员,它表示监视事件的类型,常见的取值有以下几种:
EPOLLIN
: 所关联的文件可读。EPOLLOUT
: 所关联的文件可写。EPOLLRDHUP
: 适用于stream socket,表示对方已关闭连接,或者对方关闭了写端。EPOLLET
: 设置该文件监听的事件为边缘触发(edge-triggered),默认为水平触发(level-triggered)。(边缘触发和水平触发的区别,下面会将…)
struct epoll_event
的data
成员是用户自己设置的数据。我们一般会设置这个联合(union)里的fd
字段,将其设置为所关联的文件描述符fd
。
1.3 epoll_wait
epoll_wait()
函数类似之前讲过的select()
和poll()
,等待内核 I/O 事件的分发。如果没有事件就绪,调用线程则会被挂起。
1 |
|
参数
epfd
: 指向 epoll 实例的文件描述符。
events
: 这是一个数组,当epoll_wait()
返回的时候,里面存放的就是已经就绪的事件。
maxevents
: epoll_wait()
可以返回的最大事件数目,一般设置events
数组的长度。
timeout
: 超时时间 (ms)。-1 表示永久等待,0 表示立马返回,不等待。
2. 经典示例——聊天室
1 | // epoll_chatroom.c |
3. 水平触发 VS 边缘触发
水平触发(level-triggered)和边缘触发(edge-triggered)这两个术语来自于电子电路科学。其中水平触发的意思是,一个信号的持续状态(而不是其变化)触发某个事件的发生,比如如果有数据可读,就会一直触发EPOLLIN
事件。
而边缘触发的意思是,事件是由信号的变化触发的,而不是由信号的持续状态触发。举个例子,再边缘触发机制下,当数据到达时,会触发EPOLLIN
事件;尽管这些数据没有被读取,之后也不会再触发EPOLLIN
事件了。[demo演示]
边缘触发会导致一些小问题:明明有数据可读,却不会触发EPOLLIN
事件,导致应用程序陷入阻塞。为了避免这类现象发生,边缘触发往往会配合非阻塞I/O一起使用,最佳实践如下:
配合非阻塞I/O使用。
一直
read
和write
,直到这两个操作返回EAGAIN
。
💬
Q: So, 水平触发和边缘触发哪个会更好呢? A: 一般来说,我们认为边缘触发的效率会更高,因为它可以有效地减少事件触发的次数。但是相应的代码复杂性也会更高,因为它得配合非阻塞I/O一起使用,并且我们得一直读和写,直到返回EAGAIN
。而在水平触发模式(默认),epoll
仅仅是一个更高效版本的poll
。
**epoll
**的边缘触发模式是构建高性能服务器的有力杀手锏之一。
归纳总结
epoll
的出现,为Linux高性能网络编程补齐了最后一块拼图。epoll
避免了用户态—内核态频繁的数据拷贝,大大提高了性能。在使用epoll
的时候,我们一定要理解条件触发和边缘触发两种模式。其中边缘触发模式是构建高性能服务器的有力杀手锏之一,著名的http服务器nginx就是基于这种模式构建的。