当前位置: 首页 > news >正文

I/O 多路复用 (I/O Multiplexing)

一、核心概念

1.1 定义与本质

I/O 多路复用 是指单线程或单进程能够同时监测多个文件描述符(File Descriptor, FD),并判断哪些描述符已经就绪、可以执行I/O操作的能力。

本质:用更少的系统资源(避免线程/进程创建开销、上下文切换成本、资源竞争)完成更多事件流的并发处理。

1.2 作用与背景

典型场景

  • 操作系统需要同时处理键盘/鼠标输入、中断信号等事件流

  • Web服务器(如Nginx)需要同时处理来自成千上万个客户端的连接请求

并发本质:多条逻辑控制流在时间上相互重叠(通过CPU时分复用实现)

传统并发方案的缺点

  • 线程/进程创建开销大

  • CPU上下文切换成本高

  • 多线程资源竞争问题复杂

二、五种I/O模型详解

2.1 阻塞I/O(Blocking I/O)

核心特性:当I/O操作无法立即完成时,进程/线程会挂起等待,直到数据准备就绪。

FIFO管道通信示例

写端代码

c#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <errno.h>int main() {// 创建命名管道if (mkfifo("myfifo", 0666) == -1 && errno != EEXIST) {perror("mkfifo error");return 1;}// 以只写模式打开FIFO(阻塞等待读端连接)int fd = open("myfifo", O_WRONLY);if (fd == -1) {perror("open error");return 1;}while (1) {char buf[] = "hello, this is fifo test...\n";write(fd, buf, strlen(buf) + 1);sleep(3); // 每3秒写一次}close(fd);return 0;
}

读端代码

c#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <errno.h>int main() {// 创建命名管道if (mkfifo("myfifo", 0666) == -1 && errno != EEXIST) {perror("mkfifo error");return 1;}// 以只读模式打开FIFO(阻塞等待写端连接)int fd = open("myfifo", O_RDONLY);if (fd == -1) {perror("open error");return 1;}while (1) {char buf[512] = {0};read(fd, buf, sizeof(buf)); // 阻塞读取printf("fifo: %s", buf);// 从终端读取输入fgets(buf, sizeof(buf), stdin);printf("terminal: %s", buf);}close(fd);return 0;
}

特点

  • 简单易用,但无法同时处理多事件流

  • FIFO管道特性:无对端连接时open()阻塞

2.2 非阻塞I/O(Non-blocking I/O)

核心特性:通过fcntl()设置O_NONBLOCK标志,使I/O操作立即返回(无数据时返回EAGAIN错误)。

代码示例

c#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <errno.h>int main() {// 创建FIFOif (mkfifo("myfifo", 0666) == -1 && errno != EEXIST) {perror("mkfifo error");return 1;}int fd = open("myfifo", O_RDONLY);if (fd == -1) {perror("open error");return 1;}// 设置非阻塞模式int flags = fcntl(fd, F_GETFL);fcntl(fd, F_SETFL, flags | O_NONBLOCK);// 设置标准输入为非阻塞flags = fcntl(STDIN_FILENO, F_GETFL);fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK);while (1) {char buf[512] = {0};// 非阻塞读管道ssize_t n = read(fd, buf, sizeof(buf));if (n > 0) {printf("fifo: %s\n", buf);} else if (n == -1 && errno != EAGAIN) {perror("read error");break;}// 非阻塞读终端if (fgets(buf, sizeof(buf), stdin) != NULL) {printf("terminal: %s", buf);}usleep(100000); // 避免CPU占用过高}close(fd);return 0;
}

特点

  • 避免单个I/O操作阻塞整个进程

  • 需要轮询检查数据就绪状态(忙等待)

  • 通过errno == EAGAIN判断无数据

2.3 信号驱动I/O(Signal-driven I/O)

核心特性:设置O_ASYNC标志,当I/O就绪时内核发送SIGIO信号。

配置步骤

  1. 添加异步标志:fcntl(fd, F_SETFL, flags | O_ASYNC)

  2. 设置信号接收者:fcntl(fd, F_SETOWN, getpid())

  3. 注册信号处理函数:signal(SIGIO, handler)

代码示例

