Java NIO分析(5): I/O多路复用之epoll系统调用

1. epoll概念

poll系统调用相比于select主要解决了文件描述符的数量限制,但是在高并发场景下没有解决根本问题:

  1. fd数组整体在内核空间和用户空间之间拷贝
  2. 遍历整个fd数组找事件浪费资源

这俩性能问题在Banga在1999年写了篇论文A Scalable and Explicit Event
Delivery Mechanism for UNIX
,提出selectpoll都是无状态的,需要用户空间的进程自行遍历查找事件, 一种改进方案是内核内部自己维护事件集合.通过一个类似declare_interest的系统调用,内核能够增量得更新进程感兴趣的事件集合列表, 应用进程通过使用get_next_event调用能派发新事件给内核。

根据论文的研究成果,LINUXFreeBSD各自给出的解决方案:epollkqueue.我们主要讨论epoll, 毕竟日常服务端环境都是LINUX.

在LINUX内核2.6以上,epoll才受到支持。

2. epoll函数

epoll操作过程有3个函数

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

int epoll_create(int size);

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

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  1. epoll_create创建一个epoll fd和事件表, 在LINUX2.6.8以后是使用红黑树来管理epoll事件表,所以size没有太大作用
  2. epoll_ctl操作上面创建的epoll事件表, 可以加入socket读写事件
  3. epoll_wait类似于以前的selectpoll,得到发生的事件, 如socket可读可写

epoll_ctl的第二个参数使用3个宏来表示动作:

  • EPOLL_CTL_ADD: 注册新的fd到epfd中
  • EPOLL_CTL_MOD: 修改意见注册的fd的监听事件
  • EPOLL_CTL_DEL: 从epfd中删除一个fd

第四个参数用来告诉内核需要监听什么事件, epoll_event结构如下

1
2
3
4
struct epoll_event {
__uint32_t events; // epoll事件
epoll_data_t data; // 用户数据变量
}

epoll事件和以前poll的事件类型差不多,主要还是这仨:

  • EPOLLIN: 对应的fd可读
  • EPOLLOUT: 对应的fd可写
  • EPOLLERR: 对应的fd发送初五

3. epoll实战

首先,MacOS是基于BSD的,所以是没有epoll函数的,在Mac下开发,得去LINUX环境编译。依然是老5步

  1. 新建socket, 用于监听端口
  2. socket的fd绑定端口
  3. socket监听
  4. 创建epoll fd
  5. 发起epoll_wait获取事件,使用epoll_ctl管理事件
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
103
104
105
106
107
108
109
110
111
112
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/epoll.h>

#define MAX_FD_NUM 1024
#define MAXLEN 1024

int buf_len = 0;

int main()
{
// 1. 新建socket, 用于监听端口
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1) printf("创建socket失败, error: %s (errno: %d)\n", strerror(errno), errno);

// 2. 绑定端口
unsigned short listenPort = 8090;
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(listenPort);

int on = 1;
// 设置socket绑定的端口,再程序关闭之后可以重复使用
if ((setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(int))) < 0) {
exit(1);
}
int bindRet = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
if (bindRet == -1) {
printf("socket绑定地址失败, error: %s (errno: %d)\n", strerror(errno), errno);
exit(1);
}

// 3. 监听端口
int listenRet = listen(listenfd, 10);
if (listenRet == -1) printf("socket监听端口失败, error: %s (errno: %d)\n", strerror(errno), errno);
printf("socket 监听完毕, 地址: 127.0.0.1:%d", listenPort);

struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(struct sockaddr_in);

// 4. 创建一个 epfd,并且把 listenfd 注册到这个 epfd上。
int epfd = epoll_create(1024);
struct epoll_event ev,events[20];
ev.data.fd = listenfd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);

int cur_fd_num = 1;
char buf[MAXLEN]={0};

while (1) {
// 5. 调用epoll_wait获取IO事件
// nReady 就是 events 数组的长度。
int nready = epoll_wait(epfd, events, 20, 50);

int i = 0;
for (; i < nready; i++) {
if (events[i].data.fd == listenfd) {
int client_sockfd = accept(listenfd,(struct sockaddr*)&client_addr,&client_addr_len);

if(client_sockfd < 0) {
perror("accept");
}
else {
printf("accept client_addr %s\n",inet_ntoa(client_addr.sin_addr));
ev.data.fd = client_sockfd;
ev.events=EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, client_sockfd, &ev);
}
}
else if (events[i].events & EPOLLIN) {
int connfd = events[i].data.fd;
int n = recv(connfd, buf, MAXLEN, 0);
if(n <= 0) {
if(ECONNRESET == errno) {
close(connfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, 0);
}
else {
perror("recv");
}
}

printf("receive %s", buf);
buf_len = n;

ev.data.fd = connfd;
ev.events = EPOLLOUT;
epoll_ctl(epfd, EPOLL_CTL_MOD, connfd, &ev);
}
else if (events[i].events & EPOLLOUT) {
int connfd = events[i].data.fd;
write(connfd, buf, buf_len);

ev.data.fd = connfd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_MOD, connfd, &ev);
}
}
}

return 0;
}

使用gcc编译,运行

1
2
$ gcc -o epoll ./epoll.c
$ ./epoll

然后使用nc测试就好了

4. epoll总结

epoll解决了selectpoll时代遗留的2个性能问题,不需要使fd数组整体在内核空间和用户空间之间来回拷贝,同时
不需要应用进程遍历整个fd数组以查找发生的事件。epoll使用mmap加速了内核和用户空间的消息传递,避免不必要的内存拷贝。
epoll只会返回活跃的socket fd,所以I/O效率不会随着fd数目增加而显著下降。

epoll还支持ET(边缘触发)LT(水平触发),这些就不细讲了。