一、引言
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