c#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <errno.h>
#include <signal.h>int fd; // 全局文件描述符void sigio_handler(int sig) {char buf[512] = {0};read(fd, buf, sizeof(buf));printf("fifo: %s\n", buf);
}int main() {signal(SIGIO, sigio_handler);// 创建FIFOif (mkfifo("myfifo", 0666) == -1 && errno != EEXIST) {perror("mkfifo error");return 1;}fd = open("myfifo", O_RDONLY);if (fd == -1) {perror("open error");return 1;}// 配置信号驱动I/Oint flags = fcntl(fd, F_GETFL);fcntl(fd, F_SETFL, flags | O_ASYNC);fcntl(fd, F_SETOWN, getpid());while (1) {char buf[512] = {0};// 处理其他任务fgets(buf, sizeof(buf), stdin);printf("terminal: %s", buf);}close(fd);return 0;
}

特点

  • 通过信号机制异步通知I/O就绪

  • 避免轮询开销,但信号处理函数限制多

  • 实际应用较少

2.4 多进程/多线程并发

核心机制

  • 进程:fork()创建子进程处理独立事件流

  • 线程:pthread_create()创建线程共享内存空间

局限性

  • 资源开销大(创建/销毁成本高)

  • 上下文切换消耗CPU资源

  • 需处理同步/互斥问题

2.5 I/O多路复用(I/O Multiplexing)

核心价值:单线程内高效监控多个文件描述符的状态变化。

三、I/O多路复用技术详解

3.1 select模型

函数原型

c#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

关键宏

  • FD_ZERO(fd_set *set) - 清空集合

  • FD_SET(int fd, fd_set *set) - 添加描述符

  • FD_CLR(int fd, fd_set *set) - 移除描述符

  • FD_ISSET(int fd, fd_set *set) - 检查是否就绪

示例代码

c#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/stat.h>
#include <errno.h>int main() {// 创建FIFOif (mkfifo("myfifo", 0666) == -1 && errno != EEXIST) {perror("mkfifo error");return 1;}int fd = open("myfifo", O_RDONLY);if (fd == -1) {perror("open error");return 1;}fd_set readfds, tmpfds;FD_ZERO(&readfds);FD_ZERO(&tmpfds);// 监控标准输入和FIFOFD_SET(STDIN_FILENO, &tmpfds);FD_SET(fd, &tmpfds);int maxfd = (fd > STDIN_FILENO) ? fd : STDIN_FILENO;while (1) {readfds = tmpfds;// 等待事件if (select(maxfd + 1, &readfds, NULL, NULL, NULL) == -1) {perror("select error");break;}char buf[512] = {0};// 检查FIFO是否就绪if (FD_ISSET(fd, &readfds)) {read(fd, buf, sizeof(buf));printf("fifo: %s\n", buf);}// 检查标准输入是否就绪if (FD_ISSET(STDIN_FILENO, &readfds)) {fgets(buf, sizeof(buf), stdin);printf("terminal: %s", buf);}}close(fd);return 0;
}

局限性

  • 描述符数量限制(FD_SETSIZE=1024)

  • 每次调用需传递整个描述符集合

  • 返回后需遍历所有描述符检查就绪状态(O(n)复杂度)

3.2 epoll模型(高性能方案)

核心函数

  • epoll_create(int size) - 创建epoll实例

  • epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) - 管理描述符

  • epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout) - 等待事件

示例代码

c#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <sys/stat.h>
#include <errno.h>int main() {// 创建FIFOif (mkfifo("myfifo", 0666) == -1 && errno != EEXIST) {perror("mkfifo error");return 1;}int fd = open("myfifo", O_RDONLY | O_NONBLOCK);if (fd == -1) {perror("open error");return 1;}// 创建epoll实例int epfd = epoll_create1(0);if (epfd == -1) {perror("epoll_create1 error");close(fd);return 1;}// 添加监控描述符struct epoll_event ev;ev.events = EPOLLIN;ev.data.fd = fd;if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1) {perror("epoll_ctl error");close(fd);close(epfd);return 1;}ev.events = EPOLLIN;ev.data.fd = STDIN_FILENO;if (epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev) == -1) {perror("epoll_ctl error");close(fd);close(epfd);return 1;}struct epoll_event events[2];while (1) {// 等待事件int nfds = epoll_wait(epfd, events, 2, -1);if (nfds == -1) {perror("epoll_wait error");break;}for (int i = 0; i < nfds; i++) {if (events[i].data.fd == fd) {char buf[512] = {0};read(fd, buf, sizeof(buf));printf("fifo: %s\n", buf);} else if (events[i].data.fd == STDIN_FILENO) {char buf[512] = {0};fgets(buf, sizeof(buf), stdin);printf("terminal: %s", buf);}}}close(fd);close(epfd);return 0;
}

