I/O 多路转接之 poll
一、poll 函数接口
cpp
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// pollfd结构(描述单个监控对象)
struct pollfd {
int fd; /* 要监控的文件描述符(-1表示忽略此结构体) */
short events; /* 期望监控的事件(输入参数) */
short revents; /* 实际发生的事件(输出参数,由内核填充) */
};
二、参数说明
fds:struct pollfd类型数组,每个元素对应一个待监控的文件描述符(fd),包含 "要监控的 fd、期望事件、实际事件" 三部分,实现输入输出参数分离。nfds:fds数组中有效元素的数量(即实际要监控的 fd 总数),无需计算 "最大 fd+1"。timeout:超时时间,单位为毫秒(ms) ,取值分三种:-1:永久阻塞,直到有 fd 就绪或被信号中断;0:非阻塞模式,立即返回(仅检查当前 fd 状态,不等待);- 正数:最多阻塞
timeout毫秒,超时无事件则返回 0。
三、events 与 revents 的取值
events(输入)用于指定要监控的事件,revents(输出)用于返回实际发生的事件,常用取值如下:
| 事件宏 | 含义说明 |
|---|---|
POLLIN |
可读事件(如 socket 接收缓冲区有数据、对端正常关闭连接) |
POLLOUT |
可写事件(如 socket 发送缓冲区有空闲空间,可无阻塞写入数据) |
POLLERR |
错误事件(无需在events中设置,内核自动检测并填充到revents) |
POLLHUP |
挂起事件(如 socket 对端关闭连接,或 fd 关联的设备断开) |
POLLNVAL |
无效 fd 事件(如 fd 未打开或已关闭,revents中返回,提示参数错误) |
四、返回结果
- 返回值 < 0 :调用失败,错误原因存于
errno(如EINTR被信号中断、EINVAL参数无效); - 返回值 = 0:超时,指定时间内无任何 fd 就绪;
- 返回值 > 0 :就绪的 fd 数量(即
revents非 0 的pollfd结构体个数)。
五、socket 就绪条件
与 select 完全一致,核心判断标准如下:
读就绪
- socket 接收缓冲区字节数 ≥ 低水位标记(SO_RCVLOWAT),读操作不阻塞且返回值 > 0;
- 对端关闭连接(读操作返回 0);
- 监听 socket 有新连接请求;
- socket 发生错误(
revents中会同时包含POLLIN和POLLERR)。
写就绪
- socket 发送缓冲区空闲字节数 ≥ 低水位标记(SO_SNDLOWAT),写操作不阻塞且返回值 > 0;
- socket 写操作被关闭(如调用
shutdown(fd, SHUT_WR),写操作会触发SIGPIPE); - 非阻塞
connect成功或失败后。
异常就绪
- socket 收到带外数据(TCP 紧急数据),
revents中会包含POLLPRI。
六、poll 的优点
相比 select,poll 主要解决了以下痛点:
- 无 fd 数量限制 :select 受
FD_SETSIZE(默认 1024)限制,而 poll 通过数组长度nfds控制,理论上仅受系统资源(内存)限制; - 接口更友好 :
- 无需计算 "最大 fd+1",直接传入数组长度
nfds; events(输入)与revents(输出)分离,无需每次调用前重新初始化监控集合(数组可复用,仅需重置revents);
- 无需计算 "最大 fd+1",直接传入数组长度
- 错误事件处理更清晰 :
POLLERR、POLLHUP等错误事件无需手动监控,内核会自动填充到revents,减少用户代码冗余。
七、poll 的缺点
poll 本质仍属于 "遍历式" I/O 多路复用,与 select 存在共通缺陷:
- 遍历开销高 :poll 返回后,需遍历整个
fds数组检查revents,才能确定哪些 fd 就绪;当 fd 数量庞大(如万级)时,遍历耗时显著; - 用户态 - 内核态拷贝开销 :每次调用 poll,需将整个
fds数组从用户态拷贝到内核态;fd 数量越多,拷贝数据量越大,开销越高; - 效率线性下降:若大量 fd 处于未就绪状态,内核仍需逐一检查每个 fd 的状态,导致效率随 fd 数量增加而线性降低。
八、poll 使用示例:监控标准输入
cpp
#include <poll.h>
#include <unistd.h>
#include <stdio.h>
int main() {
struct pollfd poll_fd; // 单个pollfd结构体(监控标准输入)
poll_fd.fd = 0; // 标准输入的fd为0
poll_fd.events = POLLIN; // 期望监控"可读"事件
poll_fd.revents = 0; // 初始化输出事件为0
for (;;) { // 循环监控
// 调用poll:监控1个fd,超时1000ms(1秒)
int ret = poll(&poll_fd, 1, 1000);
if (ret < 0) { // 调用失败
perror("poll");
continue;
} else if (ret == 0) { // 超时无事件
printf("poll timeout\n");
continue;
}
// 检查是否为期望的可读事件
if (poll_fd.revents == POLLIN) {
char buf[1024] = {0};
read(0, buf, sizeof(buf) - 1); // 读取标准输入
printf("stdin:%s", buf);
poll_fd.revents = 0; // 重置输出事件,避免下次误判
}
}
return 0;
}
I/O 多路转接之 epoll
一、epoll 初识
按照 man 手册定义,epoll 是 "为处理大批量文件描述符而设计的改进版 poll",于 Linux 2.5.44 内核中引入。它解决了 select/poll 的效率瓶颈,是 Linux 2.6 + 下性能最优的 I/O 多路复用机制,被广泛用于高并发服务器(如 Nginx、Redis)。
核心优势:基于 "回调 + 红黑树 + 就绪队列",实现高效的 fd 监控,避免遍历和频繁拷贝。
二、epoll 的相关系统调用
epoll 通过 3 个核心系统调用完成功能,分别对应 "创建句柄、注册事件、等待就绪" 三步。
1. epoll_create:创建 epoll 句柄
cpp
#include <sys/epoll.h>
int epoll_create(int size);
- 功能 :创建一个 epoll 实例(句柄),内核会为其分配一个
eventpoll结构体(存储监控信息)。 - 参数
size:Linux 2.6.8 之后被忽略,仅需传入一个大于 0 的整数(历史用途是指定预分配的 fd 监控数量)。 - 返回值 :成功返回 epoll 句柄(非负整数),失败返回 - 1(错误存于
errno)。 - 注意 :epoll 句柄使用后需调用
close()关闭,避免资源泄漏。
2. epoll_ctl:注册 / 修改 / 删除事件
cpp
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
-
功能 :向 epoll 句柄(
epfd)中注册、修改或删f5t47u除指定 fd 的监控事件。 -
参数说明:
参数 含义 epfdepoll_create () 返回的 epoll 句柄 op操作类型,用 3 个宏表示:- EPOLL_CTL_ADD:新增 fd 及事件-EPOLL_CTL_MOD:修改已有 fd 的事件-EPOLL_CTL_DEL:删除 fd(event传 NULL)fd要监控的文件描述符 eventstruct epoll_event结构体,描述要监控的事件(op为EPOLL_CTL_DEL时传 NULL) -
核心结构体:
struct epoll_eventcppstruct epoll_event { uint32_t events; // 要监控的事件(宏集合) epoll_data_t data;// 关联数据(如fd、指针等,供用户态识别fd) }; // epoll_data_t是联合体,常用fd字段 typedef union epoll_data { void *ptr; // 指向用户自定义数据 int fd; // 关联的文件描述符(最常用) uint32_t u32; uint64_t u64; } epoll_data_t; -
events常用宏:事件宏 含义说明 EPOLLIN可读事件(同 select/poll,如接收缓冲区有数据、对端关闭) EPOLLOUT可写事件(同 select/poll,如发送缓冲区有空闲空间) EPOLLPRI紧急数据可读(如 TCP 带外数据) EPOLLERRfd 错误事件(内核自动检测,无需手动注册) EPOLLHUPfd 挂起事件(如对端关闭,内核自动检测) EPOLLET边缘触发(Edge Triggered,ET)模式(默认是水平触发 LT) EPOLLONESHOT单次监控模式(事件触发后,该 fd 自动从 epoll 中移除,需重新注册才能再次监控) -
返回值 :成功返回 0,失败返回 - 1(如
epfd无效、fd未打开、op操作非法)。
3. epoll_wait:等待事件就绪
cpp
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
-
功能:等待 epoll 句柄中监控的 fd 就绪,收集就绪事件并返回给用户态。
-
参数说明:
参数 含义 epfdepoll_create () 返回的 epoll 句柄 events用户态预分配的 struct epoll_event数组,用于存储内核返回的就绪事件(输出参数)maxeventsevents数组的最大长度(必须≤epoll_create () 的size,且大于 0)timeout超时时间(毫秒):- -1:永久阻塞-0:非阻塞,立即返回- 正数:阻塞指定毫秒 -
返回值:
- 成功:返回就绪的 fd 数量(即
events数组中有效元素的个数); - 0:超时,无 fd 就绪;
- -1:失败(错误存于
errno,如EINTR被信号中断)。
- 成功:返回就绪的 fd 数量(即
-
注意 :
events数组需用户提前分配内存(如struct epoll_event events[1024];),内核仅负责将就绪事件拷贝到数组中,不负责内存分配。
三、epoll 工作原理

epoll 的高效依赖内核中的eventpoll结构体及 "红黑树 + 双链表 + 回调" 的设计,核心流程如下:
1. 内核核心结构体:eventpoll
每个 epoll 句柄对应一个eventpoll结构体,关键成员如下:
cpp
struct eventpoll {
struct rb_root rbr; // 红黑树根节点:存储所有注册的fd及事件(高效增删查)
struct list_head rdlist; // 双链表:存储就绪的fd对应的事件(供epoll_wait读取)
// 其他成员(如等待队列、锁等)
};
同时,每个注册的 fd 会对应一个epitem结构体(挂载到红黑树和双链表):
cpp
struct epitem {
struct rb_node rbn; // 红黑树节点:用于挂载到eventpoll->rbr
struct list_head rdllink; // 双链表节点:就绪时挂载到eventpoll->rdlist
struct epoll_filefd ffd; // 关联的fd信息
struct eventpoll *ep; // 指向所属的eventpoll结构体
struct epoll_event event; // 注册的事件类型
};
2. 核心工作流程
- 创建 epoll 句柄 :调用
epoll_create(),内核创建eventpoll结构体(初始化红黑树和双链表)。 - 注册事件 :调用
epoll_ctl(EPOLL_CTL_ADD):- 内核创建
epitem结构体,关联 fd 和事件; - 将
epitem插入eventpoll的红黑树(红黑树保证增删查的时间复杂度为 O (log n)); - 为 fd 注册内核回调函数(
ep_poll_callback):当 fd 就绪时,回调函数会将epitem插入eventpoll的双链表(rdlist)。
- 内核创建
- 等待就绪 :调用
epoll_wait():- 若双链表
rdlist非空,直接将链表中的就绪事件拷贝到用户态的events数组,返回就绪数量; - 若双链表为空,进程阻塞在 epoll 的等待队列中,直到有 fd 就绪(回调函数触发,将
epitem加入双链表),或超时 / 被信号唤醒。
- 若双链表
- 处理就绪事件 :用户态遍历
events数组,根据data.fd和events处理对应的 IO 操作(如读、写)。
问题 1:你怎么看待就绪队列?答案:就绪队列,epoll 的本质是一个基于事件就绪的生产者消费者模型。epoll 接口是线程安全的。
问题 2:获取就绪事件,如果缓冲区大小不够了怎么办?答案:不影响,没拿完,默认给你保留着,下次拿。细节:内核会严格按照 0 下标开始,依次拷贝保存就绪事件和 fd。应用层处理就绪事件的时候,处理的全都是就绪的,基本不需要非法检测。
四、epoll 的优点(对比 select/poll)
- 接口更灵活 :拆分为
epoll_create/epoll_ctl/epoll_wait,无需每次调用都重新注册所有 fd(仅首次注册,后续修改 / 删除即可); - 数据拷贝轻量 :仅在
epoll_ctl注册 / 修改时拷贝 fd 信息到内核,epoll_wait仅拷贝就绪事件(而非所有注册的 fd),拷贝开销极低; - 无 fd 数量限制:红黑树存储注册的 fd,理论上仅受系统内存限制(支持十万级甚至百万级 fd 监控);
- 效率无衰减:无需遍历所有注册的 fd,仅需处理双链表中的就绪事件(时间复杂度为 O (1),与 fd 总数无关);
- 支持两种触发模式 :可通过
EPOLLET选择边缘触发(ET),减少epoll_wait的返回次数,进一步提升效率。
五、epoll 工作方式:水平触发(LT)与边缘触发(ET)
epoll 支持两种事件触发模式,核心差异在于 "fd 就绪后,内核通知用户态的次数",用 "妈妈喊吃饭" 的例子可直观理解:
1. 水平触发(Level Triggered,LT):默认模式
- 触发规则 :只要 fd 处于就绪状态(如读缓冲区有数据),每次调用
epoll_wait都会返回该 fd 的就绪事件,直到 fd 的就绪状态消失(如数据被读完)。 - 类比:妈妈喊你吃饭,你没动,妈妈会一直喊(直到你去吃)------"亲妈模式"。
- 示例场景 :
- socket 接收缓冲区收到 2KB 数据,
epoll_wait返回EPOLLIN; - 用户仅读取 1KB 数据,缓冲区剩余 1KB;
- 再次调用
epoll_wait,仍会返回EPOLLIN(直到 2KB 数据被读完)。
- socket 接收缓冲区收到 2KB 数据,
- 优点:编程简单,支持阻塞 / 非阻塞 IO;
- 缺点 :若用户未及时处理就绪事件,
epoll_wait会反复返回,可能增加系统调用次数。
2. 边缘触发(Edge Triggered,ET):需显式指定EPOLLET
- 触发规则 :fd 的就绪状态从 "未就绪" 变为 "就绪" 时,
epoll_wait仅返回一次该 fd 的就绪事件;后续即使 fd 仍处于就绪状态(如缓冲区有残留数据),epoll_wait也不会再返回,直到 fd 的就绪状态再次变化(如再次收到数据)。 - 类比:妈妈喊你吃饭,你没动,妈妈就不喊了 ------"后妈模式"。
- 示例场景 :
- socket 接收缓冲区收到 2KB 数据(状态从 "空"→"有数据"),
epoll_wait返回EPOLLIN; - 用户仅读取 1KB 数据,缓冲区剩余 1KB(状态仍为 "有数据",无变化);
- 再次调用
epoll_wait,不会返回该 fd 的EPOLLIN(需等待对端再次发送数据,状态变化后才触发)。
- socket 接收缓冲区收到 2KB 数据(状态从 "空"→"有数据"),
- 关键要求 :ET 模式下,fd 必须设为非阻塞(fd必须设置为非阻塞是为了避免阻塞操作导致的进程挂起),且需一次性读完 / 写完缓冲区中的所有数据(避免残留数据无法处理);
- 优点 :减少
epoll_wait的返回次数,降低系统调用开销,适合高并发场景(如 Nginx 默认使用 ET 模式); - 缺点 :编程复杂度高,需处理非阻塞 IO 的 "重试" 逻辑(如读数据时循环调用
read,直到返回EAGAIN/EWOULDBLOCK)。
3. LT 与 ET 对比总结
| 维度 | 水平触发(LT) | 边缘触发(ET) |
|---|---|---|
| 触发次数 | 就绪状态持续期间,每次epoll_wait都触发 |
仅状态从 "未就绪→就绪" 时触发一次 |
| fd 状态要求 | 支持阻塞 / 非阻塞 | 必须为非阻塞 |
| 数据处理要求 | 可分多次处理缓冲区数据 | 必须一次性处理完缓冲区所有数据 |
| 编程复杂度 | 低(无需重试) | 高(需循环重试,处理EAGAIN) |
| 效率 | 较低(可能多此系统调用) | 高(减少系统调用次数) |
| 适用场景 | 简单业务、低并发 | 高并发、高性能场景(如服务器) |
LT vs ET:谁更高效?
- ET通知效率更高,有效通知的数量最大
- ET尽快读完所有数据,可以给对方更新一个更大的win窗口,提高对方滑动窗口大小,提高网络发送的报文并发度(同一时间段内,网络设备(如服务器、客户端)能同时发送的报文数量)。
六、epoll 的使用场景
epoll 的高效并非普适,需结合业务场景选择:
- 适合场景:多连接、且仅少数连接活跃的高并发场景(如互联网 APP 的入口服务器、直播服务器)------ 此时 epoll 无需遍历大量 fd,仅处理活跃连接,效率优势明显。
- 不适合场景:连接数少且全部活跃的场景(如系统内部服务器间通信)------ 此时 epoll 的红黑树、回调等机制反而增加开销,效率可能不如 select/poll。
七、epoll 使用示例
示例 1:epoll 服务器(LT 模式)
1. 封装 Epoll 类(tcp_epoll_server.hpp)
cpp
#pragma once
#include <vector>
#include <functional>
#include <sys/epoll.h>
#include "tcp_socket.hpp" // 假设已实现TcpSocket类(封装socket/bind/listen/accept等)
// 业务处理回调函数类型:输入请求,输出响应
typedef std::function<void (const std::string& req, std::string* resp)> Handler;
class Epoll {
public:
Epoll() {
// 创建epoll句柄(size传10,2.6.8后忽略)
epoll_fd_ = epoll_create(10);
}
~Epoll() {
close(epoll_fd_); // 关闭epoll句柄,释放资源
}
// 向epoll中添加fd及事件(LT模式,默认监控EPOLLIN)
bool Add(const TcpSocket& sock) const {
int fd = sock.GetFd();
printf("[Epoll Add] fd = %d\n", fd);
struct epoll_event ev;
ev.data.fd = fd; // 关联fd
ev.events = EPOLLIN; // 监控可读事件(LT模式)
// 注册事件
int ret = epoll_ctl(epoll_fd_, EPOLL_CTL_ADD, fd, &ev);
if (ret < 0) {
perror("epoll_ctl ADD");
return false;
}
return true;
}
// 从epoll中删除fd
bool Del(const TcpSocket& sock) const {
int fd = sock.GetFd();
printf("[Epoll Del] fd = %d\n", fd);
// 删除时event传NULL
int ret = epoll_ctl(epoll_fd_, EPOLL_CTL_DEL, fd, NULL);
if (ret < 0) {
perror("epoll_ctl DEL");
return false;
}
return true;
}
// 等待fd就绪,输出就绪的TcpSocket
bool Wait(std::vector<TcpSocket>* output) const {
output->clear();
struct epoll_event events[1000]; // 预分配1000个就绪事件的空间
// 调用epoll_wait:永久阻塞(timeout=-1)
int nfds = epoll_wait(epoll_fd_, events, sizeof(events)/sizeof(events[0]), -1);
if (nfds < 0) {
perror("epoll_wait");
return false;
}
// 遍历就绪事件,收集对应的TcpSocket
for (int i = 0; i < nfds; ++i) {
TcpSocket sock(events[i].data.fd);
output->push_back(sock);
}
return true;
}
private:
int epoll_fd_; // epoll句柄
};
// Epoll服务器类
class TcpEpollServer {
public:
TcpEpollServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {}
// 启动服务器,传入业务处理回调
bool Start(Handler handler) {
// 1. 创建监听socket
TcpSocket listen_sock;
if (!listen_sock.Socket()) return false;
// 2. 绑定IP和端口
if (!listen_sock.Bind(ip_, port_)) return false;
// 3. 开始监听
if (!listen_sock.Listen(5)) return false;
// 4. 创建Epoll对象,添加监听socket
Epoll epoll;
if (!epoll.Add(listen_sock)) return false;
// 5. 事件循环
for (;;) {
// 6. 等待fd就绪
std::vector<TcpSocket> ready_socks;
if (!epoll.Wait(&ready_socks)) continue;
// 7. 处理就绪的fd
for (size_t i = 0; i < ready_socks.size(); ++i) {
if (ready_socks[i].GetFd() == listen_sock.GetFd()) {
// 7.1 监听socket就绪:处理新连接
TcpSocket new_sock;
listen_sock.Accept(&new_sock, NULL, NULL); // 接收新连接
epoll.Add(new_sock); // 将新连接的socket加入epoll监控
} else {
// 7.2 客户端socket就绪:处理请求
std::string req, resp;
bool ret = ready_socks[i].Recv(&req); // 读取请求(假设Recv是阻塞读)
if (!ret) {
// 客户端关闭连接:从epoll删除并关闭socket
epoll.Del(ready_socks[i]);
ready_socks[i].Close();
continue;
}
handler(req, &resp); // 调用业务回调生成响应
ready_socks[i].Send(resp); // 发送响应
}
}
}
return true;
}
private:
std::string ip_; // 服务器IP
uint16_t port_; // 服务器端口
};
2. 业务代码(dict_server.cc)
只需将服务器对象类型改为TcpEpollServer,传入业务回调(如字典查询)即可:
cpp
#include "tcp_epoll_server.hpp"
#include <unordered_map>
// 字典业务回调:输入单词,输出释义
void DictHandler(const std::string& req, std::string* resp) {
std::unordered_map<std::string, std::string> dict = {
{"apple", "苹果"},
{"banana", "香蕉"},
{"orange", "橙子"}
};
auto it = dict.find(req);
if (it != dict.end()) {
*resp = it->second;
} else {
*resp = "未找到该单词";
}
}
int main() {
TcpEpollServer server("0.0.0.0", 8080); // 监听所有IP的8080端口
server.Start(DictHandler); // 启动服务器,传入字典业务回调
return 0;
}
示例 2:epoll 服务器(ET 模式)
ET 模式需修改两部分:1. TcpSocket 类支持非阻塞 IO;2. Epoll 类支持注册 ET 事件。
1. 修改 TcpSocket 类(支持非阻塞读写)
在tcp_socket.hpp的TcpSocket类中添加非阻塞相关接口:
cpp
class TcpSocket {
public:
// 其他已有接口(Socket/Bind/Listen/Accept等)...
// 设置fd为非阻塞模式
bool SetNoBlock() {
int fl = fcntl(fd_, F_GETFL); // 获取当前状态标记
if (fl < 0) {
perror("fcntl F_GETFL");
return false;
}
// 添加非阻塞标志(O_NONBLOCK)
int ret = fcntl(fd_, F_SETFL, fl | O_NONBLOCK);
if (ret < 0) {
perror("fcntl F_SETFL");
return false;
}
return true;
}
// 非阻塞读:循环读取直到缓冲区为空(处理ET模式的残留数据)
bool RecvNoBlock(std::string* buf) const {
buf->clear();
char tmp[1024 * 10] = {0}; // 10KB临时缓冲区
for (;;) {
ssize_t read_size = recv(fd_, tmp, sizeof(tmp) - 1, 0);
if (read_size < 0) {
// EAGAIN/EWOULDBLOCK:非阻塞读时缓冲区为空,正常返回
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break;
}
perror("recv");
return false;
} else if (read_size == 0) {
// 对端关闭连接,返回false
return false;
}
tmp[read_size] = '\0';
*buf += tmp;
// 读取到的数据小于缓冲区大小,说明已读完所有数据
if (read_size < (ssize_t)sizeof(tmp) - 1) {
break;
}
}
return true;
}
// 非阻塞写:循环写入直到数据写完(处理发送缓冲区满的情况)
bool SendNoBlock(const std::string& buf) const {
ssize_t cur_pos = 0; // 当前写入位置
ssize_t left_size = buf.size(); // 剩余未写入数据大小
for (;;) {
ssize_t write_size = send(fd_, buf.data() + cur_pos, left_size, 0);
if (write_size < 0) {
// EAGAIN/EWOULDBLOCK:发送缓冲区满,重试
if (errno == EAGAIN || errno == EWOULDBLOCK) {
continue;
}
perror("send");
return false;
}
cur_pos += write_size;
left_size -= write_size;
// 所有数据写入完成,退出循环
if (left_size <= 0) {
break;
}
}
return true;
}
private:
int fd_; // 类内部存储的socket fd
};
2. 修改 Epoll 类(支持 ET 模式注册)
修改Epoll::Add方法,增加epoll_et参数控制是否启用 ET 模式:
cpp
class Epoll {
public:
// 其他接口不变...
// 添加fd,支持LT/ET模式(默认LT)
bool Add(const TcpSocket& sock, bool epoll_et = false) const {
int fd = sock.GetFd();
printf("[Epoll Add] fd = %d\n", fd);
struct epoll_event ev;
ev.data.fd = fd;
if (epoll_et) {
ev.events = EPOLLIN | EPOLLET; // ET模式:添加EPOLLET标志
} else {
ev.events = EPOLLIN; // LT模式
}
int ret = epoll_ctl(epoll_fd_, EPOLL_CTL_ADD, fd, &ev);
if (ret < 0) {
perror("epoll_ctl ADD");
return false;
}
return true;
}
// 其他接口不变...
};
3. 修改 TcpEpollServer 的 Start 方法(使用非阻塞 IO)
在处理新连接和客户端请求时,使用非阻塞读写接口:
cpp
bool TcpEpollServer::Start(Handler handler) {
// 1-3步(创建监听socket、绑定、监听)与LT模式一致...
Epoll epoll;
epoll.Add(listen_sock); // 监听socket仍用LT模式(ET模式需非阻塞accept,暂不实现)
for (;;) {
std::vector<TcpSocket> ready_socks;
if (!epoll.Wait(&ready_socks)) continue;
for (size_t i = 0; i < ready_socks.size(); ++i) {
if (ready_socks[i].GetFd() == listen_sock.GetFd()) {
// 处理新连接:设置为非阻塞,添加到epoll(ET模式)
TcpSocket new_sock;
listen_sock.Accept(&new_sock, NULL, NULL);
new_sock.SetNoBlock(); // 关键:ET模式必须设为非阻塞
epoll.Add(new_sock, true); // 启用ET模式
} else {
// 处理客户端请求:使用非阻塞读
std::string req, resp;
bool ret = ready_socks[i].RecvNoBlock(&req); // 非阻塞读
if (!ret) {
epoll.Del(ready_socks[i]);
ready_socks[i].Close();
continue;
}
handler(req, &resp);
ready_socks[i].SendNoBlock(resp); // 非阻塞写
printf("[client %d] req: %s, resp: %s\n", ready_socks[i].GetFd(), req.c_str(), resp.c_str());
}
}
}
return true;
}
附录:回调机制(内核代码片段)
epoll 的回调机制核心在于 fd 就绪时,内核通过ep_poll_callback将epitem加入就绪链表,关键代码如下:
1. epoll_ctl 中的回调注册
cpp
static int ep_insert(struct eventpoll *ep, struct epoll_event *event, struct file *tfile, int fd) {
// 其他初始化...
struct ep_pqueue epq;
epq.epi = epi;
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc); // 注册回调函数ep_ptable_queue_proc
// 其他逻辑...
}
// 为fd注册内核回调
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead, poll_table *pt) {
struct epitem *epi = ep_item_from_epqueue(pt);
struct eppoll_entry *pwq;
if ((pwq = kmem_cache_alloc(pwq_cache, SLAB_KERNEL))) {
init_waitqueue_func_entry(&pwq->wait, ep_poll_callback); // 回调函数设为ep_poll_callback
pwq->whead = whead;
pwq->base = epi;
add_wait_queue(whead, &pwq->wait); // 将回调加入fd的等待队列
// 其他逻辑...
}
}
2. 就绪回调函数:ep_poll_callback
cpp
static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key) {
struct epitem *epi = ep_item_from_wait(wait);
struct eventpoll *ep = epi->ep;
write_lock_irqsave(&ep->lock, flags);
// 将就绪的epitem加入eventpoll的就绪链表(rdlist)
list_add_tail(&epi->rdllink, &ep->rdlist);
write_unlock_irqrestore(&ep->lock, flags);
// 唤醒阻塞在epoll_wait的进程
wake_up_interruptible(&ep->wq);
// 其他逻辑...
}
3. socket 就绪时的回调触发
socket 接收数据后,内核会调用sock_def_readable,唤醒等待队列中的回调:
cpp
void sock_def_readable(struct sock *sk, int len) {
read_lock(&sk->sk_callback_lock);
if (sk->sk_sleep && waitqueue_active(sk->sk_sleep)) {
wake_up_interruptible(sk->sk_sleep); // 唤醒等待队列,触发ep_poll_callback
}
// 其他逻辑...
}