Linux_高级IO
1.五种IO模型
2.高级IO重要概念
2.1同步通信 vs 异步通信(synchronous communication/ asynchronous communication)
2.2阻塞 vs 非阻塞
2.3理解这四者的关系
1. 同步 vs 异步
关心的是:通信双方怎么协作、结果怎么返回
同步(Synchronous)
- 你发起调用 → 必须等对方处理完 → 拿到结果才继续
- 你主动等结果
例子:
- 打电话:你说一句,等对方说完,你再说下一句
- 函数调用:
ret = func();必须等 func 执行完
特点:
- 顺序执行
- 逻辑简单
- 不等完不能干别的
异步(Asynchronous)
- 你发起调用 → 立刻返回,不用等结果
- 对方做完了 主动通知你(回调 / 事件 / 信号)
例子:
- 发邮件:发完就走,对方回信是后来通知你
- 网络异步请求、消息队列
特点:
- 不阻塞当前流程
- 靠回调 / 通知拿结果
- 并发能力强
2. 阻塞 vs 非阻塞
关心的是:调用时当前线程 / 进程会不会卡住
阻塞(Blocking)
- 调用发起 → 一直等,啥也不干,直到完成
- 线程被挂起,不占 CPU
例子:
recv()没数据就一直等scanf()等待输入
非阻塞(Non-blocking)
- 调用发起 → 立刻返回
- 成了 → 返回结果
- 没成 → 返回错误 / 状态,不卡住
- 你可以轮询,也可以去干别的
例子:
- 非阻塞 socket:没数据直接返回
EAGAIN - 异步 IO 的发起阶段
3. 四者真正关系:两个正交维度
它们不是二选一,而是可以组合:
组合 1:同步阻塞(最常见)
- 同步:你等结果
- 阻塞:调用时卡死不动
- 例子:普通文件读写、阻塞 socket 收发
组合 2:同步非阻塞
- 同步:最终还是要你自己拿结果
- 非阻塞:调用不卡住,立刻返回
- 你要轮询反复问:好了没?好了没?
- 例子:非阻塞 socket + 循环
recv
组合 3:异步阻塞(极少用)
- 异步:结果将来通知你
- 阻塞:你非要等着通知到来
- 等于把异步用成了同步,一般不这么写
组合 4:异步非阻塞(高性能标配)
- 异步:结果靠通知 / 回调
- 非阻塞:发起时绝不卡住
- 例子:
- Linux
aio epoll/io_uring- Node.js、Nginx、高性能网络框架
- Linux
2.4其他高级IO
2.5非阻塞IO
2.5.1fcntl
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
2.5.2实现函数SetNoBlock
void SetNoBlock(int fd) {
int fl = fcntl(fd, F_GETFL);
if (fl < 0) {
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
2.5.3轮询方式读取标准输入
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
void SetNoBlock(int fd) {
int fl = fcntl(fd, F_GETFL);
if (fl < 0) {
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
int main() {
SetNoBlock(0);
while (1) {
char buf[1024] = {0};
ssize_t read_size = read(0, buf, sizeof(buf) - 1);
if (read_size < 0) {
perror("read");
sleep(1);
continue;
}
printf("input:%s\n", buf);
}
return 0;
}
2.I/O多路转接之select
2.1初识select
2.2select函数原型
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
1. 参数详细说明
| 参数 | 作用 |
|---|---|
nfds |
需要监听的最大文件描述符 + 1(核心!select 会轮询 0 ~ nfds-1 的 fd) |
readfds |
要监听可读事件的 fd 集合(比如 socket 有数据可接收、文件有数据可读取) |
writefds |
要监听可写事件的 fd 集合(比如 socket 发送缓冲区有空位、文件可写入) |
exceptfds |
要监听异常事件的 fd 集合(比如 socket 收到带外数据 OOB) |
timeout |
超时时间(阻塞时长),分 3 种情况:1. NULL:永久阻塞,直到有事件触发2. tv_sec=0, tv_usec=0:非阻塞,立刻返回3. 非 0 值:阻塞指定时长(秒 + 微秒) |
辅助宏(操作 fd_set 集合)
fd_set 是一个位图结构,不能直接操作,必须用系统提供的宏:
// 清空 fd 集合
void FD_ZERO(fd_set *set);
// 将 fd 加入集合
void FD_SET(int fd, fd_set *set);
// 将 fd 从集合中移除
void FD_CLR(int fd, fd_set *set);
// 检查 fd 是否在集合中(返回非 0 表示存在)
int FD_ISSET(int fd, fd_set *set);
timeout 结构体定义
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微秒(1 秒 = 1000000 微秒)
};
2. 返回值说明
- 成功:返回触发事件的 fd 总数(可读 + 可写 + 异常的总数)。
- 超时:返回 0(没有任何 fd 触发事件)。
- 失败:返回 -1,并设置
errno(常见:EBADF无效 fd、EINTR被信号中断、EINVAL参数非法)。
2.3理解select执行过程
2.4socket就绪条件
2.5select的特点
2.6select缺点
2.7select使用示例: 检测标准输入输出
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>
int main() {
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(0, &read_fds)
for (;;) {
printf("> ");
fflush(stdout);
int ret = select(1, &read_fds, NULL, NULL, NULL);
if (ret < 0) {
perror("select");
continue;
}
if (FD_ISSET(0, &read_fds)) {
char buf[1024] = {0};
read(0, buf, sizeof(buf) - 1);
printf("input: %s", buf);
} else {
printf("error! invaild fd\n");
continue;
}
FD_ZERO(&read_fds);
FD_SET(0, &read_fds);
}
return 0;
}
2.8select使用示例
// 防止头文件重复包含(替代传统的#ifndef #define #endif)
#pragma once
// STL 容器:存储多个socket、fd和socket的映射关系
#include <vector>
#include <unordered_map>
// 函数对象:用于封装业务处理逻辑
#include <functional>
// select相关的系统头文件
#include <sys/select.h>
// 自定义的TCP Socket封装类(包含Socket/Bind/Listen/Accept/Recv/Send等接口)
#include "tcp_socket.hpp"
// 调试函数:打印fd_set中被监听的文件描述符(方便排查问题)
inline void PrintFdSet(fd_set* fds, int max_fd) {
printf("select fds: ");
// 遍历0 ~ max_fd的所有fd,打印被置位(监听)的fd
for (int i = 0; i < max_fd + 1; ++i) {
if (!FD_ISSET(i, fds)) {
continue;
}
printf("%d ", i);
}
printf("\n");
}
// 业务处理函数的类型别名:接收客户端请求,填充响应
// 参数:req-客户端请求内容,resp-输出参数,用于存储要返回给客户端的响应
typedef std::function<void (const std::string& req, std::string* resp)> Handler;
// Selector类:封装select I/O多路复用的核心逻辑
// 注意:该类仅保存TcpSocket对象的副本,不管理socket的内存生命周期
class Selector {
public:
// 构造函数:初始化fd_set和最大fd值
Selector() {
// 【关键】max_fd_必须初始化,否则默认随机值会导致select出错
max_fd_ = 0;
// 【关键】fd_set必须用FD_ZERO清空,否则内存随机值会监听无效fd
FD_ZERO(&read_fds_);
}
// 向Selector中添加一个监听的socket
// 参数:sock - 要监听的TCP socket对象
// 返回值:true-添加成功,false-添加失败(fd已存在)
bool Add(const TcpSocket& sock) {
// 获取socket对应的文件描述符
int fd = sock.GetFd();
printf("[Selector::Add] %d\n", fd);
// 检查fd是否已存在,避免重复添加
if (fd_map_.find(fd) != fd_map_.end()) {
printf("Add failed! fd has in Selector!\n");
return false;
}
// 1. 将fd和对应的socket存入映射表
fd_map_[fd] = sock;
// 2. 将fd加入读事件监听集合(select会监听该fd的可读事件)
FD_SET(fd, &read_fds_);
// 3. 更新最大fd值(select需要max_fd+1作为第一个参数)
if (fd > max_fd_) {
max_fd_ = fd;
}
return true;
}
// 从Selector中移除一个监听的socket
// 参数:sock - 要移除的TCP socket对象
// 返回值:true-移除成功,false-移除失败(fd不存在)
bool Del(const TcpSocket& sock) {
int fd = sock.GetFd();
printf("[Selector::Del] %d\n", fd);
// 检查fd是否存在,不存在则移除失败
if (fd_map_.find(fd) == fd_map_.end()) {
printf("Del failed! fd has not in Selector!\n");
return false;
}
// 1. 从映射表中删除fd对应的记录
fd_map_.erase(fd);
// 2. 从读事件监听集合中清除该fd
FD_CLR(fd, &read_fds_);
// 3. 重新计算最大fd值(从当前max_fd_往前找,效率更高)
// 原因:如果删除的是当前最大的fd,需要找到下一个最大的有效fd
for (int i = max_fd_; i >= 0; --i) {
// 找到第一个仍在监听集合中的fd,作为新的max_fd_
if (FD_ISSET(i, &read_fds_)) {
max_fd_ = i;
break;
}
// 如果遍历到0都没有有效fd,max_fd_会保持0(不影响select,因为select会处理0)
}
return true;
}
// 等待可读事件触发,返回就绪的socket列表
// 参数:output - 输出参数,存储就绪的TcpSocket对象
// 返回值:true-等待成功,false-等待失败(select调用出错)
bool Wait(std::vector<TcpSocket>* output) {
// 清空输出容器,避免残留旧数据
output->clear();
// 【核心注意点】select会修改传入的fd_set,因此必须用临时副本
// 若直接传read_fds_,会导致原集合被清空,后续无法继续监听
fd_set tmp = read_fds_;
// 调试:打印当前监听的fd集合
PrintFdSet(&tmp, max_fd_);
// 调用select,监听可读事件(只关注readfds,writefds和exceptfds设为NULL)
// 参数1:max_fd_ + 1(select会监听0 ~ max_fd_的所有fd)
// 参数5:timeout=NULL → 永久阻塞,直到有fd就绪
int nfds = select(max_fd_ + 1, &tmp, NULL, NULL, NULL);
if (nfds < 0) {
// select调用失败(如被信号中断、fd无效等)
perror("select");
return false;
}
// 遍历所有可能的fd,找出就绪的fd
// 【关键】循环条件必须是i < max_fd_ + 1,否则会漏掉最大的fd
for (int i = 0; i < max_fd_ + 1; ++i) {
// 检查fd是否在就绪的临时集合中
if (!FD_ISSET(i, &tmp)) {
continue;
}
// 找到就绪fd,从映射表中取出对应的socket,加入输出列表
output->push_back(fd_map_[i]);
}
return true;
}
private:
fd_set read_fds_; // 监听可读事件的fd集合(核心)
int max_fd_; // 监听的最大fd值(select的关键参数)
std::unordered_map<int, TcpSocket> fd_map_; // fd到TcpSocket的映射,快速查找
};
// TcpSelectServer类:基于Selector(select)实现的TCP服务器
class TcpSelectServer {
public:
// 构造函数:初始化服务器的IP和端口
// 参数:ip - 服务器绑定的IP地址(如"0.0.0.0"表示所有网卡)
// port - 服务器绑定的端口号
TcpSelectServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {
}
// 启动服务器,进入事件循环
// 参数:handler - 业务处理函数,由用户自定义(处理请求并生成响应)
// 返回值:true-启动成功(理论上不会返回,除非循环出错),false-启动失败
bool Start(Handler handler) const {
// 1. 创建监听socket(TCP)
TcpSocket listen_sock;
bool ret = listen_sock.Socket();
if (!ret) {
printf("创建监听socket失败!\n");
return false;
}
// 2. 绑定IP和端口
ret = listen_sock.Bind(ip_, port_);
if (!ret) {
printf("绑定IP:%s 端口:%d 失败!\n", ip_.c_str(), port_);
return false;
}
// 3. 开始监听(backlog=5,表示半连接队列长度)
ret = listen_sock.Listen(5);
if (!ret) {
printf("监听失败!\n");
return false;
}
printf("服务器启动成功,IP:%s 端口:%d,等待客户端连接...\n", ip_.c_str(), port_);
// 4. 创建Selector对象,用于监听fd的可读事件
Selector selector;
// 将监听socket加入Selector(监听其可读事件:有新客户端连接)
selector.Add(listen_sock);
// 5. 进入无限事件循环(服务器核心)
for (;;) {
// 存储就绪的socket列表
std::vector<TcpSocket> output;
// 等待可读事件触发
bool ret = selector.Wait(&output);
if (!ret) {
// select调用失败,跳过本次循环,继续监听
continue;
}
// 6. 遍历就绪的socket,处理事件
for (size_t i = 0; i < output.size(); ++i) {
// 区分事件类型:监听socket就绪 vs 客户端socket就绪
if (output[i].GetFd() == listen_sock.GetFd()) {
// 【事件1】监听socket就绪 → 有新客户端连接
TcpSocket new_sock; // 新客户端的socket
// 接受客户端连接(不关心客户端IP和端口,设为NULL)
listen_sock.Accept(&new_sock, NULL, NULL);
// 将新客户端socket加入Selector,监听其可读事件(有数据发送过来)
selector.Add(new_sock);
printf("新客户端连接,fd=%d\n", new_sock.GetFd());
} else {
// 【事件2】客户端socket就绪 → 有数据可读
std::string req, resp;
// 接收客户端请求
bool ret = output[i].Recv(&req);
if (!ret) {
// 接收失败(客户端断开连接/网络错误)
printf("客户端fd=%d 断开连接\n", output[i].GetFd());
// 从Selector中移除该fd
selector.Del(output[i]);
// 【关键】关闭socket,释放文件描述符
output[i].Close();
continue;
}
printf("收到客户端fd=%d 请求:%s\n", output[i].GetFd(), req.c_str());
// 调用用户自定义的业务处理函数,生成响应
handler(req, &resp);
// 将响应发送回客户端
output[i].Send(resp);
printf("向客户端fd=%d 发送响应:%s\n", output[i].GetFd(), resp.c_str());
}
} // end for (遍历就绪socket)
} // end for (;;) 事件循环
return true;
}
private:
std::string ip_; // 服务器绑定的IP地址
uint16_t port_; // 服务器绑定的端口号
};
3.I/O多路转接之poll [选学]
3.1poll函数接口
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// pollfd结构
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
3.2socket就绪条件
3.3poll 相对于 select 的 优点(解决了 select 的硬伤)
-
没有 1024 个 fd 的上限
- select:受
fd_set位图限制,最多监听 1023 个 fd。 - poll:用动态数组
struct pollfd[],大小你自己定,只受系统最大打开文件数限制。
- select:受
-
不需要每次重新构造、重置监听集合
- select:
fd_set会被内核直接修改,每次select前必须重新拷贝、重置。 - poll:把要监听的事件存在
events字段,就绪结果存在revents字段。原始监听集合不会被破坏,下一轮循环直接复用,代码更简单、更少坑。
- select:
-
参数更简单,不用算 max_fd+1
- select:必须传
max_fd + 1,算错就漏监听。 - poll:直接传数组长度即可,心智负担极低。
- select:必须传
-
支持的事件类型更丰富poll 可以监听:
- 可读 POLLIN
- 可写 POLLOUT
- 带外数据 POLLPRI
- 挂断 POLLHUP
- 错误 POLLERR比 select 只有读 / 写 / 异常强得多。
-
代码更干净、更易封装不需要维护
max_fd、不需要备份fd_set,封装成类比 select 简单太多。
3.4poll 依然存在的缺点(和 select 一样的性能瓶颈)
-
本质还是 O (n) 轮询,效率不高
- 每次调用都要从头到尾遍历整个 pollfd 数组。
- 1 万个 fd 里只有 1 个就绪,也要扫 1 万次。
-
用户态 ↔ 内核态数据拷贝开销大
- 每次
poll()都要把整个pollfd[]从用户态拷贝到内核态。 - fd 越多,拷贝开销越大。
- 每次
-
内核没有缓存就绪队列
- 内核每次都要全量扫描所有 fd,不会记住哪些上次就绪。
- 高并发下性能急剧下降。
-
不支持高效的边缘触发(ET)
- poll 默认水平触发(LT)。
- 虽然某些系统支持边缘触发,但不通用、不稳定,不能像 epoll 那样大规模高性能使用。
-
无法直接拿到 “就绪列表”
- 你仍然要自己遍历所有 fd,检查
revents。 - 不像 epoll 直接返回就绪数组,不用瞎遍历。
- 你仍然要自己遍历所有 fd,检查
3.6poll示例: 使用poll监控标准输入
#include <poll.h>
#include <unistd.h>
#include <stdio.h>
int main() {
struct pollfd poll_fd;
poll_fd.fd = 0;
poll_fd.events = POLLIN;
for (;;) {
int ret = poll(&poll_fd, 1, 1000);
if (ret < 0) {
perror("poll");
continue;
}
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);
}
}
}
4.I/O多路转接之epoll
一、为什么需要 epoll?(对比 select/poll 痛点)
先回顾 select/poll 的核心问题,才能理解 epoll 的设计初衷:
| 痛点(select/poll) | epoll 的解决方案 |
|---|---|
| 最大 fd 数限制(select 1024) | 无上限,仅受系统最大打开文件数限制 |
| 每次调用要遍历所有 fd(O (n)) | 内核维护「就绪链表」,只返回就绪的 fd(O (1)) |
| 每次要拷贝所有 fd 到内核 | 仅在注册 / 修改时拷贝,后续复用(一次拷贝,多次使用) |
| 无缓存,每次全量扫描 fd | 内核缓存监听的 fd(红黑树),无需重复扫描 |
用通俗比喻理解:
- select/poll:你每天要挨个问 1 万个同事「有没有事」,哪怕只有 1 个人有事,也要问完 1 万次。
- epoll:你建了一个「有事请举手」的群,只有有事的人会主动举手,你只需要看举手的人就行。
二、epoll 核心概念
epoll 的核心是内核态维护的三个关键结构,不用记底层源码,记住功能即可:
1. epoll 实例(epoll_fd)
- 用
epoll_create()创建,是一个特殊的文件描述符(epoll_fd)。 - 每个 epoll 实例对应内核中的两个核心结构:
- 红黑树:存储你要监听的所有 fd 和对应的事件(比如可读、可写)→ 相当于「监听注册表」。
- 就绪链表:内核主动把就绪的 fd 加入这个链表 → 相当于「待办事项列表」。
2. 事件触发模式(LT/ET)
epoll 支持两种触发模式,这是新手最容易混淆的点,用大白话解释:
| 模式 | 全称 | 通俗解释 | 特点 |
|---|---|---|---|
| LT(默认) | Level Trigger 水平触发 | 只要 fd 有数据 / 事件,就一直通知你,直到你处理完 | 简单、不易漏事件,新手首选 |
| ET | Edge Trigger 边缘触发 | 只在 fd 状态「变化」时通知一次(比如从无数据→有数据),必须一次性处理完所有数据 | 高效、减少通知次数,高并发首选(需配合非阻塞 I/O) |
举例子:客户端给你发了 1000 字节数据:
- LT:你第一次读了 500 字节,epoll 还会继续通知你「还有数据」,直到你读完 1000 字节。
- ET:只通知你一次「有数据」,如果你只读了 500 字节,剩下的 500 字节不会再通知,除非客户端又发新数据。
3. 核心事件结构(epoll_event)
struct epoll_event {
uint32_t events; // 要监听的事件/就绪的事件(比如 EPOLLIN 可读)
epoll_data_t data;// 自定义数据,通常存 fd(方便后续处理)
};
// data 是一个联合体,最常用的是 fd 字段
typedef union epoll_data {
void *ptr;
int fd; // 存储要监听的文件描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;
常用事件宏:
EPOLLIN:fd 可读(比如 socket 有数据、有新连接)。EPOLLOUT:fd 可写(比如 socket 发送缓冲区有空位)。EPOLLERR:fd 出错。EPOLLHUP:fd 挂断(比如客户端断开)。EPOLLET:启用边缘触发(默认 LT)。EPOLLONESHOT:只监听一次事件,触发后需重新注册。
三、epoll 三个核心函数
epoll 的 API 非常简洁,只有 3 个核心函数:
1. epoll_create ():创建 epoll 实例
#include <sys/epoll.h>
// size 参数:已废弃(历史遗留),传任意正数即可(比如 1024)
// 返回值:成功返回 epoll_fd(epoll 实例的句柄),失败返回 -1
int epoll_create(int size);
- 作用:在内核中创建 epoll 实例,初始化红黑树和就绪链表。
- 示例:
int epoll_fd = epoll_create(1024);
2. epoll_ctl ():控制监听事件(增 / 删 / 改)
// epfd:epoll 实例的 fd(epoll_create 的返回值)
// op:操作类型:
// EPOLL_CTL_ADD:添加 fd 和监听事件
// EPOLL_CTL_MOD:修改 fd 的监听事件
// EPOLL_CTL_DEL:删除 fd 的监听
// fd:要监听的文件描述符(比如 listen_sock、client_sock)
// event:要监听的事件(epoll_event 结构体)
// 返回值:成功返回 0,失败返回 -1
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 核心作用:往 epoll 实例的红黑树里「加 / 删 / 改」监听的 fd 和事件。
- 示例(添加监听可读事件):
struct epoll_event ev; ev.events = EPOLLIN; // 监听可读事件 ev.data.fd = listen_fd; // 绑定要监听的 fd epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev);
3. epoll_wait ():等待就绪事件
// epfd:epoll 实例的 fd
// events:输出参数,存储就绪的事件(epoll_event 数组)
// maxevents:events 数组的最大长度(最多接收多少个就绪事件)
// timeout:超时时间(毫秒):
// -1:永久阻塞,直到有事件就绪
// 0:非阻塞,立刻返回
// >0:阻塞指定毫秒数
// 返回值:成功返回就绪的事件数,超时返回 0,失败返回 -1
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 核心作用:从 epoll 实例的就绪链表中取出就绪事件,无需遍历所有 fd。
- 示例:
struct epoll_event events[1024]; // 存储就绪事件,大小自定义 // 等待就绪事件,永久阻塞 int n = epoll_wait(epoll_fd, events, 1024, -1); // 遍历就绪事件 for (int i = 0; i < n; ++i) { int fd = events[i].data.fd; // 取出就绪的 fd if (events[i].events & EPOLLIN) { // 处理可读事件 } }
四、epoll 基本使用流程(新手实战模板)
结合 TCP 服务器场景,给你一个极简的 epoll 使用模板(新手可直接参考):
#include <stdio.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#define MAX_EVENTS 1024 // 最多处理 1024 个就绪事件
int main() {
// 1. 创建监听 socket(TCP)
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_addr = {
.sin_family = AF_INET,
.sin_port = htons(8080),
.sin_addr.s_addr = INADDR_ANY
};
bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
listen(listen_fd, 10);
// 2. 创建 epoll 实例
int epoll_fd = epoll_create(1024);
if (epoll_fd == -1) {
perror("epoll_create failed");
return 1;
}
// 3. 注册监听 socket 的可读事件(LT 模式)
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = listen_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev);
// 4. 事件循环
struct epoll_event ready_events[MAX_EVENTS];
while (1) {
// 4.1 等待就绪事件(永久阻塞)
int n = epoll_wait(epoll_fd, ready_events, MAX_EVENTS, -1);
if (n == -1) {
perror("epoll_wait failed");
break;
}
// 4.2 遍历就绪事件
for (int i = 0; i < n; ++i) {
int fd = ready_events[i].data.fd;
// 事件1:监听 socket 就绪 → 新客户端连接
if (fd == listen_fd) {
int client_fd = accept(listen_fd, NULL, NULL);
// 注册客户端 socket 的可读事件
ev.events = EPOLLIN;
ev.data.fd = client_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev);
printf("新客户端连接:fd=%d\n", client_fd);
}
// 事件2:客户端 socket 就绪 → 有数据可读
else if (ready_events[i].events & EPOLLIN) {
char buf[1024] = {0};
ssize_t ret = read(fd, buf, sizeof(buf)-1);
if (ret <= 0) {
// 客户端断开,删除监听
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
printf("客户端断开:fd=%d\n", fd);
} else {
printf("收到数据(fd=%d):%s\n", fd, buf);
// 简单回显
write(fd, buf, ret);
}
}
}
}
// 5. 清理资源
close(listen_fd);
close(epoll_fd);
return 0;
}
五、epoll vs select/poll 核心对比
| 特性 | select | poll | epoll |
|---|---|---|---|
| 最大 fd 数 | 1024(FD_SETSIZE) | 无上限 | 无上限 |
| 遍历方式 | 遍历所有 fd(O (n)) | 遍历所有 fd(O (n)) | 只遍历就绪 fd(O (1)) |
| 数据拷贝 | 每次调用全量拷贝 | 每次调用全量拷贝 | 仅注册 / 修改时拷贝 |
| 触发模式 | 仅 LT | 仅 LT | LT/ET(推荐 ET + 非阻塞) |
| 适用场景 | 小并发(<1000 fd) | 中小并发(<5000 fd) | 高并发(万级 + fd) |
| 系统支持 | 跨平台(Linux/BSD/macOS) | 跨平台 | 仅 Linux |
5.epoll工作原理

