IO 复用之 epoll

Posted by KalosAner on January 23, 2025

一、引言

IO复用之 select 介绍了基于 select 的 IO 复用方法,但是 select 函数每次需要向操作系统传递监视对象信息导致性能太低,并且每次都需要对所有的文件描述符进行遍历查看是否有事件到来。select 适用于服务器端访问量小需求可移植性的程序。

有一种方式只需要向操作系统传递一次监视对象,当监视范围或者内容发送变化时,函数只返回发生变化的部分。在 Linux 上可以通过 epoll 来实现,Windows 上通过 IOCP 方法。

使用 epoll 需要正确区分条件触发(Level Trigger)和边缘触发(Edge Trigger)。

条件触发

当输入/输出缓冲可读/可写则一直触发通知。

边缘触发

当输入/输出缓冲从不可读/不可写变为可读/可写时触发一次通知。

边缘触发必须使用非阻塞套接字,这是由边缘触发模式的设计特性和操作系统内核行为共同决定的。

二、epoll 介绍

epoll 方法用到的函数有三个:

  • epoll_create:创建保存 epoll 文件描述符的空间。
  • epoll_ctl:向空间注册或注销文件描述符
  • epoll_wait:等待文件描述符发生变化。

函数原型:

1
2
3
4
5
6
7
8
9
10
11
12
#include <sys/epoll.h>  

int epoll_create(int size);  
// 早期内核需要指定事件表大小,现版本已经忽略,传入任意大于 0 的数即可

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);  
// epfd:epoll_create 返回的文件描述符,op:操作类型,
// fd:需操作的目标文件描述符,event:指向epoll_event 的指针

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);  
// events:传出参数,存储就绪事件的数组,maxevents:数组大小,需大于 0,
// timeout:阻塞时长,-1代表无限阻塞,0代表非阻塞,正数代表最长等待时长

op:操作类型:

  • EPOLL_CTL_ADD:注册新事件。

  • EPOLL_CTL_MOD:修改已有事件。

  • EPOLL_CTL_DEL:删除事件

结构体原型:

1
2
3
4
struct epoll_event {  
    uint32_t   events;   // 事件类型掩码(位标志)  
    epoll_data_t data;    // 用户自定义数据(联合体)  
};  

events:监听或触发的事件类型,常用值有:

  • EPOLLIN:可读(包括对端关闭)。
  • EPOLLOUT:可写。
  • EPOLLERR:错误(默认监听,无需显式设置)。
  • EPOLLET:启用边缘触发模式(ET)。
  • EPOLLPRI:收到 OOB 数据的情况。
  • EPOLLRDHUP:断开连接或者半关闭的情况,这在边缘触发方式下非常有用
  • EPOLLONESHOT:发生一次事件后,相应文件描述符不再收到时间通知。需要向 epoll_ctl 函数的第二个参数传递 EPOLL_CTL_MOD,再次设置事件。
1
2
3
4
5
6
typedef union epoll_data {  
    void* ptr;   // 指向自定义数据结构(如连接上下文),
    int fd;      // 存储文件描述符(最常用)  
    uint32_t u32;  
    uint64_t u64;  
} epoll_data_t;  

简单场景用 fd 传递套接字,复杂场景用 ptr 传递连接对象

void* ptr 应用场景:

1
2
3
4
5
6
7
8
9
struct Connection {
    int fd;
    void* buffer;
};
Connection* conn = malloc(sizeof(Connection));
conn->fd = sockfd;
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.ptr = conn;  // 存储自定义结构体指针

uint32_t u32 一般存储标记协议类型(如 HTTP=1001、WebSocket=1002)或者状态码、线程 ID 或枚举值。

uint64_t u64 一般存储局唯一 ID(如会话 ID、请求 ID)或者时间戳、计数器等大范围数值。

三、使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 创建 epoll 实例  
int epfd = epoll_create(1);  

// 注册 socket 可读事件(ET 模式)  
struct epoll_event ev;  
ev.events = EPOLLIN | EPOLLET;  
ev.data.fd = sockfd;  
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);  

// 等待事件  
struct epoll_event events[MAX_EVENTS];  
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
if (n == -1) {
    puts("epoll_wait() error");
    return 1;
}
for (int i=0; i<n; i++) {  
    if (events[i].events & EPOLLIN) {  
        handle_read(events[i].data.fd);  
    }  
}  

高并发服务器代码:echo_epollserv.c