优势

  • 无描述符数量限制

  • 事件就绪时直接返回就绪列表(O(1)复杂度)

  • 用户/内核空间共享内存(避免数据拷贝)

  • 支持水平触发(LT)和边沿触发(ET)模式

四、select vs poll vs epoll 对比

特性selectpollepoll
描述符上限FD_SETSIZE(1024)无硬限制无硬限制
性能复杂度O(n)O(n)O(1)
数据拷贝每次调用全量拷贝每次调用全量拷贝共享内存
事件返回方式修改输入集合填充就绪数组返回就绪列表
触发模式水平触发水平触发支持水平/边沿触发
适用场景小规模连接(<1000)中等规模连接高并发场景

关键结论

  • epoll在高并发场景下性能显著优于select/poll

  • epoll优势前提:监听大量描述符 + 每次仅少量事件就绪

五、TCP服务器应用实例

5.1 基于select的TCP服务器

#include <netinet/in.h>     // 定义网络地址结构(如sockaddr_in)及字节序转换函数(如htons)
#include <netinet/ip.h>     // IP协议相关定义(本程序未实际使用)
#include <stdio.h>          // 标准输入输出函数(如printf、perror)
#include <stdlib.h>         // 标准库函数
#include <string.h>         // 字符串处理函数(如bzero、strlen、sprintf)
#include <sys/socket.h>     // 套接字相关函数(如socket、bind、listen、accept)
#include <sys/types.h>      // 基本系统数据类型
#include <time.h>           // 时间相关函数(如time、ctime)
#include <unistd.h>         // 系统调用函数(如close)
#include <sys/select.h>     // 提供select函数及文件描述符集相关定义typedef struct sockaddr *(SA);  // 将struct sockaddr*简化为SAint main(int argc, char **argv)
{// 创建监听套接字:AF_INET(IPv4协议),SOCK_STREAM(TCP流式套接字),0(默认协议)int listfd = socket(AF_INET, SOCK_STREAM, 0);if (-1 == listfd) {perror("scoket error\n");  // 套接字创建失败return 1;}// 定义服务器和客户端的地址结构并初始化struct sockaddr_in ser, cli;bzero(&ser, sizeof(ser));bzero(&cli, sizeof(cli));// 设置服务器地址信息ser.sin_family = AF_INET;                  // 使用IPv4协议ser.sin_port = htons(50000);               // 绑定端口50000ser.sin_addr.s_addr = INADDR_ANY;          // 绑定到本机所有可用IP地址// 将监听套接字与服务器地址绑定int ret = bind(listfd, (SA)&ser, sizeof(ser));if (-1 == ret) {perror("bind");return 1;}// 将套接字设为监听状态listen(listfd, 3);socklen_t len = sizeof(cli);// 1. 初始化文件描述符集fd_set rd_set, tmp_set;FD_ZERO(&rd_set);FD_ZERO(&tmp_set);// 2. 向临时集合添加初始监听的文件描述符(监听套接字listfd)FD_SET(listfd, &tmp_set);int maxfd = listfd;      // 记录当前最大文件描述符time_t tm;while (1){rd_set = tmp_set;  // 恢复监听集合// 调用select监听可读事件select(maxfd + 1, &rd_set, NULL, NULL, NULL);// 遍历所有可能的文件描述符for (int i = 0; i < maxfd + 1; i++){// 若事件来自监听套接字listfd(新客户端连接请求)if (FD_ISSET(i, &rd_set) && i == listfd){// 接受客户端连接int conn = accept(listfd, (SA)&cli, &len);if (-1 == conn) {perror("accept");close(conn);continue;}// 将新连接的套接字添加到监听集合FD_SET(conn, &tmp_set);// 更新最大文件描述符if (conn > maxfd) {maxfd = conn;}}// 若事件来自已连接的客户端套接字if (FD_ISSET(i, &rd_set) && i != listfd){int conn = i;char buf[1024] = {0};// 接收客户端发送的数据int ret = recv(conn, buf, sizeof(buf), 0);if (ret <= 0) {printf("client offline\n");FD_CLR(conn, &tmp_set);close(conn);continue;}// 获取当前时间戳time(&tm);// 将接收的数据与当前时间拼接sprintf(buf, "%s %s", buf, ctime(&tm));// 将拼接后的数据回发给客户端send(conn, buf, strlen(buf), 0);}}}close(listfd);return 0;
}

