Linux—网络通信04-IO多路复用

一、IO 多路复用 --- 概念

定义:在单线程/单进程中,同时监测多个文件描述符(fd)是否可以执行 IO 操作的能力,也称为 IO 事件(读/写)的通知机制。

核心价值:

  • 应用程序通常需要处理来自多条事件流的事件(键盘/鼠标输入、网络连接等)
  • Web 服务器(如 Nginx)需要同时处理来自 N 个客户端的请求
  • 用单个执行体检测多个阻塞设备,避免为每个连接创建进程/线程的开销

二、五种 IO 模型

|-------------------|-------------------------------------------------------------|
| IO 模型 | 说明 |
| 阻塞 IO | 默认模式。调用 read/recv 后线程挂起,直到数据就绪才返回。 |
| 非阻塞 IO | 使用 fcntl 设置 O_NONBLOCK。数据未就绪时立即返回 EAGAIN,需要轮询(忙等待),CPU 消耗高。 |
| 信号驱动 IO | 使用 SIGIO 信号通知。数据就绪时系统发信号给进程,用得相对少,了解即可。 |
| 并行模型 | 多进程 / 多线程各自独立处理 IO,编程简单但资源消耗大。 |
| ⑤ IO 多路复用 | select / poll / epoll。单线程监控多个 fd,高并发服务器首选。 |

三、select

3.1 使用步骤

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ① 定义描述符集合(fd_set),并清零: FD_ZERO(&rd_set) ② 向集合中添加需要监测的 fd: FD_SET(fd, &rd_set) ③ 调用 select,阻塞等待通知(轮询) ④ select 返回后,遍历 fd 集合,用 FD_ISSET 找到就绪的 fd ⑤ 对就绪 fd 执行 read / recv ⑥ 清除标志位,重新添加 fd,循环执行步骤 ③ |

3.2 函数原型

|----------------------------------------------------------------------------------------------------------|
| int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); |

|---------------|----------------------------------|
| 参数 | 说明 |
| nfds | 描述符上限值,通常为集合中最大 fd + 1,可直接写 1024 |
| readfds | 只读描述符集(最常用) |
| writefds | 只写描述符集 |
| exceptfds | 异常描述符集 |
| timeout | 超时设置;NULL = 永久阻塞;0 = 非阻塞 |
| 返回值 | > 0 就绪 fd 数量 | 0 超时 | -1 出错 |

3.3 辅助宏函数

|-------------------------|--------------------|
| 函数 | 作用 |
| FD_ZERO(&set) | 清空集合中所有描述符 |
| FD_SET(fd, &set) | 将 fd 添加到集合 |
| FD_CLR(fd, &set) | 从集合中删除 fd |
| FD_ISSET(fd, &set) | 判断 fd 是否在集合中(就绪检测) |

3.4 注意事

|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ⚠️ select 执行后会修改 readfds 集合,循环调用前必须重新设置(清除标志位) ⚠️ 超时版本:每次调用 select 前都要重新设置 timeout 结构体 ⚠️ 单个集合最多监测 1024 个描述符(FD_SETSIZE 限制) ⚠️ nfds 传入进程中最大 fd 的整数值,也可直接写 1024 |

四、epoll

4.1 使用步骤

|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ① epoll_create(size) --- 创建 epoll 实例(红黑树),得到 epfd ② epoll_ctl(EPOLL_CTL_ADD, fd, event) --- 向集合添加需要监测的 fd ③ epoll_wait(epfd, events, maxevents, timeout) --- 阻塞等待,主动上报(中断机制) ④ 遍历返回的 events 数组,对就绪 fd 执行读取操作 |

4.2 相关函数

epoll_create --- 创建集合

|-----------------------------|
| int epoll_create(int size); |

|----------|----------------------------------|
| size | 集合可存储 fd 的最大数量 |
| 返回值 | > 0 epfd(表示该红黑树的文件描述符) | -1 出错 |

epoll_ctl --- 管理集合

|----------------------------------------------------------------------|
| int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); |

|-------------------|--------------------------------------------------------|
| epfd | epoll_create 返回的集合描述符 |
| op | EPOLL_CTL_ADD 添加 / EPOLL_CTL_DEL 删除 / EPOLL_CTL_MOD 修改 |
| fd | 需要操作的文件描述符 |
| event.events | EPOLLIN(可读) / EPOLLOUT(可写) |
| event.data.fd | 用户自定义数据,查找 fd 时使用 |
| 返回值 | 0 成功 | -1 出错 |

epoll_wait --- 等待 IO 事件

|------------------------------------------------------------------------------------|
| int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); |

|---------------|--------------------------------|
| epfd | 被检测的集合 |
| events | 输出集合:就绪 fd 由系统写入此数组 |
| maxevents | 一次最多复制到 events 的 fd 数量 |
| timeout | -1 永久阻塞 | 0 非阻塞 | 5000 等待 5s |
| 返回值 | > 0 就绪 fd 数量 | 0 超时 | -1 出错 |

