epoll是Linux内核为处理大批量文件描述符而设计的IO多路复用机制,它避免了select和poll需要遍历所有监听描述符的性能问题,在高并发场景下表现优异。C++开发者可以通过系统调用直接操作epoll,实现高效的事件驱动型网络服务。

epoll核心接口说明
Linux系统为epoll提供了三个核心系统调用,所有操作都围绕这三个接口展开:
- epoll_create:创建一个epoll实例,返回对应的文件描述符,后续所有epoll操作都基于这个描述符进行。
- epoll_ctl:用于控制epoll实例,可以向其中添加、修改或删除需要监听的文件描述符及其对应的事件。
- epoll_wait:等待epoll实例上注册的事件发生,当有事件就绪时返回对应的文件描述符和事件类型。
epoll事件结构体说明
epoll使用epoll_event结构体来描述需要监听的事件,结构定义如下:
#include <sys/epoll.h>
// epoll_event结构体定义
typedef union epoll_data {
void *ptr;
int fd; // 文件描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; // 事件掩码,如EPOLLIN、EPOLLOUT等
epoll_data_t data; // 用户数据,通常存储文件描述符
};
完整C++ epoll服务端示例
下面是一个基于epoll的简单TCP服务端实现,支持多个客户端并发连接,处理客户端的消息并返回响应:
#include <iostream>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <vector>
#define MAX_EVENTS 1024 // 最大事件数量
#define BUFFER_SIZE 1024 // 缓冲区大小
#define PORT 8888 // 监听端口
int main() {
// 1. 创建TCP监听socket
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) {
std::cerr << "创建socket失败" << std::endl;
return -1;
}
// 设置socket地址可重用
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 绑定地址和端口
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
std::cerr << "绑定端口失败" << std::endl;
close(listen_fd);
return -1;
}
// 开始监听
if (listen(listen_fd, 10) < 0) {
std::cerr << "监听失败" << std::endl;
close(listen_fd);
return -1;
}
std::cout << "服务端启动,监听端口:" << PORT << std::endl;
// 2. 创建epoll实例
int epoll_fd = epoll_create(1);
if (epoll_fd < 0) {
std::cerr << "创建epoll实例失败" << std::endl;
close(listen_fd);
return -1;
}
// 3. 将监听socket添加到epoll监听
struct epoll_event ev;
ev.events = EPOLLIN; // 监听读事件
ev.data.fd = listen_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) < 0) {
std::cerr << "添加监听socket到epoll失败" << std::endl;
close(listen_fd);
close(epoll_fd);
return -1;
}
// 事件数组,用于存储就绪的事件
struct epoll_event events[MAX_EVENTS];
char buffer[BUFFER_SIZE];
while (true) {
// 4. 等待事件发生
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (nfds < 0) {
std::cerr << "epoll_wait错误" << std::endl;
break;
}
// 遍历所有就绪的事件
for (int i = 0; i < nfds; ++i) {
if (events[i].data.fd == listen_fd) {
// 有新客户端连接
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd < 0) {
std::cerr << "接受客户端连接失败" << std::endl;
continue;
}
std::cout << "新客户端连接,fd:" << client_fd
<< ",地址:" << inet_ntoa(client_addr.sin_addr) << std::endl;
// 将新的客户端socket添加到epoll监听
ev.events = EPOLLIN | EPOLLET; // 边缘触发模式
ev.data.fd = client_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev) < 0) {
std::cerr << "添加客户端socket到epoll失败" << std::endl;
close(client_fd);
}
} else if (events[i].events & EPOLLIN) {
// 客户端有数据可读
int client_fd = events[i].data.fd;
memset(buffer, 0, BUFFER_SIZE);
int len = read(client_fd, buffer, BUFFER_SIZE - 1);
if (len <= 0) {
// 客户端断开连接
std::cout << "客户端断开连接,fd:" << client_fd << std::endl;
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, nullptr);
close(client_fd);
} else {
// 处理客户端数据
std::cout << "收到客户端" << client_fd << "消息:" << buffer << std::endl;
// 回显消息给客户端
write(client_fd, buffer, len);
}
}
}
}
// 关闭资源
close(listen_fd);
close(epoll_fd);
return 0;
}
epoll两种触发模式说明
epoll支持两种事件触发模式,开发者可以根据场景选择:
- 水平触发(LT):默认模式,只要文件描述符处于就绪状态,epoll_wait就会一直通知,直到数据被处理完成。这种模式容错性高,不容易丢失事件。
- 边缘触发(ET):只有文件描述符状态发生变化时才会通知一次,如果本次没有把数据全部处理完,后续不会再通知,直到下次状态变化。这种模式效率更高,但需要配合非阻塞socket使用,避免阻塞导致后续事件无法处理。
开发注意事项
在使用C++操作epoll时,需要注意以下几点:
- 使用边缘触发模式时,必须将socket设置为非阻塞模式,并且要循环读取数据直到返回EAGAIN错误,避免数据残留。
- 监听的文件描述符不需要设置为非阻塞,只有边缘触发模式下监听的读写描述符建议设置非阻塞。
- epoll实例本身也是一个文件描述符,使用完成后需要及时关闭,避免资源泄漏。
- 处理客户端断开连接时,要记得从epoll实例中删除对应的描述符,再关闭socket。