欢迎光临
我们一直在努力

【网络编程】—— IO多路复用

IO多路复用是一种单线程同时监测多个文件描述符IO状态的机制,主要包括select和epoll两种实现方式。select使用fd_set集合管理文件描述符,通过轮询方式检测就绪状态,但受限于1024个文件描述符且效率随数量增加下降。epoll则采用红黑树存储文件描述符,就绪事件主动上报,支持高并发且效率稳定。两者主要区别在于:select需要每次重置集合并轮询所有文件描述符,而epoll仅需处理就绪事件;select有1024的文件描述符限制,epoll则无此限制;select每次调用都需数据拷贝,epoll使用共享内存减少拷贝开销。epoll在高并发场景下性能优势明显,而select适用于简单低并发场景。


一、基本概念

1.定义:

单线程或者单进程同时监测若干个文件描述符是否可以执行IO操作的能力(一种IO事件的通知机制)

2.应用场景:

应用程序通常需要处理来自多事件流中的事件,例如电脑需要同时处理键盘的输入、多种信号的中断等(单个执行体中需要监测多个阻塞设备)

3.并发:

逻辑控制流在时间上的重叠叫并发。

4.使用并发处理的成本:

1)线程/进程创建成本

2)CPU切换不同线程/进程成本

3)多线程的资源竞争

5)IO多路复用的优点

5.多路复用和并发的区别


二、IO模型

1.阻塞IO

最常用的默认设置

2.非阻塞IO

3.信号驱动IO

4.并行模型

5.IO多路复用


三、select 和 epoll

1.select

1)定义:

select 是 Linux 系统中传统的 I/O 多路复用技术,核心是通过fd_set 集合管理待监听的文件描述符(fd),程序调用 select 后,内核会轮询集合内所有 fd,判断是否就绪(可读 / 可写),就绪的 fd 会保留在集合中,程序需遍历所有 fd 识别就绪的 fd 并处理。

2)使用步骤:
  • 定义两个集合变量:先定义两个 fd_set 类型的集合(如 tmp_set 和 rd_set),tmp_set 用于存储 “需要监听的所有 fd”(原始集合),rd_set 用于接收 “select 返回的就绪 fd”(就绪集合)。
  • 清空原始集合:调用 FD_ZERO(&tmp_set) 清空原始集合,避免集合中残留旧的 fd 数据导致监听异常。
  • 添加待监听的 fd:调用 FD_SET(fd, &tmp_set) 将需要监听的 fd(如 0 = 键盘输入、udpfd=UDP 套接字)逐个添加到 tmp_set 中。
  • 确定最大 fd 值:找出所有待监听 fd 中的最大值(记为 maxfd),后续 select 调用需要用到 maxfd + 1 作为参数。
  • 重置就绪集合并调用 select:每次循环中先执行 rd_set = tmp_set(因为 select 会修改集合,需用原始集合重置),再调用 select(maxfd + 1, &rd_set, NULL, NULL, NULL) 阻塞等待 fd 就绪(仅监听读事件,写 / 异常集合填 NULL,永久阻塞则超时参数填 NULL)。
  • 遍历判断就绪 fd:遍历从 0 到 maxfd 的所有 fd,调用 FD_ISSET(fd, &rd_set) 判断当前 fd 是否在就绪集合中。
  • 处理就绪 fd:对判断为 “就绪” 的 fd 执行对应操作(如 fd=0 则处理键盘输入,udpfd 则处理数据接收)。
  • 循环重复监听:处理完本轮就绪 fd 后,回到步骤 5,继续下一轮的 select 监听,直到程序退出。
