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

Linux IO复用

这是一个非常核心的概念,是构建高性能网络服务器(如 Nginx、Redis、Node.js)的基石。

什么是IO复用?

“复用” 指的是用一个进程(或线程)来同时监听多个 I/O 事件例如多个网络套接字的可读、可写、异常等),而不是为每个连接创建一个线程/进程。

  • 传统阻塞 I/O 的问题:如果一个进程使用 read() 系统调用从 socket 读取数据,但数据还没到来,进程就会被操作系统挂起(阻塞),什么也做不了,直到数据到达。如果要处理 1000 个连接,就需要 1000 个线程或进程,上下文切换的开销巨大,资源消耗也极高。

  • I/O 复用的解决方案:让一个进程“守护”在多个 socket 上。当其中任何一个 socket 有我们关心的事件发生时(比如数据可读了),这个进程就会被唤醒,然后处理那个就绪的 socket。这样,一个进程就可以高效地管理成千上万的网络连接。

一个生动的比喻
一个餐厅(操作系统)有很多桌客人(Socket 连接)。

  • 阻塞 I/O:一个服务员(进程)专门服务一桌客人。客人点完菜,服务员就一直站在厨房门口等菜做好,期间不能做任何其他事。餐厅需要雇佣大量服务员。

  • I/O 复用:一个服务员(进程)同时照看多桌客人。她记下每桌客人的需求(注册到 epoll),然后就去忙别的了。当厨房通知“A桌的菜好了”(事件就绪),服务员就去给A桌上菜。一个服务员就能照看整个大厅。

为什么需要 I/O 复用?

  • 高性能与高并发:在连接数非常高的场景下(C10K、C1000K 问题),创建大量进程/线程是不可行的。I/O 复用可以用 1 个或少量几个 进程/线程处理所有连接,极大减少资源消耗。

  • 避免阻塞:应用程序无需在等待某个慢速 I/O 操作(如网络请求)时完全停止,可以充分利用 CPU 时间去处理其他已经就绪的任务。

主要的 I/O 复用机制

Linux 提供了三种主要的机制,它们不断演进,能力也越来越强。

select - 第一代

工作原理

  • 应用程序将所有需要监听的文件描述符 (fd) 的集合通过 FD_SET 宏添加到 fd_set 中。

  • 调用 select 函数,将 fd_set(可读、可写、异常集合)传入内核。

  • 内核会线性扫描所有传入的 fd,判断是否有事件发生。

  • 当有事件发生或超时后,select 返回,并修改 fd_set 集合,只保留就绪的 fd。

  • 应用程序需要遍历整个 fd_set 才能找到哪些 fd 就绪了。

