4、I/O多路复用入门:什么是I/O多路复用、select/poll/epoll发展史、适用场景对比
聊到高性能网络编程,有个概念你肯定绕不开——I/O多路复用。
说白了,就是让一个线程能同时盯着成百上千个socket。哪个socket有数据来了,就去处理哪个。不用再给每个连接都配一个线程,也不用傻乎乎地轮询。
我记得刚入行那会儿,接手一个老项目,用的是最原始的阻塞式模型。来一个连接就fork一个进程。连接数一上到几百,服务器直接卡死。后来换成select,才算是打开了新世界的大门。
4.1 什么是I/O多路复用?
先看个最朴素的场景。
你写了一个服务端,需要同时处理多个客户端的数据。最直接的做法是:
- 多线程/多进程:每个连接一个线程。连接多了,线程切换开销巨大。
- 非阻塞轮询:用一个循环,挨个检查每个socket。CPU空转,浪费严重。
这两种方式,说白了都不够优雅。
I/O多路复用的思路是:交给内核去帮我们盯着这些socket。你告诉内核:“帮我看着这100个socket,哪个可读/可写了,通知我一声。”然后你阻塞在系统调用上。内核一旦发现有socket就绪,就返回给你。你再挨个处理就绪的socket。
这样,一个线程就能管理成千上万个连接。效率高得多。
核心本质:I/O多路复用是一种同步I/O模型。它只是帮我们“监听”多个文件描述符的状态,实际的读写操作还是由应用程序自己完成。别跟异步I/O搞混了。
为了让你更直观地理解,我画了一张图:
4.2 发展史:从select到poll再到epoll
这东西不是一蹴而就的。我经历过从select到epoll的迁移,感触很深。
4.2.1 select:老前辈,但问题不少
select是最早出现在POSIX标准里的。它的工作方式很简单:
- 把要监听的fd集合传给内核。
- 内核遍历所有fd,检查状态。
- 返回时,把就绪的fd集合返回给用户。
听着还行?但坑很多。
我曾经踩过的坑:select默认最大监听1024个fd。有一次线上服务连接数刚过1000,新连接死活连不上。查了半天,才发现是FD_SETSIZE的限制。后来改源码重新编译才解决。
select的主要问题:
- fd数量限制:默认1024,虽然能改,但本质是硬伤。
- 重复拷贝:每次调用都要把整个fd集合从用户态拷贝到内核态。连接多了,拷贝开销巨大。
- 全量遍历:内核不知道哪些fd是活跃的,只能全部遍历一遍。O(n)复杂度。
- 修改fd集合:内核会修改传入的fd集合,下次调用前必须重新初始化。
4.2.2 poll:解决了上限,但没解决本质
poll的出现,算是解决了select的“1024魔咒”。它用链表来存储fd,理论上没有上限。
但其他问题还在:
- 依然要全量拷贝fd列表到内核。
- 内核依然要全量遍历所有fd。
- 依然是O(n)的复杂度。
说白了,poll只是把select的“数组”换成了“链表”,治标不治本。
4.2.3 epoll:Linux下的王者
epoll是Linux 2.6内核引入的。它彻底改变了游戏规则。
它的核心改进有三点:
| 特性 | select/poll | epoll |
|---|---|---|
| fd数量 | 有限制(select 1024) | 无上限(受系统内存限制) |
| 拷贝方式 | 每次调用全量拷贝 | 通过epoll_create创建红黑树,增量注册 |
| 就绪通知 | 全量遍历,O(n) | 回调机制,只返回就绪fd,O(1) |
| 工作模式 | 仅LT(水平触发) | 支持LT和ET(边缘触发) |
epoll的API只有三个函数:
// 1. 创建一个epoll实例,返回一个文件描述符
int epoll_create(int size); // size参数在Linux 2.6.8后已废弃,但必须大于0
// 2. 注册/修改/删除要监听的fd
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 3. 等待事件发生
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
我个人习惯:用epoll时,epoll_create的size参数我习惯传1。因为内核已经不再依赖这个参数了。但为了兼容老版本,传个大于0的值就行。
4.3 适用场景对比
没有银弹。每种模型都有它最舒服的战场。
4.3.1 什么时候用select?
- 跨平台需求(Windows、Linux、macOS都支持)。
- 监听的fd数量很少(几十个以内)。
- 对性能要求不高,快速实现原型。
4.3.2 什么时候用poll?
- 需要突破1024限制,但又不想用epoll(比如某些嵌入式环境)。
- fd数量在几千以内,且对性能不敏感。
说实话,我在生产环境中已经很少用poll了。它比select好不了多少。
4.3.3 什么时候用epoll?
- 高并发网络服务:比如Web服务器、消息队列、游戏服务器。连接数动辄上万甚至几十万。
- 需要高性能I/O:比如Redis、Nginx、Node.js的底层都依赖epoll。
- Linux平台:epoll是Linux专属。macOS用kqueue,Windows用IOCP。
避坑指南:我曾经在一个项目里,把epoll用在了一个只有几十个连接的管理后台。结果代码复杂度上去了,性能提升几乎感觉不到。后来换回select,代码清爽多了。所以,别为了用而用。选型要看场景。
4.4 一张图总结
我把三者的核心差异整理成了一张对比图: