epoll的LT模式和ET模式
LDK Lv4

epoll

epoll是Linux特有的IO复用函数,关于epoll的原理,参见:Linux的IO多路复用

LT模式

epoll的默认模式,这种情况下epoll相当于一个效率较高的poll

对于采用LT工作模式的文件描述符,当epoll_wait检测到其上有事件发生并且将此事件通知给应用程序后,应用程序还可以不立即处理该事件。这样,当应用程序下一次调用epoll_wait时,epoll_wait还会再次同志应用程序,直到该事件被处理。

ET模式

当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll将以ET模式来操作该文件描述符。ET模式是epoll的高效工作模式。

对于采用ET模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知给应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件。

可见,ET模式和大程度上降低了同一个epoll事件被重复触发的次数。

具体实现

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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#include <libgen.h>

#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 10

/**
* @brief 将文件描述符设置为非阻塞的
*/
int setnonblocking(int fd) {
int old_option = fcntl(fd, F_GETFL); // 读取当前文件状态标志
int new_option = old_option | O_NONBLOCK; // 设置非阻塞
fcntl(fd, F_SETFL, old_option); // 写入新的文件状态标志
return old_option;
}

/**
* @brief 注册EPOLLIN事件到epoll内核事件表
* @param epollfd 内核事件表对应的文件描述符
* @param fd 要注册EPOLLIN的文件描述符
* @param enable_et 是否对fd启用ET模式
*/
void addfd(int epollfd, int fd, bool enable_et) {
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN;
if (enable_et) {
event.events |= EPOLLET; // 通过按位或,增加EPOLLET属性
}
// 注意此处的event作为一个局部变量竟然传入了地址(使用epoll_ctl该参数是指针),
// 推测是因为传指针可以避免拷贝,速度更快
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
setnonblocking(fd); // 设置文件描述符为非阻塞
}

/**
* @brief LT模式工作流程
* @param events 已经就绪的事件的数组
* @param number 已就绪事件的数量
* @param epollfd 内核事件表的文件描述符
* @param listenfd 要处理的事件
*/
void LT(epoll_event *events, int number, int epollfd, int listenfd) {
char buf[BUFFER_SIZE];
// 用循环遍历所有已经就绪的事件的列表(events)
for (int i = 0; i < number; ++i) {
int sockfd = events[i].data.fd; // 通常不用event.data的fd成员
if (sockfd == listenfd) { // 连接未受理。执行accept进行受理。注意此处可以用==判断相等!
sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
// 受理连接。注意accept的后两个参数都需要取地址
int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);
// 将受理后的连接加入内核事件表,监听后续客户端的消息
addfd(epollfd, connfd, false); // 对connfd禁用ET模式
} else if (events[i].events & EPOLLIN) { // 连接已经受理并且有数据可读(数据未被全部读出)
// 只要socket读缓存中还有未读出的数据,这段代码就会触发
printf("enevt trigger once\n");
memset(buf, '\0', BUFFER_SIZE);
// 读出数据。注意recv的第三个参数是 BUFFERE_SIZE - 1。不是BUFFER_SIZE
int ret = recv(sockfd, buf, BUFFER_SIZE - 1, 0);
if (ret < 0) {
close(sockfd);
continue;
}
printf("get %d bytes of content: %s\n", ret, buf);
} else {
printf("something else happened \n");
}
}
}

/**
* @brief ET模式工作流程
* @param
*/
void ET(epoll_event *events, int number, int epollfd, int listenfd) {
char buf[BUFFER_SIZE];
for (int i = 0; i < number; ++i) {
int sockfd = events[i].data.fd;
if (sockfd == listenfd) { // 未受理的连接。调用accept受理该连接
sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
int connfd = accept(listenfd, (sockaddr *)&client_address, &client_addrlength);
addfd(epollfd, connfd, true); // 对connfd开启ET模式
} else if (events[i].events & EPOLLIN) {
printf("event trigger once\n");
/**
* else if 中的这段代码不会被重复触发,所以我们循环读取数据,以确保把socket读缓存中的所有数据读出
*/
while (1) {
memset(buf, '\0', BUFFER_SIZE);
// 读出数据
int ret = recv(sockfd, buf, BUFFER_SIZE - 1, 0);
if (ret < 0) {
/**
* 对于非阻塞IO,下面的条件成立则表示数据已经被全部读取完毕。
* 此后epoll就能再次触发sockfd上的EPOLLIN事件,以驱动下一次读操作
*/
if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) {
printf("read later\n");
break;
}
close(sockfd);
break;
} else if (ret == 0) {
close(sockfd);
} else {
printf("get %d bytes of content: %s\n", ret, buf);
}
}
} else {
printf("something else happened \n");
}
}
}

int main(int argc, char *argv[]) {
if (argc <= 2) {
// 参数不够
printf("usage: %s ip_address port_number\n", basename(argv[0]));
return 1;
}
const char *ip = argv[1];
int port = atoi(argv[2]);

int ret = 0;
sockaddr_in address;
memset(&address, 0, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr); // 将标准文本表示形式的IPv4或IPv6地址转换为网络字节序
address.sin_port = htons(port); // 将主机字节序(host)的port转换为网络字节序(net)

int listenfd = socket(PF_INET, SOCK_STREAM, 0); // 创建socket
assert(listenfd >= 0);

// bind函数的第二个参数可以直接强转
ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));
assert(ret != -1);

ret = listen(listenfd, 5);
assert(ret != -1);

epoll_event events[MAX_EVENT_NUMBER];
int epollfd = epoll_create(5); // 创建内核事件表
assert(epollfd != -1); // 确保创建成功
addfd(epollfd, listenfd, true); // 添加socket文件描述符到内核事件表。注意此处是开启ET模式的!

while (1) {
// 第三个参数为-1, 则epoll_wait将永远阻塞,直到某个事件发生
int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if (ret < 0) {
printf("epoll failure\n");
break;
}

LT(events, ret, epollfd, listenfd); // 使用LT模式
// ET(events, ret, epollfd, listenfd); // 使用ET模式
}

close(listenfd);
return 0;
}

由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 34.6k 访客数 访问量