缺点

  • 监听的 fd 数量有上限:通常由 FD_SETSIZE(默认 1024)定义,编译时确定,难以修改。

  • 性能随 fd 数量线性下降:每次调用都需要将整个 fd_set 从用户态拷贝到内核态,内核也需要遍历所有 fd,返回后用户态还需要再次遍历。fd 越多,开销越大。(重新遍历是因为:select 系统调用修改的是用户空间中的原始 fd_set,而不是内核内部的拷贝。

  • 内核会修改传入的 fd_set,因此每次调用前都需要重新设置参数。

    • 你调用 FD_SET 将你关心的 fd 添加到 readfds 等集合中。

    • 然后你调用 select,并将这些集合传入内核。

    • select 返回时,内核会修改这些集合。它会将所有未就绪的 fd 对应的比特位清零,只保留那些就绪的 fd 的比特位为 1

    • 这意味着,下一次调用 select 之前,你必须使用 FD_SET 重新添加所有你关心的 fd,因为上一次调用可能已经把你精心设置的集合改得面目全非了。

fd_set 是一个数据结构,用于代表一个“文件描述符的集合”。它本质上是一个位图(bit mask),其中的每一个比特(bit)代表一个文件描述符(file descriptor, fd)。

  • 如果某个比特被设置为 1,则表示这个文件描述符被包含在集合中,是 select 需要去监听的对象。

  • 如果某个比特为 0,则表示这个文件描述符不在当前集合中,select 会忽略它。

函数签名

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

poll - 第二代

工作原理
poll 解决了 select 的一些问题,但本质相似。

  • 应用程序提供一个 struct pollfd 数组,每个元素包含要监听的 fd 和感兴趣的事件。

  • 调用 poll 函数,将该数组传入内核。

  • 内核遍历这个数组,检查每个 fd 的事件。

  • 返回时,内核会修改每个 pollfd 中的 revents 字段,表示哪些事件就绪。

  • 应用程序需要遍历整个数组,检查每个元素的 revents 字段。

对比 select 的改进

  • 没有最大数量限制(仅受系统资源限制)。

  • 使用事件的方式,而不是原始的 fd 集合,提供了更多的事件类型。

  • 内核不会“破坏”传入的参数,无需每次调用前重置。

缺点

  • select 一样,性能会随着监听 fd 数量的增加而线性下降。因为内核和用户空间都需要遍历整个列表。

数据结构与函数

struct pollfd {int   fd;         /* 文件描述符 */short events;     /* 请求的事件(我们关心什么) */short revents;    /* 返回的事件(实际发生了什么) */
};int poll(struct pollfd *fds, nfds_t nfds, int timeout);

epoll - 第三代 (Linux 2.6+,现代首选)

epoll 是 Linux 为解决 select/poll 的性能瓶颈而设计的,是高性能网络编程的事实标准

核心改进

  • 无需遍历epoll 使用一个红黑树在内核中管理所有需要监听的 fd,使得添加和删除 fd 的效率很高(O(log n))。

  • 事件回调:当某个 fd 有事件发生时,内核会通过回调函数将其直接插入到一个就绪链表中。

  • 只返回就绪的 fdepoll_wait 调用返回时,只需从就绪链表中取出元素即可,直接拿到了就绪的 fd,无需遍历所有监听的 fd。这使得效率与活跃连接数(就绪的 fd 数)成正比,而与总连接数无关。在处理大量空闲连接时,性能优势巨大。

三个核心 API

  • epoll_create / epoll_create1:创建一个 epoll 实例,返回一个文件描述符(epfd)。
int epoll_create1(int flags); // 常用,flags 可设为 0
  • epoll_ctl:向 epoll 实例(epfd)中添加、修改或删除需要监听的 fd 及其事件。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// op: EPOLL_CTL_ADD, EPOLL_CTL_MOD, EPOLL_CTL_DEL
  • epoll_wait:等待事件发生。它阻塞调用,直到有事件发生或超时。返回的是发生事件的 epoll_event 数组,直接就是就绪的 fd,无需遍历。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

工作模式

  • 水平触发 (LT - Level-Triggered):默认模式。只要 fd 处于就绪状态(例如,socket 接收缓冲区不为空,可读),epoll_wait 就会持续通知你。如果你这次没有读完数据,下次调用 epoll_wait 它还会再次通知你。编程更简单,不容易遗漏事件。

  • 边缘触发 (ET - Edge-Triggered):只有当 fd 状态发生变化时(例如,从不可读变为可读),epoll_wait 才会通知你一次。如果一次没有读完数据,且没有新的数据到来,剩余的数据将不会触发新的通知。ET 模式效率更高,但要求应用程序必须一次性读完或写完所有数据(通常使用非阻塞 I/O 循环读写,直到返回 EAGAINEWOULDBLOCK 错误)。

对比

特性selectpollepoll
性能随 fd 数量线性下降随 fd 数量线性下降活跃 fd 数相关,性能高
最大连接数有上限 (FD_SETSIZE)无上限 (系统资源限制)无上限 (系统资源限制)
工作效率每次调用都需全量传入/传出和遍历每次调用都需全量传入/传出和遍历内核红黑树管理,只返回就绪的fd
事件粒度较粗,仅可读、可写、异常较细,更多事件类型非常精细,支持丰富的事件
触法模式LTLT支持 LT 和 ET(高性能)
跨平台几乎所有平台大多数 UNIXLinux 特有

使用一个简单的epoll代码框架

#include <sys/epoll.h>
#include <unistd.h>#define MAX_EVENTS 10int main() {int listen_sock, conn_sock, nfds, epollfd;struct epoll_event ev, events[MAX_EVENTS];// 1. 创建 epoll 实例epollfd = epoll_create1(0);if (epollfd == -1) { /* 错误处理 */ }// 2. 添加监听套接字到 epoll,关注可读事件(新连接)ev.events = EPOLLIN; // 水平触发 LT// ev.events = EPOLLIN | EPOLLET; // 边缘触发 ETev.data.fd = listen_sock;if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) { /* 错误处理 */ }for (;;) {// 3. 等待事件发生nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1); // -1 表示永久阻塞if (nfds == -1) { /* 错误处理 */ }// 4. 处理就绪的事件 (nfds 个)for (int n = 0; n < nfds; ++n) {if (events[n].data.fd == listen_sock) {// 有新连接到来conn_sock = accept(listen_sock, ...);// ... 设置 conn_sock 为非阻塞(如果是ET模式必须)ev.events = EPOLLIN | EPOLLET; // 为新连接设置ET模式ev.data.fd = conn_sock;epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock, &ev);} else {// 已有连接可读或可写if (events[n].events & EPOLLIN) {// 可读事件,读取数据// ... 如果是ET模式,必须循环 read 直到 EAGAIN}if (events[n].events & EPOLLOUT) {// 可写事件,发送数据}// ... 处理其他事件,如 EPOLLERR, EPOLLHUP}}}close(epollfd);return 0;
}

