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系统内核的异步)

相关推荐
IT_陈寒21 分钟前
JavaScript的闭包把我坑惨了,说好的内存会自动回收呢?
前端·人工智能·后端
CaffeinePro1 小时前
Pydantic深度使用:数据校验、枚举、ORM映射
后端·fastapi
Chenyiax2 小时前
从 Chat 到 Responses:OpenAI API 抽象为什么变了?
后端
MariaH2 小时前
Koa和Express的区别
后端
MariaH2 小时前
Koa框架的使用
后端
luckdewei3 小时前
那个用 passlib 做认证的新同事,上线第一天就把用户密码写进了日志
后端
ping某4 小时前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
JustHappy4 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
uhakadotcom4 小时前
在python 的 工程化架构中 ,什么是 薄包装器层?
后端·面试·github
用户1474853079749 小时前
CodeX使用Skill生成游戏美术和音乐资源,一分钟入门
后端