5.2 基于epoll的TCP服务器

#include <netinet/in.h>   // 提供Internet地址族相关定义
#include <netinet/ip.h>   // 提供IP协议相关定义
#include <stdio.h>        // 标准输入输出函数
#include <stdlib.h>       // 标准库函数
#include <string.h>       // 字符串处理函数
#include <sys/epoll.h>    // epoll相关函数
#include <sys/socket.h>   // 套接字相关函数
#include <sys/types.h>    // 基本系统数据类型
#include <time.h>         // 时间相关函数
#include <unistd.h>       // Unix标准函数
typedef struct sockaddr *(SA);  // 简化sockaddr指针类型/*** 向epoll实例中添加文件描述符* @param epfd epoll实例的文件描述符* @param fd 要添加的文件描述符* @return 0表示成功,1表示失败*/
int add_fd(int epfd, int fd)
{struct epoll_event ev;ev.events = EPOLLIN;    // 关注读事件ev.data.fd = fd;        // 绑定要监控的文件描述符// 向epoll实例添加文件描述符及对应的事件int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);if (-1 == ret){perror("add fd");return 1;}return 0;
}/*** 从epoll实例中删除文件描述符* @param epfd epoll实例的文件描述符* @param fd 要删除的文件描述符* @return 0表示成功,1表示失败*/
int del_fd(int epfd, int fd)
{struct epoll_event ev;ev.events = EPOLLIN;ev.data.fd = fd;// 从epoll实例中删除文件描述符int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &ev);if (-1 == ret){perror("add fd");  // 原代码错误信息,应为"del fd"return 1;}return 0;
}int main(int argc, char **argv)
{// 创建监听套接字(IPv4协议,字节流服务,默认TCP协议)int listfd = socket(AF_INET, SOCK_STREAM, 0);if (-1 == listfd){perror("scoket error\n");return 1;}// 定义服务器和客户端的地址结构体struct sockaddr_in ser, cli;bzero(&ser, sizeof(ser));bzero(&cli, sizeof(cli));ser.sin_family = AF_INET;                // 使用IPv4地址族ser.sin_port = htons(50000);             // 设置端口号ser.sin_addr.s_addr = INADDR_ANY;        // 绑定到所有可用网络接口// 将监听套接字与服务器地址绑定int ret = bind(listfd, (SA)&ser, sizeof(ser));if (-1 == ret){perror("bind");return 1;}// 开始监听listen(listfd, 3);socklen_t len = sizeof(cli);// 1. 创建epoll实例struct epoll_event rev[10];  // 用于存放epoll_wait返回的就绪事件int epfd = epoll_create(10);if (-1 == epfd){perror("epoll_creaete");  // 原代码拼写错误return 1;}// 2. 将监听套接字添加到epoll实例中add_fd(epfd, listfd);time_t tm;while (1){// 等待epoll实例中的事件就绪int ep_ret = epoll_wait(epfd, rev, 10, -1);// 遍历所有就绪事件for (int i = 0; i < ep_ret; i++){// 如果是监听套接字就绪,表示有新的客户端连接请求if (rev[i].data.fd == listfd){// 接受客户端连接int conn = accept(listfd, (SA)&cli, &len);if (-1 == conn){perror("accept");close(conn);continue;}// 将新的连接套接字添加到epoll实例中add_fd(epfd, conn);}else  // 其他就绪事件,为客户端连接套接字的读事件{int conn = rev[i].data.fd;char buf[1024] = {0};// 从连接套接字接收数据int ret = recv(conn, buf, sizeof(buf), 0);if (ret <= 0){del_fd(epfd, conn);close(conn);continue;}// 获取当前时间戳time(&tm);// 将接收的数据与当前时间拼接sprintf(buf, "%s %s", buf, ctime(&tm));// 将拼接后的数据发送回客户端send(conn, buf, strlen(buf), 0);}}}close(listfd);return 0;
}

六、核心回顾

6.1 多路I/O复用

  • 定义:系统提供的I/O事件通知机制

  • 应用场景:单进程中需要处理多个阻塞I/O,希望及时知道哪个设备可读写

6.2 I/O模型对比

