epoll的性能是最好的,即使在监听多达 10000 个文件描述符的情况下,其性能和监听 10 个文件描述符相比,差别也不大。而随着文件描述符的增大,selectpoll的性能逐渐变得很差。

1. epoll的使用

首先,通过编写一个聊天室服务器的例子认识一下epoll

使用epoll编写网络程序需要三个步骤:分别是epoll_create``epoll_ctlepoll_wait。接下来,我们详细讲解一下这三个 API。

1.1 epoll_create()

epoll_create()函数会创建一个 epoll 实例,并返回一个文件描述符指向该 epoll 实例。

1
2
3
4
5
#include <sys/epoll.h>

int epoll_create(int size);

// Returns: a file descriptor if OK; -1 on error and errno is set.

参数说明

size: 自Linux 2.6.8,参数size将被忽略,但是仍需传入一个大于 0 的整数。

这其实是一个历史包袱:在早期的epoll_create实现中,size参数表示应用程序期望监控的文件描述符的数量,然后内核会根据size来初始化内核的数据结构。但在新的实现中,内核可以动态分配所需的数据结构,因此就不再需要这个参数了。我们只需注意,将size设置成一个大于 0 的整数就可以了。

1.2 epoll_ctl

epoll_ctl()函数是 epoll 的控制接口,它可以往 epoll 实例中添加一个文件描述符,从epoll 实例中删除一个文件描述符,或者修改和某个文件描述符关联的事件。

1
2
3
4
5
#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

// Returns: 0 if OK; -1 on error and errno is set.

参数

epfd: 指向 epoll 实例的文件描述符。

op: 操作,可取值有EPOLL_CTL_ADD``EPOLL_CTL_MOD``EPOLL_CTL_DEL

fd: 要添加、修改或删除的文件描述符。

event: 与文件描述符fd关联的事件。如果op的值为EPOLL_CTL_DELevent参数则被忽略。struct epoll_event的定义如下:

1
2
3
4
5
6
7
8
9
10
11
struct epoll_event { 
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};

typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;

首先,我们来看一下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 事件的分发。如果没有事件就绪,调用线程则会被挂起。

1
2
3
4
5
6
#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

// Returns: number of ready file descriptors if Ok;
// 0 on timeout; –1 on error and errno is set.

参数

epfd: 指向 epoll 实例的文件描述符。

events: 这是一个数组,当epoll_wait()返回的时候,里面存放的就是已经就绪的事件。

maxevents: epoll_wait()可以返回的最大事件数目,一般设置events数组的长度。

timeout: 超时时间 (ms)。-1 表示永久等待,0 表示立马返回,不等待。

2. 经典示例——聊天室

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// epoll_chatroom.c 
// 为了方便,省略了错误处理
#include "lib/common.h"
#include "lib/bitmap.h"

#define PORT "12123"
#define BACKLOG 10
#define MAXLINE 1024
#define MAXEVENTS 1024

int main(void) {
int listenfd = tcp_listen();

/* track all file descriptors that epoll instance monitors */
bitmap* bm = bitmap_create(16);

// create epoll instance
int epfd;
if ((epfd = epoll_create(1)) == -1) {
error(1, errno, "epoll_create");
}

// Add the listener to `epfd`
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = listenfd;

if (epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &event) == -1) {
error(1, errno, "epoll_ctl");
}
bitmap_set(bm, listenfd);

/* Buffer where events are returned */
struct epoll_event events[MAXEVENTS];

// Main loop
for (;;) {
int n;
if ((n = epoll_wait(epfd, events, MAXEVENTS, -1)) == -1) {
error(0, errno, "epoll_wait");
continue;
}

// run through `events` for new connection and data to read
for (int i = 0; i < n; i++) {
int sockfd = events[i].data.fd;

if (!(events[i].events & EPOLLIN)) {
// something went wrong
error(0, 0, "epoll error on socket %d", sockfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
bitmap_unset(bm, sockfd);
close(sockfd);
continue;
} else if (sockfd == listenfd) {
// listener is ready to read, handle new connection
struct sockaddr_storage cliaddr;
socklen_t addrlen = sizeof(cliaddr);

int connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &addrlen);
if (connfd == -1) {
error(0, errno, "accept");
} else {
event.data.fd = connfd;
event.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &event);
bitmap_set(bm, connfd);

char ipstr[50];
inet_ntop(cliaddr.ss_family, get_ip_addr(&cliaddr), ipstr, 50);

printf("INFO: new connection from %s on socket %d\n", ipstr, connfd);
}
} else {
// handle data from a client
char recvline[MAXLINE];
int n;
if ((n = recv(sockfd, recvline, MAXLINE, 0)) <= 0) {
if (n == 0) {
// connection closed by client
printf("INFO: socket %d hung up\n", sockfd);
} else {
error(0, errno, "recv"); // something wrong
}
// remove from `epfd`
close(sockfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
bitmap_unset(bm, sockfd);
} else {
// got some data from a client
for (size_t j = 0; j < bm->bits; j++) {
if (bitmap_isset(bm, j) && j != listenfd && j != sockfd) {
if (send(j, recvline, n, 0) == -1) {
error(0, errno, "send");
}
}
}
}
}
}
}
}

3. 水平触发 VS 边缘触发

水平触发(level-triggered)和边缘触发(edge-triggered)这两个术语来自于电子电路科学。其中水平触发的意思是,一个信号的持续状态(而不是其变化)触发某个事件的发生,比如如果有数据可读,就会一直触发EPOLLIN事件。

而边缘触发的意思是,事件是由信号的变化触发的,而不是由信号的持续状态触发。举个例子,再边缘触发机制下,当数据到达时,会触发EPOLLIN事件;尽管这些数据没有被读取,之后也不会再触发EPOLLIN事件了。[demo演示]

边缘触发会导致一些小问题:明明有数据可读,却不会触发EPOLLIN事件,导致应用程序陷入阻塞。为了避免这类现象发生,边缘触发往往会配合非阻塞I/O一起使用,最佳实践如下:

  1. 配合非阻塞I/O使用。

  2. 一直readwrite,直到这两个操作返回EAGAIN

💬

Q: So, 水平触发和边缘触发哪个会更好呢? A: 一般来说,我们认为边缘触发的效率会更高,因为它可以有效地减少事件触发的次数。但是相应的代码复杂性也会更高,因为它得配合非阻塞I/O一起使用,并且我们得一直读和写,直到返回EAGAIN。而在水平触发模式(默认),epoll仅仅是一个更高效版本的poll

**epoll**的边缘触发模式是构建高性能服务器的有力杀手锏之一。

归纳总结

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