IO多路转接

📅 2026/8/18 13:07:37
IO多路转接
IO多路转接( I/O 多路复用)就是一个线程同时监视多个文件描述符socket、文件、管道哪个 IO 就绪了就处理哪个。多路转接的思路把 “监视多个 fd” 这件事交给内核。用户进程告诉内核帮我盯着这一堆 fd内核阻塞等待只要有任意 fd 就绪可读 / 可写就返回给用户进程用户再去做真正的 read/write。select /poll/epoll的定位回顾select最早多路转接位图最大 1024 个 fdO (n) 遍历水平触发。pollselect 改良数组存储 pollfd无 1024 限制依旧 O (n)水平触发。epoll (Linux 独有)内核维护就绪链表O (1) 拿到就绪 fd支持 LT 和 ET适合大量并发连接。下面将介绍select /poll/epoll的用法一.select1.函数原型#include sys/select.h #include sys/time.h int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);2.参数详解(1)nfds含义所有被监听文件描述符中最大的描述符数值 1。作用指定内核需要检测的描述符范围为[0, nfds内核会遍历该区间内的所有描述符。注意文件描述符从 0 开始编号必须传入最大 fd 1否则最大的描述符不会被内核检测。(2)readfds/writefds/exceptfds三个fd_set类型的指针分别对应可读事件集合、可写事件集合、异常事件集合。fd_set本质是位图结构每一位对应一个文件描述符默认容量由宏FD_SETSIZE定义Linux 下通常为 1024。输入时用户通过宏设置需要监听的描述符输出时内核会直接修改集合仅保留处于就绪状态的描述符。配套 4 个操作宏用于管理fd_set宏函数功能说明FD_ZERO(fd_set *set)清空集合将所有位清零FD_SET(int fd, fd_set *set)将文件描述符fd添加到监听集合FD_CLR(int fd, fd_set *set)将文件描述符fd从监听集合移除FD_ISSET(int fd, fd_set *set)调用 select 后判断fd是否就绪(3)timeout类型struct timeval *结构体定义struct timeval { long tv_sec; // 秒 long tv_usec; // 微秒 };控制 select 的阻塞行为传入NULL永久阻塞直到至少一个描述符就绪传入tv_sec0 tv_usec0非阻塞模式立即返回传入正时间值阻塞等待指定时长超时则返回 0。关键特性timeout是输入输出参数内核会修改其值为剩余等待时间。因此循环复用超时逻辑时每次调用 select 前必须重新赋值。3. 返回值 0调用成功返回三个集合中所有就绪文件描述符的总数量 0超时在指定时间内没有任何描述符就绪 -1调用失败errno会被设置为对应错误码如EINTR被信号中断、EINVAL参数非法等。4. 核心特点优点跨平台兼容性极强几乎所有操作系统都支持接口简单学习和使用门槛低。缺点描述符数量硬限制受FD_SETSIZE约束默认 1024修改需重新编译内核集合需重复重置每次调用后内核会覆盖集合下次调用前必须重新设置所有监听项O (n) 轮询开销内核需要遍历所有监听的描述符连接数越大效率越低内存拷贝开销每次调用都需要在用户态与内核态间拷贝整个位图开销随描述符数量线性增长。二.pollpoll 是对 select 的优化版本同样属于 I/O 多路复用基于结构体数组管理监听项突破了描述符数量限制事件语义更丰富在类 Unix 系统中广泛使用。1. 函数原型#include poll.h int poll(struct pollfd *fds, nfds_t nfds, int timeout);2. 参数详解(1)fds类型struct pollfd *指向pollfd结构体数组的首地址。每个数组元素对应一个被监听的文件描述符及其关注的事件。pollfd结构体定义struct pollfd { int fd; // 目标文件描述符 short events; // 用户设置要监听的事件输入 short revents; // 内核填充实际发生的事件输出 };fd目标文件描述符若设为负值该元素会被忽略revents始终返回 0可用于动态复用数组。events用户手动设置指定监听的事件类型可通过位或组合多个事件。revents由内核写入调用结束后反映该描述符上实际触发的事件。事件标志可设置位置含义说明POLLINevents / revents有普通数据可读POLLOUTevents / revents可写发送缓冲区有空闲空间POLLPRIevents / revents有高优先级 / 带外数据可读POLLERR仅 revents发生错误无需用户设置内核自动返回POLLHUP仅 revents连接挂起对端关闭连接无需用户设置POLLNVAL仅 revents文件描述符非法未打开无需用户设置POLLRDHUPevents / revents对端关闭了读半连接Linux 2.6.17 支持(2)nfds类型nfds_t无符号整型含义fds数组的元素个数即监听的文件描述符总数。无内置上限由用户自行管理数组大小支持动态扩容。(3)timeout类型int单位为毫秒ms。控制 poll 的阻塞行为-1永久阻塞直到有描述符就绪0非阻塞模式立即返回正整数阻塞等待指定毫秒数超时返回 0。关键特性该参数为传值调用内核不会修改原值循环使用时无需重复重置。3. 返回值 0调用成功返回所有revents非零的文件描述符数量就绪总数 0超时指定时间内无任何描述符就绪 -1调用失败errno被设置为对应错误码。4. 核心特点优点无描述符数量上限基于用户数组管理可动态调整监听数量不受 1024 硬限制无需重复设置监听事件events与revents分离内核只修改revents循环调用无需重置监听配置事件类型更丰富支持多种细分事件语义更精准可处理更多场景没有位图结构的天然限制更适合中等规模的连接场景。缺点本质仍是轮询机制时间复杂度为 O (n)上万级高并发下效率依然低下同样存在用户态与内核态之间的数组拷贝开销三.epollepoll是 Linux 独有的 I/O 多路复用机制是对 select/poll 的革命性改进。它通过内核维护一个事件表将 注册监听事件 和 等待就绪 拆分开实现了 O (1) 级别的就绪检测是高并发网络编程的事实标准。1.核心设计思想select/poll 的问题每次调用都要把所有监听的 fd 从用户态拷贝到内核态内核再遍历全部 fd 检查就绪状态连接越多越慢。epoll 的解决思路内核维护一个红黑树存储所有被监听的 fd叫 epoll 实例一次注册永久生效不用每次重复拷贝内核维护一个就绪链表fd 就绪时通过回调函数自动加入链表epoll_wait只需检查就绪链表是否为空不用遍历所有 fd时间复杂度 O (1)。2.三个核心函数1.epoll_create— 创建 epoll 实例#include sys/epoll.h int epoll_create(int size); int epoll_create1(int flags); // 更现代的版本参数size告诉内核要监听的 fd 数量用于内核初始分配数据结构大小Linux 2.6.8 后这个参数已被忽略内核动态扩容但必须传大于 0 的值返回值成功返回epoll 实例的文件描述符epfd失败返回 -1设置 errno。epoll 实例本身也是一个文件描述符用完需要close()释放。2.epoll_ctl— 注册 / 修改 / 删除监听事件int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);参数说明epfdepoll_create 返回的 epoll 实例 fdop操作类型见下表fd要操作的目标文件描述符event要监听的事件指向 epoll_event 结构体的指针op 的取值宏含义EPOLL_CTL_ADD向 epoll 实例中注册新的 fd 及其事件EPOLL_CTL_MOD修改已注册 fd 的监听事件EPOLL_CTL_DEL从 epoll 实例中移除 fd此时 event 参数可传 NULL返回值成功返回 0失败返回 -1设置 errno如EEXIST重复添加、ENOENT删除不存在的 fd。3.epoll_wait— 等待事件就绪int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);参数说明epfdepoll 实例 fdevents输出参数指向用户分配的数组内核将就绪事件拷贝到这里maxeventsevents 数组的最大容量必须大于 0timeout超时时间单位毫秒timeout 取值-1永久阻塞直到有事件就绪0非阻塞立即返回 0阻塞等待指定毫秒数超时返回 0。返回值 0就绪的文件描述符数量events 数组前 N 个元素有效 0超时无就绪事件 -1失败设置 errno如EINTR被信号中断。关键epoll_wait 只返回已经就绪的 fd用户不用遍历全部监听集合直接处理返回的 events 数组即可。3.核心数据结构epoll_eventstruct epoll_event { uint32_t events; // 监听的事件类型位掩码 epoll_data_t data; // 用户数据回调时原样返回 }; typedef union epoll_data { void *ptr; // 自定义指针最常用可指向自定义结构体 int fd; // 文件描述符 uint32_t u32; uint64_t u64; } epoll_data_t;常用事件标志标志含义EPOLLIN可读包括对端关闭连接此时 read 返回 0EPOLLOUT可写发送缓冲区有空闲EPOLLPRI有高优先级 / 带外数据可读EPOLLRDHUP对端关闭了读半连接Linux 2.6.17EPOLLERR发生错误内核自动设置无需注册EPOLLHUP挂起内核自动设置无需注册EPOLLET设置为边缘触发模式默认是水平触发EPOLLONESHOT只触发一次触发后该 fd 不再被监听需重新 EPOLL_CTL_MOD事件可以通过位或组合例如event.events EPOLLIN | EPOLLET; // 监听可读边缘触发4.两种触发模式这是 epoll 最重要的概念也是和 select/poll 最大的区别之一。1. 水平触发 LTLevel-Triggered默认只要 fd 上还有未处理的数据每次 epoll_wait 都会返回该事件。行为和 select/poll 完全一致socket 接收缓冲区还有数据没读完 → 下次 epoll_wait 还会返回 EPOLLIN发送缓冲区还有空闲空间 → 每次都会返回 EPOLLOUT。特点编程简单不容易漏事件数据可以分次读不要求一次性读完效率略低事件可能被重复通知。2. 边缘触发 ETEdge-Triggered需在epoll_event.events设置 EPOLLET只有 fd 状态发生变化的那一刻才通知一次之后即使缓冲区还有数据也不会再通知。举例对端发来 10KB 数据 → 触发一次EPOLLIN你只读了 4KB剩下 6KB 留在缓冲区 → 下次 epoll_wait 不会再提醒你只有对端再发新数据状态再次变化时才会再次触发。EPOLLOUT仅在「发送缓冲区从满 → 出现空闲」这个状态跳变瞬间通知一次。ET 模式的强制要求必须使用非阻塞 fd否则 read/write 可能阻塞在单 fd 上整个事件循环卡死必须循环读 / 写直到返回 EAGAIN把缓冲区彻底读空 / 写满否则剩余数据会永远丢失。5.epoll工作原理struct eventpoll { ... /*红黑树的根节点这颗树中存储着所有添加到epoll中的需要监控的事件*/ struct rb_root rbr; /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/ struct list_head rdlist; ... };struct epitem { struct rb_node rbn; // 红黑树节点挂载到eventpoll.rbr struct list_head rdllink; // 就绪链表节点就绪时链入eventpoll.rdlist struct epoll_filefd ffd; // 被监听的文件句柄信息 struct eventpoll *ep; // 归属的eventpoll实例 struct epoll_event event; // 用户注册的待监听事件 };epoll_create内核分配struct eventpoll内存为这个 epoll 实例分配一个空闲的文件描述符 fd并创建对应的struct file内核对象eventpoll结构体指针存入file-private_data建立「fd → file → eventpoll」的关联。epoll_ctl根据用户传入的epfd找到对应的file再从file-private_data取出struct eventpoll *。以「目标 fd 文件对象」为 key在eventpoll-rbr红黑树中查找对应的epitem节点。执行对应操作如EPOLL_CTL_ADD若树中不存在该 fd则分配一个struct epitem并初始化通过rbn成员插入eventpoll-rbr红黑树。调用目标文件的f_op-poll接口把ep_poll_callback回调函数注册到该文件的等待队列上 —— 当文件状态变为就绪时文件系统会自动调用这个回调通知 epoll。epoll_wait通过epfd找到内核中的struct eventpoll。先检查eventpoll-rdlist双向链表是否非空如果已有就绪的epitem不阻塞如果为空将当前进程挂入eventpoll-wq等待队列设置超时时间进入阻塞睡眠状态。当某个被监听的文件描述符状态就绪如 socket 收到数据、管道可读时文件系统会触发之前注册的ep_poll_callback回调将对应epitem通过rdllink成员链入eventpoll-rdlist就绪链表唤醒eventpoll-wq上睡眠的进程也就是调用epoll_wait的用户进程。进程被唤醒后遍历eventpoll-rdlist链表取出每个就绪epitem中的event信息将事件数据拷贝到用户态传入的events数组中