epoll 是 Linux 内核为处理大批量文件描述符而作了改进的 poll,是 Linux 下多路复用 IO接口 select/poll 的增强版本.
在 linux 的网络编程中,很长时间都在使用 select 来做事件触发。在 2.6 内核中,有一种替换它的机制,就是 epoll。
select 与 epoll 区别概述
1、函数使用上:epoll 使用一组函数来完成任务,而不是单个函数
2、效率:select 使用轮询来处理,随着监听 fd 数目的增加而降低效率。
而 epoll 把用户关心的文件描述符事件放在内核里的一个事件表中,只需要一个额外的文件描述符来标识内核中的这个事件表即可。
/*
函数名 :int epoll_create(int size)
参 数 :int size -- 监听的数目
返回值 :int fd -- 一个额外的文件描述符, 来标识内核事件表
说 明 :创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
*/
int epoll_create(int size);
epoll_create1() 是 Linux 系统中用于创建一个 epoll 实例 的系统调用,它是 epoll_create() 的增强版本,提供了更灵活的控制选项。
#include <sys/epoll.h>
int epoll_create1(int flags);
/*
函数名 :epoll_ctl
参 数 :int epfd -- 要操作的内核事件表的文件描述符,即epoll_create 的返回值
参 数 :int op -- 指定操作类型,操作类型有三种:
->EPOLL_CTL_ADD:往内核事件表中注册指定fd 相关的事件
->EPOLL_CTL_MOD:修改指定 fd 上的注册事件
->EPOLL_CTL_DEL:删除指定 fd 的注册事件
参 数 :int fd -- 要操作的文件描述符,也就是要内核事件表中监听的fd
参 数 :struct epoll_event -- 要监听的事件类型,epoll_event 结构指针类型。
返回值 :int ret -- 成功时返回 0,失败则返回 -1,并设置 errno
说 明 :epoll 的事件注册函数,用来操作内核事件表
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*
函数名 :epoll_wait;
参 数 :int epfd -- epoll_create 的返回值
参 数 :struct epoll_event* -- 内核事件表中得到的检测事件集合
参 数 :int maxevents -- 最大size
参 数 :int timeout -- 超时时间
返回值 :int ret -- 成功时返回就绪的文件描述符的个数,失败返回 -1 并设置 errno
说 明 :等待事件的发生,它在一段超时时间之内等待一组文件描述符上的事件,epoll_wait 函数如果检测到事件,
就将所有就绪的事件从内核事件表(epfd 参数决定)中复制到第二个参数 events 指向的数组中。
*/
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
Epoll events 对应的宏:
EPOLLIN: 表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT: 表示对应的文件描述符可以写;
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR: 表示对应的文件描述符发生错误;
EPOLLHUP: 表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
epoll 工作模式
LT(level trigger) 模式是默认模式
LT模式:电平触发,当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。
下次调用 epoll_wait 时,会再次响应应用程序并通知此事件。
ET(edge trigger)
边沿触发:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。
如果不处理,下次调用 epoll_wait 时,不会再次响应应用程序并通知此事件。
ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。
epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
单线程 epoll + ET 模式
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <errno.h>
#define SERVER_PORT 8000
#define MAX_EVENTS 1024 // epoll 最大监听事件数
#define BUFFER_SIZE 2048 // 缓冲区大小
#define MAX_CONNECTIONS 10000 // 最大连接数(用于 epoll_create1)
// 设置文件描述符为非阻塞
int set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) flags = 0;
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
int main() {
int server_fd, epoll_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len;
struct epoll_event ev, events[MAX_EVENTS];
// 1. 创建监听 socket
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 设置端口复用
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 绑定地址
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(SERVER_PORT);
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 监听
if (listen(server_fd, 1024) < 0) {
perror("listen failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 设置监听 socket 为非阻塞(accept 时需要)
set_nonblocking(server_fd);
// 2. 创建 epoll 实例
epoll_fd = epoll_create1(0);
if (epoll_fd < 0) {
perror("epoll_create1 failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 将 server_fd 添加到 epoll 监听可读事件(边缘触发 ET 模式)
ev.events = EPOLLIN | EPOLLET; // EPOLLET: 边缘触发
ev.data.fd = server_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev) < 0) {
perror("epoll_ctl: server_fd add failed");
close(server_fd);
close(epoll_fd);
exit(EXIT_FAILURE);
}
printf("epoll Server started on port %d\n", SERVER_PORT);
printf("Listening for connections (ET mode)...\n");
// 3. 主事件循环
while (1) {
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); // 永久阻塞等待
if (nfds < 0) {
if (errno == EINTR) continue; // 被信号中断,继续
perror("epoll_wait failed");
break;
}
// 处理所有就绪事件
for (int i = 0; i < nfds; i++) {
int fd = events[i].data.fd;
// 1. 如果是监听 socket 就绪 → 接受新连接
if (fd == server_fd && (events[i].events & EPOLLIN)) {
while (1) {
client_len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 所有连接已 accept 完毕
break;
} else {
perror("accept error");
break;
}
}
// 设置客户端 socket 为非阻塞
set_nonblocking(client_fd);
// 将新客户端加入 epoll 监听(ET 模式)
ev.events = EPOLLIN | EPOLLET | EPOLLONESHOT; // 可读 + ET + 单次通知
ev.data.fd = client_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev) == 0) {
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
printf("🔗 Accepted client: %s:%d (fd=%d)\n",
client_ip, ntohs(client_addr.sin_port), client_fd);
} else {
perror("epoll_ctl: client_fd add failed");
close(client_fd);
}
}
}
// 2. 如果是客户端 socket 就绪 → 处理数据
else if (events[i].events & (EPOLLIN | EPOLLERR | EPOLLHUP)) {
char buffer[BUFFER_SIZE];
ssize_t count;
while ((count = recv(fd, buffer, sizeof(buffer) - 1, 0)) > 0) {
buffer[count] = '\0';
printf("Received %zd bytes from fd %d: %s", count, fd, buffer);
// 回显
if (send(fd, buffer, count, 0) < 0) {
perror("send failed");
break;
}
printf("Sent %zd bytes back to fd %d\n", count, fd);
}
// recv 返回 0:客户端关闭连接
if (count == 0) {
printf("Client fd %d disconnected.\n", fd);
}
// 错误
else if (count < 0) {
if (errno != EAGAIN && errno != EWOULDBLOCK) {
perror("recv error");
}
}
// 从 epoll 删除并关闭
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
printf("Closed client fd %d\n", fd);
}
}
}
close(server_fd);
close(epoll_fd);
return 0;
}