在Linux中,epoll
是一种高效的I/O多路复用机制,支持两种工作模式:LT(Level Triggered,水平触发)和ET(Edge Triggered,边缘触发)。
概念和原理
LT 模式(Level Triggered)
LT 模式是 epoll
的默认模式。在 LT 模式下,当某个文件描述符就绪时,epoll_wait
函数会立即返回,通知应用程序有事件发生。即使应用程序没有立即处理完这些事件,下次调用 epoll_wait
时仍会再次返回这些就绪的文件描述符。
ET 模式(Edge Triggered)
ET 模式要求应用程序在处理文件描述符的就绪事件时,必须确保将其处理完毕,否则 epoll_wait
将不会重复通知该文件描述符的就绪状态。ET 模式通过设置 epoll_event
结构体中的 EPOLLET
标志来启用。
区别
-
通知机制:
- LT 模式:每当文件描述符就绪时,
epoll_wait
将通知应用程序,即使应用程序没有处理完该事件。 - ET 模式:只有在文件描述符状态发生变化时,
epoll_wait
才会通知应用程序。如果应用程序没有处理完事件,将不会重复通知该文件描述符的就绪状态。
- LT 模式:每当文件描述符就绪时,
-
效率:
- LT 模式:由于每次文件描述符就绪时都会通知应用程序,因此可能会引起频繁的上下文切换,影响效率。
- ET 模式:只在状态变化时通知应用程序,可以减少不必要的上下文切换,提高效率,特别适合处理大量事件和高并发的场景。
适用场景
-
LT 模式适用场景:
- 对实时性要求不是非常高的应用,例如普通的网络服务器或者需要周期性处理数据的情况。
- 适合处理一般的数据读取、写入等操作。
-
ET 模式适用场景:
- 对事件响应速度要求较高的应用,例如高性能网络服务器,需要快速处理大量连接或数据的情况。
- 适合处理大数据流、高并发请求等场景,可以减少因为频繁通知而引起的性能开销。
2.如何设置epoll的ET模式
在 epoll
中设置成 ET(Edge Triggered,边缘触发)模式,需要在使用 epoll_ctl
函数添加或修改事件时,设置 struct epoll_event
结构体中的 EPOLLET
标志位。
2.1 创建 epoll 实例:
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
2.2 准备事件结构体:
struct epoll_event event;
event.events = EPOLLIN | EPOLLET; // 使用 ET 模式
event.data.fd = sockfd; // sockfd 是需要监听的文件描述符
2.3 添加或修改事件:
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &event) == -1) {
perror("epoll_ctl");
exit(EXIT_FAILURE);
}
非阻塞模式:
- 使用 ET 模式通常需要将套接字设置为非阻塞模式,以充分发挥 ET 模式的优势。
- 可以通过
fcntl
函数设置套接字的非阻塞属性,如:
int flags = fcntl(sockfd, F_GETFL, 0);
if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) == -1) {
perror("fcntl");
exit(EXIT_FAILURE);
}
3.两种模式的比较
示例代码:
epoll的ET模式下的回显服务器
#include <stdio.h>
#include <ctype.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
// 设置文件描述符为非阻塞
void set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) {
perror("fcntl F_GETFL 错误");
exit(1);
}
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
perror("fcntl F_SETFL 错误");
exit(1);
}
}
// 服务器主函数
int main(int argc, const char* argv[])
{
// 创建监听套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1)
{
perror("socket 错误");
exit(1);
}
//printf("监听套接字:%d\n", lfd); //调试信息
// 设置监听套接字为非阻塞
set_nonblocking(lfd); // 注释掉这行,测试阻塞点
// 绑定服务器地址和端口
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(9999); // 监听端口9999
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定所有网络接口的IP地址
// 设置端口复用
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 将套接字绑定到指定地址
int ret = bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
if(ret == -1)
{
perror("绑定错误");
exit(1);
}
// 开始监听连接请求
ret = listen(lfd, 64);
if(ret == -1)
{
perror("监听错误");
exit(1);
}
// 创建一个 epoll 实例
int epfd = epoll_create(100);
if(epfd == -1)
{
perror("epoll_create 错误");
exit(1);
}
// 将监听套接字 lfd 加入 epoll 实例,监听读事件,使用ET模式
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 监听读事件,ET模式
ev.data.fd = lfd; // 数据是监听套接字 lfd
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
if(ret == -1)
{
perror("epoll_ctl 错误");
exit(1);
}
// 用于存放触发事件的数组
struct epoll_event evs[1024];
int size = sizeof(evs) / sizeof(struct epoll_event);
int count = 0; //调试信息,测试外循环触发次数
printf("开始监听客户端...\n"); //调试信息
// 进入事件处理循环
while(1)
{
count++;
printf("外循环次数:%d\n", count); //调试信息
// 等待事件触发
int num = epoll_wait(epfd, evs, size, -1);
printf("num: %d\n", num);
if(num == -1)
{
perror("epoll_wait 错误");
exit(1);
}
// 处理所有触发的事件
for(int i = 0; i < num; ++i)
{
printf("处理触发事件。。。\n"); //调试信息
int curfd = evs[i].data.fd; // 获取当前事件对应的文件描述符
// 如果是监听套接字 lfd 有事件发生,表示有新连接
if(curfd == lfd)
{
printf("正在处理新连接...\n"); // 调试输出
// 接受所有新连接
while (1) {
printf("进入连接循环\n"); //调试信息
int cfd = accept(lfd, NULL, NULL);
if(cfd == -1)
{
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 所有连接都已处理
break;
} else {
perror("accept 错误");
continue;
}
}
printf("新连接 %d 加入\n", cfd);
// 设置新连接为非阻塞
set_nonblocking(cfd); // 注释掉这行,测试阻塞点
// 将新连接 cfd 添加到 epoll 实例中监听其读事件,使用ET模式
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = cfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
if(ret == -1)
{
perror("epoll_ctl-accept 错误");
exit(1);
}
}
}
else
{
printf("正在处理读事件...\n"); // 调试输出
// 处理已连接套接字的数据收发
char buf[1024];
int len;
// 使用循环确保将缓冲区中所有数据读取完毕
while ((len = recv(curfd, buf, sizeof(buf), 0)) > 0) {
printf("客户端 %d 说: %s", curfd, buf);
send(curfd, buf, len, 0);
memset(buf, 0, sizeof(buf));
}
if(len == -1 && (errno != EAGAIN && errno != EWOULDBLOCK))
{
perror("recv 错误");
// 出错时关闭连接,并从 epoll 实例中删除
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
}
else if(len == 0)
{
// 客户端断开连接
printf("客户端 %d 已断开连接\n", curfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
}
}
}
}
close(lfd);
return 0;
}
- 如果监听套接字lfd和通信套接字cfd,都设置成了ET模式,那么这两类套接字都需要设置成非阻塞模式,这样程序才不会在accept和recv处阻塞,这样服务端才可以同时处理多个客户端的连接和通信。相当与服务器与客户端一对多通信
客户端1连接,发送数据成功
客户端2连接,发送数据成功
服务端打印的信息:
- 如果监听套接字lfd和通信套接字cfd,都设置成了ET模式,如果监听套接字lfd没有设置成非阻塞,通信套接字cfd设置成了非阻塞(没起作用因为阻塞在了accept处),程序会阻塞在accept处,服务端只能接收到客户端的连接无法收到客户端发送的数据,因为程序阻塞在了accept处,accept在一个循环里面。此时所有客户端都无法通信。
服务端:
客户端1:
客户端2:
- 如果监听套接字lfd和通信套接字cfd,都设置成了ET模式,如果监听套接字lfd设置成非阻塞,通信套接字cfd没有设置成了非阻塞,那么第一个客户端可以连接和通信,但是程序阻塞在了recv处,后续客户端的连接和通信都会被阻塞。相当于是变成了服务器和先到的客户端一对一,其他的客户端只能排队在。
服务端:
客户端1:(可以连接通信)
客户端2:(被阻塞,因为客户端1的cfd未被设置成非阻塞,阻塞在了recv处)
服务端:(客户端1退出后,可以接收客户端2的连接和通信)
客户端2:
总结:
- 如果监听套接字和通信套接字都被设置成了ET模式,那么程序就会在accept和recv处阻塞,因为ET模式下当有事件触发时只会通知一次,解决办法是把这两种套接字都是设置成非阻塞。这样就可以避免在这两处位置阻塞。
- 一般情况下,监听套接字不需要设置成ET模式,只需要把通信套接字设置成ET模式即可,但是accept函数需要放到一个循环中。
思考题? epoll监控监听文件描述符可以设置成ET模式吗??
答案: 可以. 但是如果设置成ET模式以后, 当调用epoll_wait函数的时候, 每次只能accept一个连接(该连接在已连接队列当中, 而调用一次accept只能已连接队列中获取一个连接), 如果同时有多个连接到来, 就得epoll_wait再次返回之后才能继续accept下一个连接, 所以如果设置成了ET模式且有多个连接请求的话, 应该将accept写在循环当中, 一次epoll_wait后循环accept所有的连接.
所以一般不会在epoll中将监听的文件描述符设置为ET模式, 使用默认的LT模式即可; 而对通信的文件描述符一般采用非阻塞模式的ET模式.
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <ctype.h>
#define MAX_EVENTS 1024
#define MAX_BUF_SIZE 1024
int main() {
int lfd, cfd, epfd, nready, sockfd, n;
struct sockaddr_in svraddr;
struct epoll_event ev, events[MAX_EVENTS];
char buf[MAX_BUF_SIZE];
// 创建监听套接字
lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
// 设置端口复用
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 绑定地址和端口
memset(&svraddr, 0, sizeof(svraddr));
svraddr.sin_family = AF_INET;
svraddr.sin_addr.s_addr = htonl(INADDR_ANY);
svraddr.sin_port = htons(8888);
if (bind(lfd, (struct sockaddr *)&svraddr, sizeof(svraddr)) == -1) {
perror("bind");
exit(EXIT_FAILURE);
}
// 监听套接字
if (listen(lfd, 128) == -1) {
perror("listen");
exit(EXIT_FAILURE);
}
// 创建 epoll 实例
epfd = epoll_create(1024);
if (epfd == -1) {
perror("epoll_create");
exit(EXIT_FAILURE);
}
// 将监听套接字 lfd 添加到 epoll 实例中
ev.events = EPOLLIN; // LT 模式
ev.data.fd = lfd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev) == -1) {
perror("epoll_ctl");
exit(EXIT_FAILURE);
}
printf("Server started. Waiting for connections...\n");
while (1) {
// 等待事件发生
nready = epoll_wait(epfd, events, MAX_EVENTS, -1);
if (nready == -1) {
perror("epoll_wait");
exit(EXIT_FAILURE);
}
for (int i = 0; i < nready; ++i) {
sockfd = events[i].data.fd;
// 处理新的客户端连接
if (sockfd == lfd) {
cfd = accept(lfd, NULL, NULL);
if (cfd == -1) {
perror("accept");
continue;
}
// 设置客户端套接字为非阻塞模式
int flags = fcntl(cfd, F_GETFL, 0);
fcntl(cfd, F_SETFL, flags | O_NONBLOCK);
// 将客户端套接字 cfd 添加到 epoll 实例中,使用 ET 模式
ev.events = EPOLLIN | EPOLLET; // ET 模式
ev.data.fd = cfd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev) == -1) {
perror("epoll_ctl");
exit(EXIT_FAILURE);
}
printf("New connection established. Client connected on socket %d.\n", cfd);
} else {
// 处理客户端发送的数据
while (1) {
n = read(sockfd, buf, MAX_BUF_SIZE);
if (n == -1) {
if (errno != EAGAIN && errno != EWOULDBLOCK) {
perror("read");
close(sockfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
}
break;
} else if (n == 0) {
printf("Client closed connection. Socket %d closed.\n", sockfd);
close(sockfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
break;
} else {
// 处理接收到的数据
printf("Received from socket %d: %.*s", sockfd, n, buf);
// 将接收到的数据转换为大写
for (int j = 0; j < n; ++j) {
buf[j] = toupper(buf[j]);
}
// 发送转换后的数据给客户端
write(sockfd, buf, n);
}
}
}
}
}
// 关闭监听套接字和 epoll 实例
close(lfd);
close(epfd);
return 0;
}