type
status
date
slug
summary
tags
category
icon
password
我们首先来看一张图(来自The Linux Programming Interface),这张图直观地展示了selectpollepoll三种不同的 I/O 多路复用技术的性能差异。
notion image
从图中可以明显地看到,epoll的性能是最好的,即使在监听多达 10000 个文件描述符的情况下,其性能和监听 10 个文件描述符相比,差别也不大。而随着文件描述符的增大,selectpoll的性能逐渐变得很差。
那么,epoll究竟使用了什么“魔法”,有如此惊人的效果呢?接下来,我们就一起来看一下吧…

1. epoll的使用

首先,我们通过编写一个聊天室服务器的例子认识一下epoll
使用epoll编写网络程序需要三个步骤:分别是epoll_createepoll_ctlepoll_wait。接下来,我们详细讲解一下这三个 API。

1.1 epoll_create()

epoll_create()函数会创建一个 epoll 实例,并返回一个文件描述符指向该 epoll 实例。
参数说明
size: 自Linux 2.6.8,参数size将被忽略,但是仍需传入一个大于 0 的整数。
💬
这其实是一个历史包袱:在早期的epoll_create实现中,size参数表示应用程序期望监控的文件描述符的数量,然后内核会根据size来初始化内核的数据结构。但在新的实现中,内核可以动态分配所需的数据结构,因此就不再需要这个参数了。我们只需注意,将size设置成一个大于 0 的整数就可以了。

1.2 epoll_ctl

epoll_ctl()函数是 epoll 的控制接口,它可以往 epoll 实例中添加一个文件描述符,从epoll 实例中删除一个文件描述符,或者修改和某个文件描述符关联的事件。
参数
epfd: 指向 epoll 实例的文件描述符。
op: 操作,可取值有EPOLL_CTL_ADDEPOLL_CTL_MODEPOLL_CTL_DEL
fd: 要添加、修改或删除的文件描述符。
event: 与文件描述符fd关联的事件。如果op的值为EPOLL_CTL_DELevent参数则被忽略。struct epoll_event的定义如下:
首先,我们来看一下struct epoll_eventevents成员,它表示监视事件的类型,常见的取值有以下几种:
  • EPOLLIN: 所关联的文件可读。
  • EPOLLOUT: 所关联的文件可写。
  • EPOLLRDHUP: 适用于stream socket,表示对方已关闭连接,或者对方关闭了写端。
  • EPOLLET: 设置该文件监听的事件为边缘触发(edge-triggered),默认为水平触发(level-triggered)。(边缘触发和水平触发的区别,下面会将…)
struct epoll_eventdata成员是用户自己设置的数据。我们一般会设置这个联合(union)里的fd字段,将其设置为所关联的文件描述符fd

1.3 epoll_wait

epoll_wait()函数类似之前讲过的select()poll(),等待内核 I/O 事件的分发。如果没有事件就绪,调用线程则会被挂起。
参数
epfd: 指向 epoll 实例的文件描述符。
events: 这是一个数组,当epoll_wait()返回的时候,里面存放的就是已经就绪的事件。
maxevents: epoll_wait()可以返回的最大事件数目,一般设置events数组的长度。
timeout: 超时时间 (ms)。-1 表示永久等待,0 表示立马返回,不等待。

2. 经典示例——聊天室

3. 水平触发 VS 边缘触发

水平触发(level-triggered)和边缘触发(edge-triggered)这两个术语来自于电子电路科学。其中水平触发的意思是,一个信号的持续状态(而不是其变化)触发某个事件的发生,比如如果有数据可读,就会一直触发EPOLLIN事件。
而边缘触发的意思是,事件是由信号的变化触发的,而不是由信号的持续状态触发。举个例子,再边缘触发机制下,当数据到达时,会触发EPOLLIN事件;尽管这些数据没有被读取,之后也不会再触发EPOLLIN事件了。[demo演示]
边缘触发会导致一些小问题:明明有数据可读,却不会触发EPOLLIN事件,导致应用程序陷入阻塞。为了避免这类现象发生,边缘触发往往会配合非阻塞I/O一起使用,最佳实践如下:
  1. 配合非阻塞I/O使用。
  1. 一直readwrite,直到这两个操作返回EAGAIN
[使用边缘触发改写聊天室服务器]
💬
Q: So, 水平触发和边缘触发哪个会更好呢? A: 一般来说,我们认为边缘触发的效率会更高,因为它可以有效地减少事件触发的次数。但是相应的代码复杂性也会更高,因为它得配合非阻塞I/O一起使用,并且我们得一直读和写,直到返回EAGAIN。而在水平触发模式(默认),epoll仅仅是一个更高效版本的poll
epoll的边缘触发模式是构建高性能服务器的有力杀手锏之一。
💬
Q: selectpoll是水平触发还是边缘触发? A: 自己写一个小程序测试一下:)

归纳总结

epoll的出现,为Linux高性能网络编程补齐了最后一块拼图。epoll避免了用户态—内核态频繁的数据拷贝,大大提高了性能。在使用epoll的时候,我们一定要理解条件触发和边缘触发两种模式。其中边缘触发模式是构建高性能服务器的有力杀手锏之一,著名的http服务器nginx就是基于这种模式构建的。

参考文章

 
💡
欢迎您在底部评论区留言,我们一起交流学习~
高性能网络编程(9)——The C10K problem高性能网络编程(7)——I/O 多路复用之 poll