2. 这才是你要看的 网络I/O模型

网络 IO 面对的最直接问题

其实网络编程面对的主要问题还是比较明确的,那就是:CPU的速度远远快于磁盘、网络等IO。在一个线程中,CPU执行代码的速度极快,然而,一旦遇到IO操作,如读写文件、发送网络数据时,就需要等待IO操作完成,才能继续进行下一步操作。

《Unix 网络编程》表述不清晰的 I/O 模型

对于这本书中提到的 IO 模型,学过网络编程的可能都了解过,书也有比较老了,这边只是将知识点罗列出来,大致知道一下这几个 IO 模型:

  • 阻塞式 I/O
  • 非阻塞式 I/O
  • I/O复用
  • 信号驱动式 I/O
  • 异步 I/O

这本书对于 I/O 模型的讲解其实比较乱,因为 同步/异步阻塞/非阻塞 这几个概念本身是清晰独立的,首先我们应该先要做到能够清晰区分这几个概念。其次它们更多的是组合搭配出现的 (如果 加上 单线程/多线程 的概念,这个 I/O 模型其实会更多)。因此在介绍 I/O 模型时,我认为不应该把 阻塞 I/O 、 非阻塞 I/O异步 I/O 概念这样混淆讲解,要讲解阻塞 I/O 、 非阻塞 I/O 就应该表述当前是 同步还是 异步 。应该表述更清晰一些。

  • 同步阻塞 I/O
  • 同步非阻塞 I/O
  • 异步阻塞 I/O
  • 异步非阻塞 I/O
  • I/O多路复用( 同步阻塞于多路复用器: epoll_wait 对应用程序来说是阻塞的
  • 异步I/O(真正意义上的异步非阻塞)

信号驱动 I/O 用的比较少,这里不需要对它有过多了解。

就绪通知 VS 真正的异步 IO

对于异步 I/O,书中对概念的解释,我认为是准确的:"告知内核启动某个操作,并让内核在 整个操作 (包括将数据从内核复制到我们自己的缓冲区)完成后通知我们。"

这里注意是整个操作 ,因为 I/O 复用在内核通知时,其实就不是整个操作完成后才通知,而是 I/O 事件准备就绪后就通知我们(应用程序),然后由应用程序主动去进行 I/O 读、写 ......操作。 (这也是为什么我们手写的基础socket代码中一般会有两层 while 循环,外层while(true)循环主要是调用操作系统底层的epoll,此处虽然阻塞,但实则是进行高效的等待,而内层循环则是遍历就绪的事件来进行读写。整个拿到数据的操作并不是由操作系统整体完成的)

I/O 多路复用:你告诉内核"帮我监视这100个socket,如果哪个有数据可读了通知我"。当通知到来时,它只是告诉你"某个socket就绪了" ,但数据的读写操作(从内核缓冲区拷贝到用户缓冲区)仍然需要你的线程自己调用read()函数来完成。所以,这个"通知"只是一个"就绪通知 ",I/O 操作本身还是同步的。

真异步I/O:你告诉内核"去读这个socket,读满1KB数据到这个缓冲区,全部搞定后通知我"。数据的等待和拷贝都由内核完成,给你的通知是"完成通知"。

"同步/异步"和"阻塞/非阻塞"是不同维度的概念

"同步/异步"和"阻塞/非阻塞"是描述不同维度的概念

同步 vs 异步

关注的是 消息通信机制或任务完成的通知方式

  • 同步:调用者发起调用后,必须亲自等待结果(无论是死等还是不断询问),才能继续执行后续代码。强调的是"主动等待结果"
  • 异步:调用者发起调用后,不必亲自等待结果。当有结果时,会通过某种机制(如回调函数、信号、事件) 来通知调用者。强调的是"被通知结果" 。(当然,人家告诉你不必亲自等待结果,因此你可以选择做任何别的事情,这样就非阻塞了。但你也可以选择啥也不干就在这儿死等,这就又阻塞了,相信肯定没人这么干的~ 😒。)

阻塞 vs 非阻塞

关注的是 调用者在等待结果期间的状态

  • 阻塞:调用发起后,在拿到结果之前,调用者所在的线程会被挂起,无法执行任何其他操作。
  • 非阻塞:调用发起后,在拿到结果之前,调用者所在的线程不会被挂起,可以继续做其他操作。

必须掌握的概念

同步阻塞

这是最容易理解的。通知方式是:主动等待; 等待结果期间的状态:啥也不干,死等;

行为:你(线程) 去书店买一本书(发起 I/O 请求),书没到货。你就一直坐在书店里等,直到书到货了你拿到书(同步等待结果),在等待期间你啥也不干(阻塞),然后才回家。

同步非阻塞

通知方式是:主动轮训; 等待结果期间的状态:去干别的事儿;

行为:你还是去书店买书,书没到货。但这次你不坐着傻等,而是离开书店做别的事情了(非阻塞)。但你心里一直惦记看这事,于是你每隔5分钟就跑回书店问一次:"书到了吗?"(主动轮询)。这个过程是同步的,因为是你主动地、反复地去检查结果。

注意:"同步"不代表线程一定要被"阻塞"。

同步非阻塞的核心是"轮询",需要不断地主动去询问结果,这本身是一种同步行为。 而关于轮询的效率问题,后面会详细讲到 (比如 selector.select() 在调用操作系统底层的 IO 多路复用系统调用时,同步阻塞其实会释放 CPU,不至 CPU 飙升至 100%),只不过,这个阻塞的selector.select() 在操作系统内部依赖了事件通知和高效监控多事件的方式,效率非常高。

异步非阻塞

这是标准的、高效的异步模式。

通知方式是:被通知; 等待结果期间的状态:去干别的事儿;

行为:你再去书店买书。这次你告诉店员:"书到了以后,请打电话通知我(回调通知),我就不在这等了"。然后你直接回家该干嘛干嘛(非阻塞)。等书到了,店员打电话给你,你再去取。整个过程你既没有傻等,也没有反复跑腿。

异步阻塞

这个组合很反直觉,可以理解为一个"设计失误"。

通知方式是:被通知; 等待结果期间的状态:啥也不干,死等;

行为:你告诉店员"书到了通知我"(异步发起请求)。但说完之后,你却没走,反而选择坐在书店里干等(阻塞)。你虽然采用了异步的通信方式,但自己的行为却是阻塞的。这相当于浪费了异步的优势。

例子:很少见。一个可能的例子是,调用了一个异步API,但随后又调用了一个阻塞的函数去等待这个异步操作完成。例如,在Python的 asyncio 中,如果在异步函数里错误地使用了阻塞式的调用,就会导致整个事件循环被阻塞。

小结:"异步"几乎总是和"非阻塞"搭配使用才能发挥其价值。"异步阻塞"是一种不高效的、通常应该避免的模式。

不要混淆的概念

"同步"不代表线程一定要被"阻塞";同步也可以非阻塞,但核心是"需要主动轮询"(因为非阻塞,线程没有被挂起,但需要不断地主动去询问结果,这本身是一种同步行为)。

"异步"不代表一定是"非阻塞"的,你一不小心可能写出"异步阻塞"的模式,要小心。

windows 对异步I/O的支持成熟稳定

Windows IOCP: Windows的 I/O完成端口 是真正的、成熟的原生异步IO支持。

当你发起一个读写请求(如 ReadFile)时,系统内核会真正接管整个IO操作。你的应用程序线程可以完全不去管这个I0,当内核完成所有数据从设备到内核缓冲区,再到你指定的用户态缓冲区的拷贝后,会向IOCP队列投递一个完成通知。这个过程应用程序线程是完全自由的。

Linux 对异步I/O的支持尚未成熟

历史上,Linux的异步 I/O 主要有两套API:

  1. aio(libaio):这套API设计有缺陷,最主要的问题是:
  • 仅对"直接 I/O "支持较好:对于缓冲式IO(比如普通的文件读写)支持不完整或仍然是模拟的。
  • 限制多:有很多限制,比如IO请求必须对齐到特定边界、目标文件描述符有特定要求等。
  • 因此,它虽然存在,但在很多场景下并不实用,导致大家形成了"Linux没有真异步"的印象。
  1. select/poll/epoll:它们本质上是 I/O 多路复用,而不是异步 I/O 。这是最关键的区别。
  • I/O 多路复用:你告诉内核"帮我监视这100个socket,如果哪个有数据可读了通知我"。当通知到来时,它只是告诉你"某个socket就绪了" ,但数据的读写操作(从内核缓冲区拷贝到用户缓冲区)仍然需要你的线程自己调用read()函数来完成。所以,这个"通知"只是一个"就绪通知 ",I/O 操作本身还是同步的。
  • 真异步I/O:你告诉内核"去读这个socket,读满1KB数据到这个缓冲区,全部搞定后通知我"。数据的等待和拷贝都由内核完成,给你的通知是"完成通知"。

现状:现代Linux内核(5.1+)大力发展的 io_uring技术,提供了不逊色于甚至优于IOCP的真正异步 I/O 支持。它通过两个无锁的环形队列与内核高效交互,同时支持存储IO和网络10,正在成为Linux上高性能异步编程的新标准。

选择稳定的 epoll

现在几乎大部分的网络编程框架,在 linux 操作系统上,底层都是基于 Linux 操作系统内核的 epoll I/O 多路复用来提升性能的。(nginx, redis, netty 底层都还是基于 I/O多路复用技术,并没有用Linux系统内核的异步)

相关推荐
野犬寒鸦3 小时前
从零起步学习MySQL || 第五章:select语句的执行过程是怎么样的?(结合源码深度解析)
java·服务器·数据库·后端·mysql·adb
橘子海全栈攻城狮3 小时前
【源码+文档+调试讲解】基于SpringBoot + Vue的知识产权管理系统 041
java·vue.js·人工智能·spring boot·后端·安全·spring
调试人生的显微镜4 小时前
iOS 26 文件导出全攻略,从系统限制到多工具协作实践
后端
该用户已不存在4 小时前
这6个网站一旦知道就离不开了
前端·后端·github
LSTM974 小时前
使用 Python 将 PDF 转成 Excel:高效数据提取的自动化之道
后端
英伦传奇4 小时前
Docker部署MySQL 8.0
后端
Ai行者心易4 小时前
10天!前端用coze,后端用Trae IDE+Claude Code从0开始构建到平台上线
前端·后端
00后程序员4 小时前
开发代码的前端工具全流程分享 从编辑、构建到调试的实战经验
后端
用户68545375977694 小时前
🎴 Card Table & Remember Set:GC的超级加速器!
后端