目录
[1.五种 I/O 模型](#1.五种 I/O 模型)
[2.非阻塞 I/O 与 fcntl 详解](#2.非阻塞 I/O 与 fcntl 详解)
[3.1.fd_set 与 FD_XXX 宏](#3.1.fd_set 与 FD_XXX 宏)
[3.2.select 参数详解](#3.2.select 参数详解)
[poll 和 select 的核心区别](#poll 和 select 的核心区别)
[5.1.epoll 的三个核心接口](#5.1.epoll 的三个核心接口)
[5.3.LT(水平触发)与 ET(边缘触发)](#5.3.LT(水平触发)与 ET(边缘触发))
[5.4.关于 EPOLLOUT 的按需设置](#5.4.关于 EPOLLOUT 的按需设置)
1.五种 I/O 模型
I/O = 等待 + 拷贝,提高 I/O 效率的本质是降低等待的比重
| 模型 | 是否同步 | 等待方式 | 拷贝参与 | 效率关键点 |
|---|---|---|---|---|
| 阻塞 I/O | 同步 | 进程阻塞等待内核数据就绪 | 进程参与拷贝 | 等待时 CPU 无法处理其他任务 |
| 非阻塞 I/O | 同步 | 轮询检查数据是否就绪(不阻塞) | 进程参与拷贝 | 轮询浪费 CPU,但可同时处理多路 |
| 信号驱动 I/O | 同步 | 数据就绪时内核发信号通知 | 进程参与拷贝 | 等待不占用 CPU,但信号处理复杂 |
| 多路复用 I/O | 同步 | select/poll/epoll 统一等待 |
进程参与拷贝 | 单线程监听多个 fd,效率高 |
| 异步 I/O | 异步 | 内核完成等待 + 拷贝后通知 | 内核完成全部工作 | 进程仅发起请求,完全不参与 I/O |
- **同步IO:**进程主动等待或轮询,并参与数据拷贝阶段
- **异步IO:**进程发起请求后立即返回,内核做完所有事情后再通知进程
- 阻塞 vs 非阻塞 :等待数据就绪时进程是否挂起(多路复用也是阻塞),数据拷贝阶段 效率一致,等待数据就绪阶段,阻塞IO让出CPU(高效),非阻塞IO忙轮询(低效)
在同步 I/O 范畴内,多路复用(尤其是 epoll)因为可以单执行流管理大量连接且只通知就绪 fd,效率最高。但异步 I/O 理论上更高效,只是编程模型复杂,只有遇到明确性能瓶颈且确定是I/O模型导致的,才值得考虑切换到异步 I/O(了解)
2.非阻塞 I/O 与 fcntl 详解
2.1.fcntl
fcntl (file control) 是 POSIX 系统中用于对已打开的文件描述符进行各种控制操作的系统调用
(1)函数原型
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */);
- 功能:修改已打开文件描述符的属性
- 常见 cmd:F_GETFL:获取当前文件状态标志(返回值即标志位图,失败返回 -1)F_SETFL:设置文件状态标志(如 O_NONBLOCK、O_APPEND)
- 返回值:fcntl 的返回值取决于具体的 cmd 命令(位图或者整数)
(2)主要功能分类
- 复制一个现有的描述符(cmd=F_DUPFD).
- 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
- 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
- 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
- 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).
(3)文件状态标志
| 标志 | 值 | 说明 | 示例 |
|---|---|---|---|
O_RDONLY |
0 | 只读 | 不可修改 |
O_WRONLY |
1 | 只写 | 不可修改 |
O_RDWR |
2 | 读写 | 不可修改 |
O_APPEND |
0x2000 | 追加模式 | 每次写追加到末尾 |
O_NONBLOCK |
0x4000 | 非阻塞模式 | 无数据时立即返回 |
O_ASYNC |
0x2000 (不同系统) | 异步 I/O | 数据就绪发送信号 |
O_SYNC |
0x1000 | 同步写 | 等待数据落盘 |
(4)SetNoBlock
获取/设置文件状态标记, 将一个文件描述符设置为非阻塞
int SetNoBlock(int fd) {
int fl = fcntl(fd, F_GETFL);
if (fl < 0) {
perror("fcntl F_GETFL");
return -1;
}
if (fcntl(fd, F_SETFL, fl | O_NONBLOCK) < 0) {
perror("fcntl F_SETFL");
return -1;
}
return 0;
}
(5)非阻塞错误码
设置O_NONBLOCK后,当底层数据未就绪时:
| 操作 | 返回值 | errno | 说明 |
|---|---|---|---|
| 读(read/recv) | -1 | EAGAIN 或 EWOULDBLOCK(值相同) |
无数据可读 |
| 写(write/send) | -1 | EAGAIN 或 EWOULDBLOCK(都判断确保可移植性) |
发送缓冲区满 |
这不是真正的错误,而是**"资源临时不可用",需要稍后重试**
#include <errno.h>
#include <unistd.h>
ssize_t n = read(fd, buf, sizeof(buf));
if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 数据未就绪,不是错误
// 等待 epoll/select 通知可读
return 0; // 或 -2 表示需要重试
}
// 真正的错误
perror("read error");
return -1;
}
// 成功读取 n 字节
3.select
3.1.fd_set 与 FD_XXX 宏
fd_set 本质是一个位图,在内核中以 unsigned long 数组实现
**位图大小:**由 FD_SETSIZE 宏定义(通常 1024),因此单个 select 能监听的最大 fd 编号为 1023
- 操作宏
| 宏 | 原型 | 功能 |
|---|---|---|
FD_ZERO |
FD_ZERO(fd_set *set) |
清空集合(所有位清零) |
FD_SET |
FD_SET(int fd, fd_set *set) |
将 fd 加入集合(对应位置1) |
FD_CLR |
FD_CLR(int fd, fd_set *set) |
将 fd 从集合移除(对应位置0) |
FD_ISSET |
FD_ISSET(int fd, fd_set *set) |
检查 fd 是否在集合中(0=不在集合中) |
3.2.select 参数详解
select 是 I/O 多路复用函数,用于监视多个文件描述符的状态变化
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
- nfds:监听的最大 fd 值 +1,内核据此缩小遍历范围
- timeout:输入输出型参数,为NULL时阻塞等待;tv_sec = tv_usec = 0时非阻塞轮询;否则就等待指定时间,返回时会被更新为剩余时间,可能需要周期重复设置
- fd_set 也是输入输出型参数:输入时表示关心的 fd ,返回时表示就绪的 fd 。因此每次调用前必须重新设置
| 返回值 | 含义 | 说明 |
|---|---|---|
| > 0 | 就绪的文件描述符数量 | 有多少个 fd 发生了事件 |
| 0 | 超时 | 在指定时间内没有 fd 就绪 |
| -1 | 错误 | 调用失败,查看 errno |
3.3.select的优缺点
(1)优点:
- 单进程引入 select 后 ,进程不再被迫在某个慢速连接上傻等,而是由 select 统一监控所有连接,内核会精确唤醒进程并告知哪个 fd 先就绪,程序随即优先处理该事件并立即返回,从而在单线程内达成了并发的效果------即哪个客户端先发数据就先服务谁,完全解除了由于某个连接阻塞导致其他连接饥饿的耦合关系
- 单进程 select 实现的是I/O 多路复用式并发 ------它通过内核事件通知让单个线程在多个连接之间高效切换,属于并发模型的一种 ,只是它不依赖多线程。而多线程是另一种并发模型,它通过多个执行流来承载并发,在多核 CPU 上还能进一步实现并行
(2)缺点
- fd 数量有上限(可重新编译内核修改,但效率下降)
- 每次调用需拷贝 fd 集合到内核,返回时再拷贝回来
- 内核采用线性扫描所有监听的 fd,效率随 fd 数量线性下降
- 用户态需要遍历整个数组找出就绪 fd
3.4.简易selectServer实现的细节:
gitee.com/dongfanqi/homework6/commit/3c29645059401652d7bbae1c40b29ccee26a87e2
(1)listenfd 在 selec中监听的是读事件,新连接到来等价于读事件就绪;必须先通过 select 检测到 listenfd 可读后再调用 accept,否则 accept 在无连接时会阻塞(阻塞模式)或返回 EAGAIN(非阻塞模式)
"新连接到达"在语义上等同于"监听 socket 有新的数据/对象可被读取"
(2)select 采用水平触发(LT)模式:只要 fd 上还有未处理的数据/事件,select 就会持续通知,上层可以不处理,下次 select 仍然会返回该 fd,此时调用 read/accept 等操作不会阻塞(因为数据已就绪)
(3)为了让文件描述符能在 select 循环中跨函数传递并保持完整监控列表,必须使用自定义数组来持久化存储所有需要监听的文件描述符。这是因为 select 的 fd_set 是输入输出型参数,每次调用后只会保留就绪的 fd,导致原始监听集合丢失,因此需要借助数组充当"全量备份"------在每轮循环前遍历数组重建 fd_set,调用 select 等待事件,事后再遍历数组找出就绪的 fd 进行处理
(4)在使用 select 实现 I/O 多路复用时,必须遍历自定义数组来找出当前最大的文件描述符值,因为 select 的第一个参数 nfds 要求传入"最大文件描述符 + 1"以限定内核的监听范围。具体做法是先将 max_fd 初始化为监听套接字,然后在将每个有效客户端 fd 加入 fd_set 的同一循环中,逐一比较并更新 max_fd,确保 select 能够覆盖数组中所有需要监控的描述符
(5)每次 accept 获取的新连接 fd 绝不能立即阻塞读写,必须存入自定义数组的空闲槽位完成"注册",随后交由 select 统一托管等待;这是因为新连接刚建立时内核接收缓冲区极可能是空的,若此时直接调用 recv,会导致单执行流服务端瞬间卡死,丧失处理其他并发连接的能力
(6)select 返回后仅能告知文件描述符"可读",但无法区分这种"可读"究竟是监听套接字上的连接就绪(需调用 accept 接纳新客户端)还是普通客户端套接字上的数据就绪(需调用 recv 读取内容),因此必须在主循环中引入一个事件派发器的逻辑:通过判断 遍历自定义数组 与 FD_ISSET判断,决定当前 fd 是否为监听 fd,若是则执行注册逻辑将新连接存入数组,若否则执行读取逻辑
4.poll
4.1.poll参数详解
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
-
参数
struct pollfd {
int fd; // 要监控的文件描述符
short events; // 关心的事件(输入参数)
short revents; // 实际发生的事件(输出参数,由内核填充)
};
| 参数 | 含义 |
|---|---|
fds |
struct pollfd 数组首地址,存放所有要监控的 fd 及事件 |
nfds |
数组有效元素个数 |
timeout |
超时时间(毫秒),-1 表示永久阻塞 |
- 事件宏
| 事件宏 | 含义 |
|---|---|
POLLIN |
数据可读(包括连接就绪) |
POLLOUT |
数据可写 |
POLLERR |
发生错误 |
POLLHUP |
连接挂起(对端关闭) |
poll 和 select 的核心区别
| 对比项 | select |
poll |
|---|---|---|
| fd 集合存储方式 | 位图 fd_set(固定大小 1024) |
动态数组 struct pollfd[] |
| 最大 fd 数量限制 | 硬限制 FD_SETSIZE(通常 1024) |
无上限,只受内存限制 |
| 参数分离 | 输入输出共用 fd_set,需每次重建 |
分离设计 :events 是输入,revents 是输出 |
| 遍历复杂度 | O(n) 但 n 上限固定 | O(n),n 可以是任意大 |
| 移植性 | 几乎所有 Unix 系统 | POSIX 标准,Windows 有 WSAPoll |
4.2.poll的改进与局限
- struct pollfd 将事件输入(events) 与事件输出(revents) 分离,无需每次重置
- 事件类型通过宏位图定义(POLLIN、POLLOUT 等)
- 突破了 select 的 fd 数量限制(数组大小由用户指定,内存超限属于硬件问题)
- 缺点:内核仍需要线性扫描所有监听的 fd,数量大时效率低
4.3.简易pollServer实现的细节:
gitee.com/dongfanqi/homework6/commit/3c29645059401652d7bbae1c40b29ccee26a87e2
| 实现细节 | select |
poll |
共同点说明 |
|---|---|---|---|
| 等待机制 | 将当前进程/线程挂载到所有监听 fd 的等待队列上 | 完全相同 | 内核睡眠等待,任意 fd 就绪时唤醒进程 |
| 内核遍历方式 | 线性扫描所有监听的 fd(0 ~ nfds-1) |
线性扫描用户传入的 pollfd 数组(0 ~ nfds-1) |
O(n) 时间复杂度,n 为监听 fd 总数 |
| 事件就绪检测 | 调用每个 fd 对应的 poll 方法(file->f_op->poll) |
完全相同 | 内核通过 VFS 层调用设备驱动的 poll 函数检查状态 |
| 用户态/内核态拷贝 | select 调用时拷贝 fd_set 到内核,返回时拷贝回用户态 |
poll 调用时拷贝 pollfd 数组到内核,返回时拷贝回用户态 |
每次调用都有 O(n) 的内存拷贝开销 |
| 输入输出参数特性 | fd_set 在返回时被修改(覆盖为就绪 fd 集合) |
pollfd 的 revents 字段被修改(保留 events 不变) |
都需要区分"关心的事件"和"就绪的事件" |
| 就绪事件通知 | 返回就绪 fd 总数,用户需遍历整个 fd_set 找出具体就绪的 fd |
返回就绪 fd 总数,用户需遍历整个 pollfd 数组检查 revents |
用户态二次遍历不可避免 |
| 超时精度 | struct timeval(微秒级) |
int timeout(毫秒级) |
底层均转换为内核的 jiffies 或高精度定时器 |
poll 在接口设计上将输入事件(events)与输出事件(revents)分离,使得用户无需在每次调用前重建监听集合 ,同时通过传入用户自定义大小的数组打破了 select 对文件描述符数量的硬性限制;但其底层内核实现与 select 几乎完全一致------依然是线性扫描所有监听的文件描述符、依然需要在用户态与内核态之间拷贝整个数组、依然需要用户层二次遍历才能定位就绪事件,因此在大量并发场景下性能瓶颈与 select 无异,最终被内核基于事件驱动回调的 epoll 所取代
5.epoll
5.1.epoll 的三个核心接口
- epoll_create ------ 创建 epoll 实例
| 项目 | 说明 |
|---|---|
| 函数原型 | int epoll_create(int size); |
| 头文件 | #include <sys/epoll.h> |
| 参数 | size:历史遗留参数,内核已忽略 ,但必须 > 0 (建议填 1) |
| 返回值 | 成功:返回 epoll 实例的文件描述符 (epfd) 失败:返回 -1,并设置 errno |
| 核心作用 | 在内核中创建一个包含**红黑树(存储所有监听fd)和就绪链表(缓存已触发事件)**的eventpoll对象,并返回一个文件描述符作为后续高效事件管理的操作句柄 |
| 资源释放 | 使用完毕后必须 close(epfd),否则造成内核内存泄漏 |
| 现代替代 | Linux 2.6.8+ 推荐使用 epoll_create1(0),支持 EPOLL_CLOEXEC 标志,避免 fork 后 fd 泄露 |
- epoll_ctl ------ 控制 epoll 事件(增/删/改)
| 项目 | 说明 |
|---|---|
| 函数原型 | int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); |
| 头文件 | #include <sys/epoll.h> |
| 参数详解 | |
epfd |
epoll_create 返回的 epoll 实例 fd |
op |
操作类型 ,取值如下: • EPOLL_CTL_ADD ------ 向 epoll 实例注册 新的 fd • EPOLL_CTL_MOD ------ 修改 已注册 fd 的监听事件 • EPOLL_CTL_DEL ------ 移除已注册的 fd |
fd |
要操作的目标文件描述符(监听 socket 或客户端 socket) |
event |
指向 struct epoll_event 的指针,指定要监听的事件类型和携带的用户数据 |
| 返回值 | 成功:返回 0 失败:返回 -1,并设置 errno |
| 核心作用 | 对内核中的 epoll 红黑树执行增、删、改操作,相当于"告诉内核我关心这个 fd 的哪些事件" |
| 常见错误 | EEXIST(重复 ADD)、ENOENT(对不存在的 fd 执行 MOD/DEL)、EBADF(fd 无效) |
struct epoll_event字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
events |
uint32_t |
监听的事件掩码,常用: • EPOLLIN ------ 数据可读 • EPOLLOUT ------ 数据可写 • EPOLLET ------ 边缘触发模式 • EPOLLRDHUP ------ 对端半关闭 • EPOLLERR / EPOLLHUP ------ 错误/挂起(内核自动监听) |
data |
epoll_data_t |
联合体 ,携带用户自定义数据: • data.fd ------ 存放 fd 本身 • data.ptr ------ 存放自定义结构体指针(更灵活) |
epoll_event 结构体的 data 成员是一个联合体,最常用的是 data.fd,用于在事件触发时告知用户是哪个 fd 就绪了
- epoll_wait ------ 等待事件就绪
| 项目 | 说明 |
|---|---|
| 函数原型 | int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); |
| 头文件 | #include <sys/epoll.h> |
| 参数详解 | |
epfd |
epoll 实例 fd |
events |
输出参数 ,指向用户态数组,内核将就绪的事件拷贝到这里 |
maxevents |
数组容量,表示一次最多能接收多少个就绪事件 |
timeout |
超时时间(毫秒): • -1 ------ 永久阻塞,直到有事件 • 0 ------ 立即返回,不阻塞 • >0 ------ 等待指定毫秒数 |
| 返回值 | • >0 ------ 就绪的 fd 数量 • 0 ------ 超时,无事件 • -1 ------ 出错,检查 errno |
| 核心作用 | 当有事件就绪或超时时返回,内核只将真正就绪的 fd 填入 events 数组,用户态只需遍历返回的 n 个就绪项,而无需遍历全部被监控的 fd |
| 关键优势 | 内核事件通知机制是 O(1) 的(就绪事件通过回调直接入队),且返回给用户态时只拷贝实际就绪的 n 个事件,而非全部监控的 N 个;整体调用开销与监控总数 N 无关,只与就绪数 n 成正比 |
若就绪队列中事件数量超过 maxevents,内核只取前 maxevents 个拷贝到用户态,剩余节点保留在就绪队列中,下次 epoll_wait 继续返回
5.2.epoll原理
epoll_create() ──→ 创建 epfd(内核红黑树 + 就绪队列)
│
↓
epoll_ctl(ADD) ──→ 将 listen_fd 和 client_fd 挂到红黑树上
│
↓
epoll_wait() ──→ 按时等待,内核把就绪 fd 从就绪队列拷贝到用户态数组
│
↓
只遍历就绪的 n 个 fd(而不是全部监控的 fd)

- 红黑树:存储所有监听的 fd 及其事件(epoll_ctl 增删改操作)
- 就绪队列:双向链表,存放已就绪 fd 对应的节点
- 读回调机制:从网卡检测到数据的那一刻起,首先网卡触发硬中断 ,内核在硬中断处理中激活软中断 ,软中断通过回调函数 将数据包读取到内核内存并推入协议栈;随后协议栈进行数据解析处理操作,在确认数据有效负载进入Socket接收队列后,回调 Socket层的默认通知函数 ;该函数遍历Socket等待队列,执行epoll_ctl预先注册的ep_poll_callback回调,将对应的epitem结构体挂入eventpoll的就绪链表,并唤醒阻塞在epoll_wait上的用户进程,事件监测从主动轮询转变为被动通知
- 写回调机制是发送缓冲区从满到非满的被动反向通知:当网卡完成数据发送、通过硬中断-软中断路径释放内核缓冲区空间后,协议栈触发 ep_poll_callback 将对应 epitem 挂入就绪链表并唤醒进程,从而将"能否写入"的主动轮询转化为"空间腾出即通知"的硬件驱动事件模型。
(1)epoll优点
select/poll 模式:
用户进程 --[每次调用拷贝全量fd列表]--> 内核 --[O(N)轮询检查状态]--> 返回
epoll 模式:
用户进程 --[epoll_ctl仅拷贝一次fd]--> 内核红黑树
硬件中断 --[回调直接注入]--> 内核就绪链表
用户进程 --[epoll_wait仅从链表取数据]--> 返回 O(n)//n表示已就绪的fd个数
epoll 优点消除无效遍历 (回调机制) 与重复拷贝 (红黑树存储), 提升 fd 修改效率 (红黑树VS数组)
红黑树常驻免拷贝,链表就绪免轮询,中断回调改 O(N) 为 O(1)
- 事件驱动回调:硬件中断与协议栈进行事件监听操作,操作系统无需主动轮询,空闲连接不消耗 CPU
- O(1) 就绪检测:epoll_wait 只需检查就绪链表是否为空,时间复杂度极低
- O(N) 有效获取:仅需遍历实际发生事件的 N 个就绪 fd,无 select/poll 的全量遍历浪费
- 无上限连接:基于红黑树管理全量 fd,大小仅受系统内存限制
- 无重复拷贝:fd 事件数据在内核中常驻 (红黑树节点) ,用户态与内核态仅交换就绪事件列表
(2)选择红黑树而不是哈希表的原因
- 哈希表的 O(1) 假设了完美的哈希函数、无碰撞、且内核愿意承受扩容时的延迟。而服务器 5 万个长连接同时活跃时,一次 rehash 导致的延迟可能触发服务崩溃
- 红黑树的 O(logN) 虽然理论常数更大,但在 N=10 万时深度不超过 18 层,操作耗时方差极小。更重要的是,红黑树赋予了 epoll "无痛清理" 的能力:进程退出时,内核只需一个中序遍历就能有序、无空洞、无额外分配地销毁所有 epitem(描述 fd 及其事件的数据结构)
红黑树换来了 epoll 在任意负载形态下的"性能可预测性"
5.3.LT(水平触发)与 ET(边缘触发)

| 模式 | 通知时机 | 特点 |
|---|---|---|
| LT(默认) | 只要 fd 对应缓冲区有数据,epoll_wait 就会通知 |
编程简单,若不处理数据会反复通知,可能导致忙等 |
| ET | 仅当 fd 状态发生变化时(如无数据→有数据)通知 | 高效,要求非阻塞 I/O 并一次性读完数据,否则可能丢失事件 |
- ET 与 LT 效率对比 :若在 LT 下也采用非阻塞 I/O 并一次性读完数据,两者效率接近。ET 的优势在于减少事件通知次数,避免无谓的用户态/内核态切换
- LT 只读一次的情况下,ET 的通知效率更高,ET 的 IO 效率也更高,因为 ET 下程序员会把数据全部取走,tcp 就会给对方通告更大的窗口,从而在概率上让对方一次可以发送更多数据
- ET 只通知一次,会倒逼程序员在每次通知时把本轮数据都全部取走,循环读取,直到读取出错,fd默认是阻塞的,所以 ET 模式需要将fd设置为非阻塞
边缘触发 (ET) 与 水平触发 (LT) 的底层区别
- LT :在 epoll_wait 将 fd 返回给用户后,内核不会把该节点从"就绪队列"里彻底摘除,而是留在队列里做一个标记。下一次 epoll_wait 如果发现数据还没读完,这个 fd 依然在队列里,会再次返回。这保证了"只要还有数据,就一直通知"
- ET :在内核回调 ep_poll_callback 后,数据加入队列。用户 epoll_wait 取走数据时,该节点直接从就绪队列移除 。如果用户没有一次性把缓冲区读空,且后续没有新数据到达 (即没有新的硬件中断触发新的回调),这个 fd再也不会被返回给用户
5.4.关于 EPOLLOUT 的按需设置
写缓冲区通常有空闲空间,若一直监听 EPOLLOUT,epoll_wait 会频繁返回,浪费 CPU
正确做法:
- 默认不监听 EPOLLOUT
- 当需要发送数据时,直接调用 write/send
- 若返回 EAGAIN,说明缓冲区满,此时将数据存入用户态输出缓冲区,并监听 EPOLLOUT
- 当 EPOLLOUT 触发,尝试继续发送输出缓冲区数据;若发送完毕,取消监听 EPOLLOUT
读事件必须常设监听以持续感知对端数据到达,而写事件必须按需设置,即仅在应用层有数据待发送且因内核缓冲区满导致 write 返回 EAGAIN 时才临时注册 EPOLLOUT,并在数据全部发出后立即移除,以防止写事件始终就绪引发 epoll_wait 空转与 CPU 满载
5.5.简易epollServer实现的细节:
gitee.com/dongfanqi/homework6/commit/3c29645059401652d7bbae1c40b29ccee26a87e2
(1)编程技巧:禁止拷贝的类,通过继承编译器自动禁止拷贝
(2)使用类封装操作,不同类进行组合,并使用智能指针管理
(3)epoll_ctl ADD 时需要 struct epoll_event 保存用户关心的 fd,便于后续通知用户
(4)epoll_ctl DEL 时确保 fd 合法:先移除,再关闭。若先执行 close(fd),内核虽会异步清理 epoll 节点,但在清理完成前该整数值 fd 可能被操作系统立即回收并分配给新创建的连接,导致 epoll 中残留的旧 epitem 节点错误地将事件注入到不相关的新连接上,造成严重的逻辑串扰或程序崩溃;只有先调用 EPOLL_CTL_DEL 将该 fd 从红黑树和等待队列中彻底摘除,才能确保后续 close 的安全性
(5)直接调用 write/send,发送数据,然后根据返回值判断缓冲区是否已满,进而设置监听