epoll_event
数据结构详解
在 Linux 的 I/O 多路复用机制 epoll
中,epoll_event
是关键的数据结构,用于描述文件描述符(fd)上的事件和关联数据。其定义在头文件 <sys/epoll.h>
中:
c
struct epoll_event {
uint32_t events; // 事件掩码(位图)
epoll_data_t data; // 用户数据(联合体)
};
typedef union epoll_data {
void *ptr; // 自定义指针(常用)
int fd; // 关联的文件描述符(常用)
uint32_t u32; // 32位整数
uint64_t u64; // 64位整数
} epoll_data_t;
核心字段解析:
-
events
(事件标志位)EPOLLIN
:fd 可读(有数据到达)EPOLLOUT
:fd 可写(可发送数据)EPOLLERR
:fd 发生错误EPOLLHUP
:fd 被挂断(对端关闭连接)EPOLLET
:启用边缘触发模式(默认水平触发)EPOLLRDHUP
:流套接字对端关闭连接(或半关闭)
-
data
(用户数据联合体)- 常用
fd
或ptr
存储与事件相关的自定义数据(如连接上下文)
- 常用
使用案例:基于 ET 模式的 Echo 服务器
以下示例展示 epoll_event
在非阻塞 TCP 服务器中的典型用法(边缘触发模式):
c
#include <sys/epoll.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#define MAX_EVENTS 64
#define PORT 8080
// 设置 fd 为非阻塞模式
void set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
int main() {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(PORT),
.sin_addr.s_addr = INADDR_ANY
};
// 绑定并监听
bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(server_fd, SOMAXCONN);
// 创建 epoll 实例
int epoll_fd = epoll_create1(0);
struct epoll_event ev, events[MAX_EVENTS];
// 添加 server_fd 到 epoll
ev.events = EPOLLIN | EPOLLET; // 边缘触发模式
ev.data.fd = server_fd; // 存储文件描述符
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev);
while (1) {
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; i++) {
// 处理新连接
if (events[i].data.fd == server_fd) {
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &len);
set_nonblocking(client_fd);
// 注册客户端 fd
ev.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
ev.data.fd = client_fd; // 存储客户端 fd
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev);
}
// 处理客户端数据
else if (events[i].events & EPOLLIN) {
int client_fd = events[i].data.fd; // 取出 fd
char buffer[1024];
ssize_t bytes_read;
// ET 模式需循环读取所有数据
while ((bytes_read = read(client_fd, buffer, sizeof(buffer))) {
if (bytes_read > 0) {
write(client_fd, buffer, bytes_read); // Echo 回显
} else if (bytes_read == 0 || (errno != EAGAIN && errno != EWOULDBLOCK)) {
close(client_fd); // 关闭连接
break;
}
}
}
// 处理连接关闭
else if (events[i].events & (EPOLLRDHUP | EPOLLHUP)) {
close(events[i].data.fd); // 关闭失效连接
}
}
}
close(server_fd);
return 0;
}
关键逻辑说明:
-
事件注册
-
通过
epoll_ctl()
添加 fd 时,初始化epoll_event
结构:cev.events = EPOLLIN | EPOLLET; // 订阅事件类型 ev.data.fd = server_fd; // 存储关联的 fd
-
-
事件处理
epoll_wait()
返回就绪的epoll_event
数组- 通过
events[i].data.fd
取出关联的 fd - 通过
events[i].events
判断具体事件类型
-
边缘触发 (ET) 要点
- 必须循环读写直到返回
EAGAIN
- 需配合非阻塞 fd 避免阻塞
- 必须循环读写直到返回
常见使用技巧
-
自定义数据存储
-
使用
data.ptr
存储复杂结构体(如连接上下文):cstruct connection { int fd; struct sockaddr_in addr; }; struct connection *conn = malloc(sizeof(*conn)); conn->fd = client_fd; ev.data.ptr = conn; // 存储指针
-
-
事件组合
- 错误处理:
EPOLLERR | EPOLLHUP
- 读写监听:
EPOLLIN | EPOLLOUT
- 错误处理:
-
触发模式选择
- 水平触发 (LT):未处理事件会持续通知(默认)
- 边缘触发 (ET):事件就绪时只通知一次(性能更高)
性能提示:ET 模式 + 非阻塞 I/O 是 epoll 高性能的关键组合,适合高并发场景。