玳瑁的嵌入式日记D33-0904(IO多路复用)
一、核心概念
- 定义:单线程 / 单进程同时监测多个文件描述符是否可执行 IO 操作的能力
- 核心价值:用更少的资源(减少进程 / 线程创建开销、避免上下文切换、解决资源竞争)处理更多事件流
- 并发本质:逻辑控制流在时间上的重叠,通过 CPU 时分复用实现
二、为何需要 IO 多路复用?
传统并发处理方案(多进程 / 多线程)存在明显成本:
- 进程 / 线程创建与销毁的资源开销
- 上下文切换(Context Switch)的性能损耗
- 多线程间的资源竞争与同步问题
IO 多路复用提供了单线程处理多事件流的高效方案
三、五大 IO 模型对比
阻塞 IO(默认常用)
- 特点:执行 IO 操作时会阻塞等待,直到操作完成
- 适用场景:简单场景,无需同时处理多个 IO 事件
非阻塞 IO
- 特点:不会阻塞等待,无数据时立即返回
- 关键标识:EAGAIN(需重试)、errno(错误码)
- 实现方式:通过 fcntl () 设置 O_NONBLOCK 标志
信号驱动 IO
- 特点:通过信号通知 IO 事件,核心是 SIGIO 信号
- 实现步骤:
- 追加 O_ASYNC 标志:
fcntl(fd, F_SETFL, flag | O_ASYNC)
- 设置信号接收者:
fcntl(fd, F_SETOWN, getpid())
- 注册信号处理函数:
signal(SIGIO, 处理函数)
- 追加 O_ASYNC 标志:
- 现状:实际应用较少
select 循环服务器(TCP 协议)
一、并发的两种主要实现方式
- 进程:通过多进程处理多个连接请求
- 线程:通过多线程处理多个连接请求
二、select 循环服务器(IO 多路复用实现)
select 是 IO 多路复用的一种实现方式,可在单进程 / 线程中管理多个文件描述符,实现并发服务器。
1. select 函数原型
#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);
- 功能:检测指定描述符集合中是否有可执行 IO 操作的描述符,具有阻塞等待特性
- 返回值:
- 超时:0
- 失败:-1
- 成功:返回就绪的文件描述符数量(>0)
2. 参数详解
nfds
:需检测的描述符上限值(通常为最大描述符值 + 1)readfds
:需检测的可读描述符集合writefds
:需检测的可写描述符集合exceptfds
:需检测的异常描述符集合timeout
:超时设置- NULL:一直阻塞等待
- 非 NULL:指定超时时间
3. 配套宏函数
宏函数 | 功能 |
---|---|
FD_CLR(int fd, fd_set *set) | 从集合中移除指定描述符 |
FD_ISSET(int fd, fd_set *set) | 判断描述符是否在集合中(就绪) |
FD_SET(int fd, fd_set *set) | 向集合中添加指定描述符 |
FD_ZERO(fd_set *set) | 清空集合中所有描述符 |
4. select 使用注意事项
描述符集合处理:
- readfds 等参数是 "值结果参数",会被函数修改
- 通常需维护一个原始集合(如 allread_fdset),每次调用前拷贝到临时集合
- 支持赋值运算符
=
进行集合拷贝
nfds 管理:
- 新增描述符时需更新最大值
- 减少描述符时处理较麻烦,可采用最大堆维护或暂时不精确修改
timeout 处理:
- NULL:阻塞等待
- 时间为 0:非阻塞模式
- Linux 中返回时会修改 timeout 为剩余时间,重复使用需重新初始化
返回值利用:
- 正数表示就绪事件总数,可优化处理流程(如已知只有 1 个事件,可减少遍历)
5. select 的缺点
- 描述符数量限制:受 FD_SETSIZE 限制(Linux 默认 1024)
- 效率问题:
- 需要遍历所有监听的描述符判断是否就绪(FD_ISSET)
- nfds 设计不彻底,即使只监听 0 和 1000,也需遍历 1001 个描述符
- 使用复杂度:需手动维护描述符集合和最大值
一、epoll 核心函数
epoll 是 Linux 特有的 IO 多路复用机制,通过三个核心函数实现高效的事件监听,解决了 select/poll 的性能缺陷。
函数原型 | 功能 |
---|---|
int epoll_create(int size); | 创建 epoll 实例,返回 epoll 文件描述符(epfd) |
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); | 管理 epoll 实例中的事件(添加 / 修改 / 删除文件描述符及监听事件) |
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); | 等待 epoll 实例中的就绪事件,返回就绪事件数量 |
1. 关键结构体:struct epoll_event
用于描述监听的事件类型及关联数据,定义如下:
struct epoll_event {uint32_t events; // 监听的事件类型(如EPOLLIN、EPOLLOUT)epoll_data_t data; // 关联的数据(可存储文件描述符、自定义指针等)
};// data的联合体定义,支持多种数据类型
typedef union epoll_data {void *ptr; // 自定义指针(可关联业务数据)int fd; // 待监听的文件描述符uint32_t u32;uint64_t u64;
} epoll_data_t;
常用事件类型:
EPOLLIN
:文件描述符可读(如客户端发送数据、连接关闭)EPOLLOUT
:文件描述符可写(如发送缓冲区空闲)EPOLLERR
:文件描述符发生错误(无需主动设置,内核自动触发)EPOLLET
:开启边沿触发模式(默认是水平触发)
2. 各函数参数与返回值详解
函数 | 参数说明 | 返回值 |
---|---|---|
epoll_create | size :早期版本用于指定监听描述符上限,现在已忽略(需传≥0 的值) | 成功:epfd(非负整数);失败:-1 |
epoll_ctl | - epfd :epoll 实例的文件描述符- op :操作类型(EPOLL_CTL_ADD 添加、EPOLL_CTL_MOD 修改、EPOLL_CTL_DEL 删除)- fd :待管理的文件描述符- event :监听的事件及关联数据(删除时可传 NULL) | 成功:0;失败:-1 |
epoll_wait | - epfd :epoll 实例的文件描述符- events :输出参数,存储就绪的事件列表- maxevents :events 数组的最大长度(需≤epoll 实例中监听的描述符总数)- timeout :超时时间(毫秒,-1 表示阻塞等待,0 表示非阻塞) | 成功:就绪事件数量(>0);超时:0;失败:-1 |
二、epoll 的核心优势(对比 select/poll)
epoll 通过底层设计优化,解决了 select/poll 的性能瓶颈,具体优势如下:
无监听描述符数量限制
- 突破 select 的
FD_SETSIZE
(默认 1024)限制,仅受进程最大打开文件描述符数(可通过ulimit
调整)约束 - poll 虽也无数量限制,但性能随描述符增多下降,epoll 无此问题
- 突破 select 的
O (1) 级别的监听性能
- select/poll 采用「轮询」机制,需遍历所有监听描述符判断是否就绪(O (n) 复杂度)
- epoll 采用「事件驱动」机制:内核维护就绪事件列表,描述符就绪时主动上报,无需轮询(O (1) 复杂度),监听大量描述符时性能优势显著
减少用户态与内核态数据拷贝
- select/poll 每次调用需将监听的描述符集合从用户态拷贝到内核态,频繁调用开销大
- epoll 通过「共享内存」存储监听的事件信息,仅在初始化(添加描述符)和就绪时(返回事件)少量拷贝,大幅降低开销
直接返回就绪事件列表
- select/poll 返回后,需遍历所有监听描述符(通过
FD_ISSET
)判断是否就绪,存在无效遍历 - epoll 的
epoll_wait
直接返回就绪的事件列表(events
数组),无需额外判断,直接处理即可
- select/poll 返回后,需遍历所有监听描述符(通过
三、epoll 的特殊特性
两种触发模式
- 水平触发(LT,Level-Triggered):默认模式
只要文件描述符处于就绪状态(如可读),每次调用epoll_wait
都会返回该事件,直到数据被完全处理。
优势:编程简单,兼容性好(类似 select/poll 的行为)。 - 边沿触发(ET,Edge-Triggered):需手动设置
EPOLLET
标志
仅在文件描述符「状态由未就绪变为就绪」时触发一次事件(如从无数据到有数据),后续即使有新数据,若未处理完也不会再次触发。
优势:减少事件触发次数,性能更高;需注意:必须使用非阻塞 IO,且需一次性处理完所有数据(避免遗漏)。
- 水平触发(LT,Level-Triggered):默认模式
资源管理注意事项
epoll_create
创建的 epfd 是文件描述符,使用后需调用close(epfd)
释放,避免资源泄漏- 当不再监听某个文件描述符时,需通过
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL)
删除,避免无效事件触发
四、epoll 的适用场景
epoll 的高性能依赖「大量监听描述符 + 少量就绪事件」的场景,典型应用:
- 高并发服务器(如 Nginx、Redis):需同时监听成千上万的客户端连接,且每次就绪的连接数较少
- 网络 IO 密集型场景:避免轮询带来的性能损耗,提升系统吞吐量