一、核心结构体拆解
1. eventpoll:epoll 实例的内核本体
对应我们调用 epoll_create() 得到的 epoll 文件描述符(epfd),是 epoll 的「大脑」,包含:
rbr:红黑树的根节点,用来管理所有被监听的 fd- 每个被监听的 fd 都会封装成
epitem节点,挂载到这棵红黑树上 - 红黑树的 key 是 fd 本身,增删改查时间复杂度 O (log n),高效管理海量 fd
- 每个被监听的 fd 都会封装成
rdllist:双向链表(就绪链表),用来存放已经就绪的 fd- 只有触发了事件的 fd 对应的
epitem才会被链入这个链表 epoll_wait()时,内核直接把这个链表的节点拷贝给用户态,无需遍历所有 fd
- 只有触发了事件的 fd 对应的
wq/pwqlist:等待队列,用于阻塞epoll_wait()调用,直到有事件就绪lock/mtx:同步锁,保证多线程操作红黑树和就绪链表的线程安全ovflist:溢出链表,处理高并发下临时的就绪节点user:关联的用户空间信息
2. epitem:被监听 fd 的封装节点
每个被 epoll_ctl() 加入监听的 fd,都会被内核封装成一个 epitem 结构体,它有「双重身份」:
rbn:红黑树节点,让epitem挂载到eventpoll的红黑树rbr上,用于管理监听集合rdllink:双向链表节点,当 fd 就绪时,epitem会通过这个成员链入eventpoll的rdllist(就绪链表)ffd:关联的文件描述符信息(包含 fd 本身)event:要监听的事件(如EPOLLIN可读、EPOLLOUT可写)- 其他成员:
next/pwqlist/ep/fllink等,用于链表管理、等待队列绑定和关联到eventpoll实例
二、epoll 完整工作流程(结合结构)
1. 创建 epoll 实例(epoll_create())
内核创建 eventpoll 结构体:
- 初始化红黑树
rbr(空树)、就绪链表rdllist(空链表) - 初始化锁、等待队列等同步机制
- 返回 epoll 文件描述符(epfd),用户态通过它操作这个
eventpoll实例
2. 注册 / 修改 / 删除监听 fd(epoll_ctl())
以「添加监听 fd」(EPOLL_CTL_ADD)为例:
- 内核为目标 fd 创建
epitem,填充ffd(关联 fd)、event(监听事件)等 - 将
epitem的rbn挂载到eventpoll的红黑树rbr上- 红黑树保证了海量 fd 下的快速查找 / 插入 / 删除(O (log n))
- 将
epitem绑定到对应 fd 的设备等待队列(如 socket 的接收队列)- 当 fd 上有事件发生时(如 socket 收到数据),设备驱动会唤醒等待队列,触发
epitem进入就绪状态
- 当 fd 上有事件发生时(如 socket 收到数据),设备驱动会唤醒等待队列,触发
修改(EPOLL_CTL_MOD)/ 删除(EPOLL_CTL_DEL):
- 修改:通过红黑树找到
epitem,更新event监听事件 - 删除:通过红黑树找到
epitem,从红黑树和设备等待队列中移除,若在就绪链表中也会被清除
3. 等待并获取就绪事件(epoll_wait())
- 调用
epoll_wait()时:- 内核先检查
eventpoll的rdllist是否为空- 若不为空:直接将
rdllist中的epitem拷贝到用户态的events数组,返回就绪事件数 - 若为空:当前进程 / 线程阻塞在
eventpoll的等待队列wq上,直到有事件就绪或超时
- 若不为空:直接将
- 内核先检查
- 当某个 fd 触发事件时:
- 设备驱动唤醒等待队列,找到对应的
epitem,将其通过rdllink链入eventpoll的rdllist - 若有进程在
wq上阻塞,会被唤醒,然后执行「拷贝就绪事件到用户态」的操作
- 设备驱动唤醒等待队列,找到对应的
- 用户态遍历返回的
events数组,直接处理就绪的 fd 和事件,无需遍历所有监听的 fd
三、epoll 高性能的本质(从结构看)
- O (log n) 管理监听集合:红黑树
rbr替代了 select/poll 的线性遍历,海量 fd 下的增删改查效率极高 - O (k) 获取就绪事件:就绪链表
rdllist只存就绪的 fd,epoll_wait()直接拷贝就绪节点,避免了 select/poll 中 O (n) 遍历所有 fd 的开销 - 一次注册,多次复用:
epitem只在epoll_ctl()时绑定到内核,后续epoll_wait()无需重复拷贝所有 fd 到内核,减少了用户态↔内核态的开销 - 内核主动通知:fd 就绪时由内核主动将
epitem加入就绪链表,而非用户态轮询,实现了「事件驱动」
四、图中结构的直观理解
- 下方的红黑树:代表所有被监听的 fd 集合,每个节点是
epitem的rbn - 上方的双向链表:代表就绪的 fd 集合,每个节点是
epitem的rdllink epitem是连接两者的桥梁:既是红黑树的管理节点,又是就绪链表的事件节点,保证了监听状态和就绪状态的一致性
6.epoll的优点(和 select 的缺点对应)
6.1epoll工作方式
你正在吃鸡, 眼看进入了决赛圈, 你妈饭做好了, 喊你吃饭的时候有两种方式:
1. 如果你妈喊你一次, 你没动, 那么你妈会继续喊你第二次, 第三次...(亲妈, 水平触发)
2. 如果你妈喊你一次, 你没动, 你妈就不管你了(后妈, 边缘触发)
6.2对比LT和ET
6.3理解ET模式和非阻塞文件描述符
6.4epoll的使用场景
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)