什么是 IO 多路复用?
IO 多路复用(IO Multiplexing) 是一种高效的 I/O 模型,它允许单个线程同时监控多个文件描述符(File Descriptor, FD)(通常是网络连接/Socket)。
简单来说,就是用一个线程来处理成千上万个并发连接。
为了让你彻底理解,我们可以从“生活案例”、“技术演进”和“底层实现”三个角度来解释。
1. 生活案例:餐厅的点餐服务
想象一个餐厅(服务器)要接待很多顾客(客户端连接):
阻塞 I/O (BIO) - 多线程模型:
- 做法: 餐厅为每一桌顾客都专门配一名服务员。服务员站在桌边,直到顾客点完菜才离开。
- 缺点: 顾客如果不点菜(连接空闲),服务员就傻站着(线程阻塞)。如果来了 1000 桌客人,就需要 1000 个服务员,老板(CPU/内存)会破产。
非阻塞 I/O (NIO) - 轮询模型:
- 做法: 只有一名服务员。他不停地在所有桌子之间跑来跑去,问第一桌:“点好了吗?”,问第二桌:“点好了吗?”……
- 缺点: 即使所有顾客都没准备好,服务员也跑断了腿(CPU 空转,利用率极低,浪费资源)。
IO 多路复用 - 事件驱动模型:
- 做法: 只有一名服务员,但他站在吧台。顾客桌上有一个按钮(注册事件)。谁准备好点菜了,就按一下按钮,吧台的灯亮起,服务员就知道“3号桌”准备好了,然后过去处理。
- 优点: 服务员不用傻站着,也不用瞎跑。只有当顾客真的有需求时,服务员才工作。一个人就能轻松应对所有桌子。
2. 技术演进:为什么要用它?
在没有 IO 多路复用之前,处理高并发网络请求主要有两种方式,但都有缺陷:
- 多进程/多线程 (BIO):
- 每来一个连接,就创建一个线程。
- 问题: 线程切换(Context Switch)开销大,内存占用高。当连接数达到几万时,系统直接崩溃。
- 非阻塞 I/O (NIO) + 忙轮询:
- 把 Socket 设置为非阻塞,程序不断循环检查是否有数据。
- 问题: CPU 会一直处于 100% 运转状态,大部分时间都在做无用功。
IO 多路复用的出现解决了这个问题:
它把“检查连接是否有数据”的工作交给了操作系统内核。应用进程只需要调用一个函数(如 select、poll 或 epoll),然后阻塞等待。一旦有一个或多个连接有数据了,内核就会唤醒应用进程,并告诉它是哪些连接准备好了。
3. 核心实现:Select、Poll、Epoll
在 Linux 系统中,实现 IO 多路复用主要有三个系统调用,它们的性能差异很大:
1. Select (早期实现)
- 机制: 你把所有要监控的连接(FD)打成一个包传给内核。内核遍历检查一遍,如果有数据,就通知你。
- 缺点:
- 限制: 默认只能监控 1024 个连接。
- 效率低: 每次调用都要把 FD 集合从用户态拷贝到内核态;内核需要遍历所有 FD 才能知道谁有数据(时间复杂度 O(n))。
2. Poll
- 机制: 和 Select 类似,但去掉了 1024 的限制(使用链表)。
- 缺点: 依然是遍历模式。如果监控 10 万个连接,只有 1 个活跃,内核还是要扫描这 10 万个连接,效率依然随着连接数增加而线性下降。
3. Epoll (现代主流,Linux 特有)
- 机制:
- 内核里维护一颗红黑树,记录所有要监控的连接。
- 当网卡收到数据时,通过回调机制,直接把活跃的连接加入到一个“就绪链表”中。
- 应用进程只需要问内核:“就绪链表里有东西吗?”
- 优点:
- 无限制: 连接数只受内存限制。
- 极快: 时间复杂度 O(1)。不管连接数有一万还是一百万,只要活跃连接数少,性能就不会下降。它只关心“活跃”的连接。
4. 总结
IO 多路复用 是一种同步非阻塞 I/O 模型。
- 多路: 指的是多个网络连接(Socket)。
- 复用: 指的是复用同一个线程。
它的核心价值在于: 避免了多线程带来的上下文切换开销,同时避免了非阻塞 IO 的 CPU 空转问题,使得单机能够支撑数万甚至数百万的并发连接。
常见应用:
- Redis: 它是单线程的,但通过 Epoll 实现了极高的吞吐量。
- Nginx: 使用 Epoll 模型,成为高性能反向代理服务器的首选。
- Node.js: 底层库 libuv 在 Linux 上也是基于 Epoll 实现的。
右滑查看面试常问