4.3 epoll 优势

|----------------------------------------------------------------------------------------------------------------------------------------|
| ✅ 不受 1024 个 fd 限制(无 FD_SETSIZE 限制) ✅ 采用主动上报(中断)机制,效率不随 fd 数量增多而下降 ✅ 使用共享内存,避免集合在用户层和内核层多次复制 ✅ epoll_wait 返回后,就绪 fd 已汇总在 events 数组,查找方便 |

五、select vs epoll 对比

|-----------------|---------------------|-----------------|
| 对比项 | select | epoll |
| fd 数量限制 | 最多 1024(FD_SETSIZE) | 无限制 |
| 通知机制 | 轮询(遍历所有 fd) | 主动上报(中断) |
| 内存拷贝 | 每次调用都需用户↔内核拷贝 | 使用共享内存,减少拷贝 |
| 就绪 fd 定位 | 需要遍历整个集合 | 直接从 events 数组获取 |
| 跨平台性 | 跨平台(POSIX 标准) | 仅限 Linux |
| 适用场景 | fd 数量少,连接数不高 | 高并发服务器,大量连接 |

六、服务器并发模型

6.1 进程版本(多进程)

|-----------------------------------------------------------------------------------------------------------------------------------------------------------|
| 原理:accept 后 fork() 创建子进程处理客户端 关键点 1:僵尸进程回收 --- 用 signal(SIGCHLD, handle) + wait(NULL) 关键点 2:父进程关闭 conn,子进程关闭 listfd(文件描述符隔离) 缺点:进程创建开销大(0~3G 用户空间),并发量有限 |

6.2 线程版本(多线程)

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 原理:accept 后 pthread_create 创建线程处理客户端 关键点 1:栈区回收 --- 使用 pthread_detach(pthread_self()) 设置分离属性 关键点 2:conn 参数传递 --- 工作线程中必须将 conn 本地保存后,再通知主线程 方案:用信号量 sem_post 通知主线程已完成复制,再继续 accept 关键点 3:int conn = *(int*)arg; // 从指针参数中复制 conn 到线程本地 |

6.3 IO 多路复用版本(epoll + 循环服务器)

|----------------------------------------------------------------------------------------------------------------------------------------------|
| 原理:单线程用 epoll 监控 listfd 和所有 connfd - listfd 就绪 → accept 新连接 → 将 connfd 加入 epoll - connfd 就绪 → recv 数据 → 处理 → send 响应 适用:高并发 Web Server、循环服务器 |

七、速查卡片

select 使用模板

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| fd_set rd_set, tmp_set; FD_ZERO(&rd_set); FD_SET(fd1, &rd_set); FD_SET(fd2, &rd_set); while (1) { tmp_set = rd_set; // 每次循环重置(select 会修改集合) int n = select(maxfd+1, &tmp_set, NULL, NULL, NULL); if (n < 0) { perror("select"); break; } for (int i = 0; i <= maxfd; i++) { if (FD_ISSET(i, &tmp_set)) { // i 号 fd 有数据可读 read(i, buf, sizeof(buf)); } } } |

epoll 使用模板

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| int epfd = epoll_create(1024); struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = listfd; epoll_ctl(epfd, EPOLL_CTL_ADD, listfd, &ev); struct epoll_event revs64; while (1) { int n = epoll_wait(epfd, revs, 64, -1); for (int i = 0; i < n; i++) { int fd = revsi.data.fd; if (fd == listfd) { int conn = accept(listfd, NULL, NULL); ev.data.fd = conn; epoll_ctl(epfd, EPOLL_CTL_ADD, conn, &ev); } else { int ret = recv(fd, buf, sizeof(buf), 0); if (ret <= 0) { epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL); close(fd); } else { send(fd, buf, ret, 0); } } } } |

相关推荐
HLC++7 小时前
Linux的进程间通信
android·linux·服务器
华清远见IT开放实验室8 小时前
实验室建设案例 | 石家庄科技信息职业学院嵌入式实验室——从底层硬件到系统应用,一所应用型高校的嵌入式人才培养这样落地
linux·arm开发·stm32·嵌入式硬件·高校·实验室建设
groundhappy10 小时前
idalib安装和codex ida-mcp配置
linux·开发语言·python
通信小小昕11 小时前
Ubuntu 26.04 中文输入法安装
linux·运维·ubuntu
张小姐的猫12 小时前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
栩栩云生13 小时前
AI 写代码犯的错,早被写进了错题集
linux·安全·ai编程
imc.1114 小时前
linux基础IO
linux·运维·服务器
BelongPanda16 小时前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
酷可达拉斯16 小时前
Linux操作系统-shell编程(0)
linux·运维·服务器·python·云计算
2301_7779983416 小时前
Linux中断机制:操作系统如何高效运行
linux·运维·服务器