epoll原理
- 简介:epoll是Linux下高性能I/O事件通知机制,是多路复用I/O(I/O multiplexing)的一种实现方式。它能高效处理大量并发连接。
- 工作方式
- 内核数据结构:epoll维护一个红黑树结构来管理用户感兴趣的文件描述符(fd),以及一个就绪链表用于存放发生事件的fd。
- epoll_ctl操作:通过
epoll_ctl
系统调用,用户可以向epoll内核对象中添加、修改或删除感兴趣的fd及其对应的事件类型(如读、写、异常等)。
- epoll_wait等待:
epoll_wait
函数会阻塞等待,直到注册的fd中有事件发生。当有事件发生时,内核将这些fd从红黑树中取出,放入就绪链表,并将就绪链表传递给用户空间,epoll_wait
返回。
事件注册与处理
- 事件注册:在C++中使用
epoll_ctl
函数来注册事件。首先创建一个epoll实例(epoll_create
或epoll_create1
),然后通过epoll_ctl
向该实例中添加需要监控的文件描述符及对应的事件。
- 事件处理:在
epoll_wait
返回后,处理返回的就绪事件。根据事件类型(如读事件、写事件等)对相应的文件描述符进行操作。
TCP服务器关键代码片段
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <fcntl.h>
#define MAX_EVENTS 10
#define PORT 8888
int main() {
// 创建socket
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1) {
perror("socket");
return 1;
}
// 设置socket为非阻塞
int flags = fcntl(listen_fd, F_GETFL, 0);
fcntl(listen_fd, F_SETFL, flags | O_NONBLOCK);
// 绑定端口
sockaddr_in server_addr{};
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = INADDR_ANY;
if (bind(listen_fd, (sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind");
close(listen_fd);
return 1;
}
// 监听
if (listen(listen_fd, 5) == -1) {
perror("listen");
close(listen_fd);
return 1;
}
// 创建epoll实例
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1");
close(listen_fd);
return 1;
}
// 注册监听socket到epoll
epoll_event event{};
event.data.fd = listen_fd;
event.events = EPOLLIN;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &event) == -1) {
perror("epoll_ctl: listen_fd");
close(listen_fd);
close(epoll_fd);
return 1;
}
epoll_event events[MAX_EVENTS];
while (true) {
// 等待事件发生
int num_events = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (num_events == -1) {
perror("epoll_wait");
break;
}
for (int i = 0; i < num_events; ++i) {
if (events[i].data.fd == listen_fd) {
// 处理新连接
sockaddr_in client_addr{};
socklen_t client_addr_len = sizeof(client_addr);
int client_fd = accept(listen_fd, (sockaddr*)&client_addr, &client_addr_len);
if (client_fd == -1) {
perror("accept");
continue;
}
// 设置客户端socket为非阻塞
flags = fcntl(client_fd, F_GETFL, 0);
fcntl(client_fd, F_SETFL, flags | O_NONBLOCK);
// 注册客户端socket到epoll
event.data.fd = client_fd;
event.events = EPOLLIN | EPOLLET; // 边缘触发模式
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) == -1) {
perror("epoll_ctl: client_fd");
close(client_fd);
}
} else {
// 处理客户端数据
int client_fd = events[i].data.fd;
char buffer[1024];
ssize_t bytes_read = recv(client_fd, buffer, sizeof(buffer), 0);
if (bytes_read <= 0) {
if (bytes_read == 0) {
// 对端关闭连接
std::cout << "Client disconnected" << std::endl;
} else {
perror("recv");
}
// 从epoll中删除并关闭客户端socket
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, nullptr);
close(client_fd);
} else {
buffer[bytes_read] = '\0';
std::cout << "Received: " << buffer << std::endl;
// 简单回显数据
send(client_fd, buffer, bytes_read, 0);
}
}
}
}
close(listen_fd);
close(epoll_fd);
return 0;
}
- 代码说明:
- 初始化部分:创建监听socket,设置为非阻塞,绑定端口并开始监听。
- epoll部分:创建epoll实例,并将监听socket注册到epoll中,监听读事件。
- 事件循环部分:
epoll_wait
等待事件发生,当有事件时,区分是新连接事件(监听socket可读)还是客户端数据事件(客户端socket可读)。对于新连接,设置为非阻塞并注册到epoll;对于客户端数据,读取并回显数据,若连接关闭则从epoll中删除并关闭socket。