高性能网络编程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就是基于这种模式构建的。