3)相关函数:
函数名核心功能参数详解返回值关键注意事项
void FD_ZERO(fd_set *set) 清空 fd_set 集合 set:待清空的集合(如 tmp_set) 无返回值 每次使用集合前必须调用,避免残留旧 fd
void FD_SET(int fd, fd_set *set) 将 fd 添加到集合中 fd:待监听的 fd(如 0、udpfd)set:目标集合(如 tmp_set) 无返回值 1. 需提前确定 maxfd;2. 只需添加一次,无需每次循环重复加
int FD_ISSET(int fd, fd_set *set) 判断 fd 是否在就绪集合中 fd:待判断的 fdset:select 返回的就绪集合(如 rd_set) 就绪:非 0未就绪:0 必须遍历 0~maxfd 逐个判断,效率低
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout) 阻塞等待 fd 就绪 nfds:maxfd+1(必填,否则监听不全)readfds:读事件集合(核心)writefds:写事件集合(填 NULL)exceptfds:异常事件集合(填 NULL)timeout:超时时间(NULL = 永久阻塞) 成功:>0(就绪 fd 数量)超时:0失败:-1 1. 集合大小受 FD_SETSIZE 限制(默认 1024);2. select 会修改集合,每次循环需用 tmp_set 重置 rd_set;3. 每次调用都会拷贝集合到内核态,性能损耗大
4)注意:
  • maxfd 必须 + 1:select 第一个参数是 “最大 fd 值 + 1”,比如监听 0 和 udpfd(假设 = 3),则 nfds=4,否则无法监听最大的 fd,select集合最多可以监测的文件描述符数为1024;
  • 集合必须重置:每次循环需执行rd_set = tmp_set,因为 select 会把未就绪的 fd 从集合中清空;
  • fd 数量限制:默认最多监听 1024 个 fd,无法支持高并发场景;
  • 如果是超时版本的select,在调用select之前必须要重新设置时间;
  • 效率问题:即使只有 1 个 fd 就绪,也需遍历 0~maxfd,fd 越多效率越低;
  • 资源释放:程序退出前需关闭监听的 fd(如 udpfd),避免资源泄露。

2.epoll

1)定义:

epoll 是 Linux 系统下高性能的 I/O 多路复用技术,核心是通过内核维护的(epfd 集合)存储待监听 fd,当 fd 就绪时,内核会将就绪 fd主动上报到「就绪数组(rev 数组)」,程序只需遍历就绪数组处理 fd,无需轮询所有 fd,效率远高于 select。

2)使用步骤:
  • 创建 epoll 实例:调用 epoll_create(int size)(或新版 epoll_create1(0))创建 epoll 集合(内核层面是红黑树),返回一个 epoll 实例的 fd(记为 epollfd),用于标识这个集合(参数 size 现在失效,填任意正数如 10 即可)。
  • 定义事件结构体并配置监听事件:定义 struct epoll_event 类型的变量(如 ev),设置 ev.events = EPOLLIN(表示监听读事件,契合 UDP / 键盘场景的核心需求),并将 ev.data.fd 赋值为待监听的 fd(如 0 或 udpfd),绑定 fd 与监听事件。
  • 添加 fd 到 epoll 集合:调用 epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev) 将配置好监听事件的 fd 添加到 epoll 集合中(如需删除 fd,将第二个参数改为 EPOLL_CTL_DEL 即可)。
  • 定义就绪事件数组并调用 epoll_wait:定义 struct epoll_event 类型的数组(如 events[10],即老师笔记中的 rev 数组),调用 epoll_wait(epollfd, events, 10, -1) 阻塞等待 fd 就绪(参数 10 是一次最多接收的就绪事件数,-1 表示永久阻塞),返回值 nfds 是本次就绪的 fd 数量。
  • 遍历就绪数组识别 fd:遍历从 0 到 nfds – 1 的就绪数组下标,通过 events[i].data.fd 识别出当前就绪的 fd(无需遍历所有 fd,仅处理就绪的)。
  • 处理就绪 fd:对识别出的就绪 fd 执行对应操作(如 fd=0 处理键盘输入,udpfd 处理数据接收)。
  • 循环重复监听:处理完本轮就绪 fd 后,回到步骤 4,继续下一轮的 epoll_wait 监听,直到程序退出。
  • 释放资源(退出前):程序退出时,调用 close(epollfd) 关闭 epoll 实例,同时关闭监听的 fd(如 udpfd),避免内核资源泄露。