模型核心特点适用场景
阻塞I/O默认模式,进程挂起等待数据就绪简单场景,单事件流
非阻塞I/O设置O_NONBLOCK,立即返回+EAGAIN需避免阻塞但可接受忙等
信号驱动I/OO_ASYNC+SIGIO信号通知特殊场景(较少使用)
多进程/线程进程/线程处理独立事件流中小规模并发
多路复用单线程监控多个描述符高并发场景

6.3 select vs epoll 关键区别

  1. 描述符数量

    • select:固定上限(通常1024)

    • epoll:仅受系统限制

  2. 检测机制

    • select:轮询所有描述符(O(n))

    • epoll:事件驱动主动上报(O(1))

  3. 数据拷贝

    • select:每次调用全量拷贝用户/内核空间

    • epoll:共享内存(仅首次拷贝)


文章转载自:

http://usHKxha0.dmrjx.cn
http://Iu17SYGO.dmrjx.cn
http://W7c8qXEG.dmrjx.cn
http://egB148qW.dmrjx.cn
http://fHZOodXu.dmrjx.cn
http://DQGfKk7X.dmrjx.cn
http://WbSHWZyB.dmrjx.cn
http://3uBisTU4.dmrjx.cn
http://cuuAA1co.dmrjx.cn
http://KE4D8u8D.dmrjx.cn
http://Ufswy4SC.dmrjx.cn
http://KQYBp3ss.dmrjx.cn
http://Jyj2EF2Q.dmrjx.cn
http://2kJL032b.dmrjx.cn
http://K3eI0nSF.dmrjx.cn
http://jMtT8BXP.dmrjx.cn
http://smauz91P.dmrjx.cn
http://5hiy6zKI.dmrjx.cn
http://1IGeqUnk.dmrjx.cn
http://yYJhMbJ2.dmrjx.cn
http://DL0f7z9k.dmrjx.cn
http://nU3KDcZa.dmrjx.cn
http://w81ByG39.dmrjx.cn
http://WW7cQh9k.dmrjx.cn
http://KRqzX3Ch.dmrjx.cn
http://KzDtFV4P.dmrjx.cn
http://EvEq9Ozc.dmrjx.cn
http://7Mi6hvvy.dmrjx.cn
http://q8aDHvT5.dmrjx.cn
http://hpbXlQPT.dmrjx.cn
http://www.dtcms.com/a/370115.html

相关文章:

  • Nginx性能调优:参数详解与压测对比
  • java接口和抽象类有何区别
  • C/C++动态爱心
  • YOLOv8 在 Intel Mac 上的 Anaconda 一键安装教程
  • 关于 React 19 的四种组件通信方法
  • Joplin-解决 Node.js 中 “digital envelope routines::unsupported“ 错误
  • [论文阅读] 软件工程 - 需求工程 | 2012-2019年移动应用需求工程研究趋势:需求分析成焦点,数据源却藏着大问题?
  • sensitive-word 敏感词性能提升14倍优化全过程 v0.28.0
  • 留数法分解有理分式
  • 基于FPGA的汉明码编解码器系统(论文+源码)
  • C++经典的数据结构与算法之经典算法思想:排序算法
  • 大恒-NF相机如何控制风扇
  • 01.单例模式基类模块
  • 数位DP -
  • kotlin - 2个Fragment实现左右显示,左边列表,右边详情,平板横、竖屏切换
  • 基于SpringBoot+Thymeleaf开发的实验室助理工作管理系统
  • 手写MyBatis第53弹: @Intercepts与@Signature注解的工作原理
  • 基于SpringBoot+JSP开发的潮鞋网络商城
  • docker run 命令,不接it选项,run一个centos没有显示在运行,而run一个nginx却可以呢?
  • 【C++框架#3】Etcd 安装使用
  • 洛谷 P3178 [HAOI2015] 树上操作-提高+/省选-
  • Java全栈开发工程师的面试实战:从基础到复杂场景的技术探索
  • 【Flask】测试平台开发,重构提测管理页面-第二十篇
  • ICPC 2023 Nanjing R L 题 Elevator
  • TensorFlow 面试题及详细答案 120道(101-110)-- 底层原理与扩展
  • 《sklearn机器学习——聚类性能指标》Davies-Bouldin Index (戴维斯-博尔丁指数)
  • 美团9-6:编程题
  • 深度学习--自然语言预处理--- Word2Vec
  • Nikto 漏洞扫描工具使用指南
  • Redis(46) 如何搭建Redis哨兵?