小结

  • 现代高性能网络程序首选 epoll

  • 理解 LT 和 ET 模式的区别及其编程模型至关重要。

  • selectpoll 因其可移植性,在一些简单的场景或非 Linux 平台上仍有价值,但在 Linux 上处理大量连接时,应毫不犹豫地选择 epoll

http://www.xdnf.cn/news/1401625.html

相关文章:

  • 优选算法:二分查找
  • 数据库攻略:“CMU 15-445”Project0:C++ Primer(2024 Fall)
  • 《Java反射与动态代理:从原理到实践》
  • SpringBoot整合Actuator实现健康检查
  • MIT 6.5840 (Spring, 2024) 通关指南——Lab 1: MapReduce
  • GitHub 热榜项目 - 日榜(2025-08-30)
  • 基于Ubuntu本地GitLab 搭建 Git 服务器
  • 解构机器学习:如何从零开始设计一个学习系统?
  • 【LeetCode】大厂面试算法真题回忆(121) —— 经典屏保
  • 并发编程——09 CountDownLatch源码分析
  • Spring Boot 后端接收多个文件的方法
  • 项目管理常用的方法有哪些
  • 三菱 PLC的中断指令/中断指针
  • 构建现代化的“历史上的今天“网站:从API到精美UI的全栈实践
  • 北方苍鹰优化算法优化的最小二乘支持向量机NGO-LSSVM多输入多输出回归预测【MATLAB】
  • 2025年06月 Scratch 图形化(二级)真题解析#中国电子学会#全国青少年软件编程等级考试
  • Robolectric如何启动一个Activity
  • 倾斜摄影是选择RGB图像还是多光谱影响进行操作?
  • Transformer:从入门到精通
  • 嵌入式Linux驱动开发:蜂鸣器驱动
  • stack queue的实现 deque的底层结构 priority_queue的实现
  • 【Java实战⑦】从入门到精通:Java异常处理实战指南
  • 漫谈《数字图像处理》之分水岭分割
  • AUTOSAR进阶图解==>AUTOSAR_TR_ClassicPlatformReleaseOverview
  • 计算机毕设项目 基于Python与机器学习的B站视频热度分析与预测系统 基于随机森林算法的B站视频内容热度预测系统
  • observer pattern 最简上手笔记
  • 如何调整Linux系统下单个文件的最大大小?
  • hadoop安欣医院挂号看诊管理系统(代码+数据库+LW)
  • 2025年高性能计算年会
  • centos7.9的openssh漏洞修复脚本