3)相关函数:
函数名核心功能参数详解返回值关键注意事项
int epoll_create(int size) 创建 epoll 集合(红黑树) size:早期预估监听 fd 数(现在失效,填 10 即可) 成功:>0(epoll 实例 fd,即 epfd)失败:-1 1. 新版 Linux 推荐用epoll_create1(0),功能一致;2. 程序退出前必须close(epfd),避免内核资源泄露;3. 无 1024 fd 数量限制
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) 增 / 删 / 改 epoll 集合中的 fd epfd:epoll_create 返回的集合标识op:操作类型(EPOLL_CTL_ADD = 添加、EPOLL_CTL_DEL = 删除)fd:待操作的 fd(如 0、udpfd)event:事件结构体:c<br>struct epoll_event {<br> uint32_t events; // EPOLLIN=读事件<br> epoll_data_t data; // data.fd绑定目标fd<br>};<br> 成功:0失败:-1 1. 添加 fd 时,event->data.fd必须和 fd 参数一致;2. 只需添加一次 fd,无需每次循环重置;3. 仅监听读事件时,events 只设 EPOLLIN
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout) 等待 fd 就绪,就绪 fd 填充到 events 数组 epfd:epoll 集合标识events:输出参数(rev 数组),存储就绪 fdmaxevents:一次最多接收的就绪事件数(数组长度)timeout:超时时间(ms):-1 = 永久阻塞、0 = 非阻塞、>0 = 指定毫秒 成功:>0(就绪 fd 数量)超时:0失败:-1 1. 只需遍历返回的 “就绪 fd 数量”,无需遍历所有 fd;2. events 数组需提前定义(如struct epoll_event events[10]);3. 内核共享内存存储就绪 fd,无需频繁拷贝
4)注意:
  • 事件类型:UDP / 键盘场景只需关注 EPOLLIN(读事件),写事件(EPOLLOUT)仅需发送大量数据时使用;
  • 存储文件描述符集合的个数不再受1024的限制
  • ev.data.fd 一致性:添加 fd 时,event->data.fd必须等于目标 fd,否则无法识别哪个 fd 就绪;
  • 高并发适配:监测机制是主动上报,fd 数量越多,epoll 效率优势越明显(select 会急剧变慢);
  • 使用共享内存,避免了集合在用户层和内核层多次复制;
  • 当eopll_wait返回后,准备就绪的文件描述符都存储在events集合中,相对容易查找;
  • 资源释放:退出前需同时关闭 epfd 和监听的 fd(如 udpfd)。

3.select 和 epoll 的区别

对比维度selectepoll
核心定义 传统 I/O 多路复用,轮询检测 fd 就绪 高性能 I/O 多路复用,就绪 fd 主动上报
数据结构 fd_set 集合(位图),大小固定 红黑树(存储待监听 fd)+ 就绪数组(动态)
fd 数量限制 受 FD_SETSIZE 限制(默认 1024) 无限制(仅受系统内存 / 文件句柄限制)
使用步骤 定义集合→清空→添加 fd→select→遍历 0~maxfd→重置集合 创建 epoll 实例→添加 fd→epoll_wait→遍历就绪数组
检测机制 内核轮询所有 fd(0~maxfd),效率随 fd 增多下降 就绪 fd 主动上报到数组,仅处理就绪 fd,效率稳定
数据拷贝 每次 select 都将集合从用户态→内核态拷贝 仅初始化时拷贝一次(共享内存),后续无拷贝
集合维护 每次循环需重置集合(rd_set = tmp_set) 内核自动维护红黑树,无需重置
核心函数 FD_ZERO/FD_SET/FD_ISSET/select epoll_create/epoll_ctl/epoll_wait
效率表现 低并发(fd 少)可用,高并发效率极低 低 / 高并发均高效,fd 越多优势越明显
注意事项 1. maxfd 必须 + 1;2. 集合需每次重置;3. 最多监听 1024 个 fd 1. epfd 需手动关闭;2. ev.data.fd 需和目标 fd 一致;3. 仅遍历就绪 fd 即可
适用场景 简单场景、fd 数量少的情况(如单机测试) 高并发场景(如服务器、多客户端通信)
赞(0)
未经允许不得转载:171主机测评 » 【网络编程】—— IO多路复用
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址