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 revs[64]; while (1) { int n = epoll_wait(epfd, revs, 64, -1); for (int i = 0; i < n; i++) { int fd = revs[i].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); } } } } |

相关推荐
gfdhy19 小时前
【Linux】服务器网络与安全核心配置|静态IP+SSH加固+防火墙,公网服务器必学实操
linux·服务器·网络·tcp/ip·算法·安全·哈希算法
somi719 小时前
Linux-网络通信02-UDP 与 TCP Socket
linux·网络·udp·tcp
Hello World . .19 小时前
Linux:网络编程-UDP通信
linux·网络·udp
susu108301891119 小时前
ubuntu重做系统后无法apt update
linux·运维·ubuntu
蜕变的小白19 小时前
Linux系统编程:揭秘网络通信 IP与端口号的奥秘
linux·网络·网络协议·tcp/ip
爱倒腾的老唐19 小时前
02、STM32——嵌入式芯片
linux·stm32·嵌入式硬件
AryShaw20 小时前
macOS 上搭建 RK3568 交叉编译环境
linux·macos
芒果披萨20 小时前
Linux文件类基础命令行1
linux·运维·服务器
学嵌入式的小杨同学20 小时前
STM32 进阶封神之路(八):外部中断 EXTI 实战 —— 按键检测从轮询到中断(库函数 + 寄存器双版本)
linux·stm32·单片机·嵌入式硬件·mcu·架构·硬件架构
杨云龙UP1 天前
ODA服务器RAC节点2/u01分区在线扩容操作记录及后续处理流程(Linux LVM + ext4 文件系统在线扩容操作手册)_20260307
linux·运维·服务器·数据库·ubuntu·centos