一文搞懂网络通信的基石✅IO模型与零拷贝

简介: 【10月更文挑战第1天】本文深入探讨了网络通信中的IO模型及其优化方法——零拷贝技术。首先介绍了IO模型的概念及五种常见类型:同步阻塞、同步非阻塞、多路复用、信号驱动和异步IO模型。文章详细分析了每种模型的特点和适用场景,特别是多路复用和异步IO在高并发场景中的优势。接着介绍了零拷贝技术,通过DMA直接进行数据传输,避免了多次CPU拷贝,进一步提升了效率。最后总结了各种模型的优缺点,并提供了相关的代码示例和资源链接。

网络通信的基石:IO模型与零拷贝

中间件作为现代软件架构的基石,扮演着承上启下的关键角色,它不仅衔接了多样化的服务与系统,还极大地促进数据的流动与处理

而这一切高效运作的背后,网络通信是各大中间件中不可或缺的一环

如常见的WEB服务器(tomcat、jetty、undertow),数据库(MySQL、Redis),MQ...

它们都需要进行网络通信,那么如何才能高效的进行网络通信呢?

在聊这个话题前,我们需要先聊聊IO模型

什么是IO模型呢?

IO即输入/输出,IO模型的提出主要是解决计算机CPU在内存与磁盘/网卡等外部设备速度不匹配的问题

当CPU想要读取磁盘/网卡上的数据时,数据拷贝到内存是需要时间的,那这时CPU是去等待数据拷贝完成,还是先去执行其他任务呢?

举个简单的例子:

精通CRUD的小菜快速完成简单的CRUD,但还需要等待其他部门提供的接口,由于其他部门的业务比较复杂,接口文档可能要几天后才能给出,小菜想趁着这段时间休息一会

可是作为小菜上司的我可不乐意了,还有这么多开发任务呢,我再分一个任务给小菜做,让小菜不能空闲下来,等到后续其他部门的接口写好了,再通知小菜完成这个开发任务(提升小菜的“吞吐量”)

这个案例中小菜就是CPU,完成简单的CRUD可以看成在内存上操作(速度快),等待其他部门的接口可以看成等待外部设备把数据拷贝到内存(速度慢)

为了解决这个问题,我提出一种“IO模型”:让小菜先去干别的活,等其他部门的接口好了再通知小菜回来完成这个开发任务

步入正题,常见的IO模型分为五种:同步阻塞IO模型、同步非阻塞IO模型、多路复用IO模型、信号驱动IO模型、异步IO模型

处理流程

为了更好的理解IO模型,先举个体现总体流程的下载文件案例:

下载文件案例中,客户端先向服务端发送请求,而服务器收到请求后,读取磁盘中的文件数据,发送到网卡上再响应给客户端

在这个过程中,以服务端的视角可以看成先从磁盘中读取数据,再往网卡上写数据

由于磁盘、网卡属于外部设备,由于外部设备速度慢,不会使用CPU进行拷贝数据,而是通过DMA进行数据拷贝(这样就不需要占用CPU的资源)

操作系统分为用户态和内核态,其中应用程序处于用户态

由于操作系统中的重要资源不能被用户态的应用程序直接访问,需要先切换到内核态再进行访问

image.png

用传统的阻塞IO(BIO)举例,当服务端接收到请求后:

  1. 读取磁盘要先切换为内核态,然后由DMA进行拷贝(将磁盘上的数据拷贝到内核缓冲区),此时用户线程一直阻塞
  2. DMA拷贝完成后,由CPU将数据从内核缓冲区拷贝到用户态内存上的用户缓冲区,此时用户线程才被唤醒
  3. 状态切换为用户态后,用户线程从用户态中内存的缓冲区将数据拷贝到JVM的堆内存缓冲区中
  4. Java程序处理数据(已经读完),开始写数据,写数据的流程与读数据流程类似,只不过是相反的

图中的直接内存为用户态的内存,而堆内存为JVM的(属于JVM管理)

在这个过程中:用户线程一直阻塞,读数据与写数据存在大量重复拷贝、状态切换,都会导致性能被大大浪费

通过IO模型、零拷贝等优化方式能够优化这个过程,提升响应速度

IO模型

同步阻塞

在同步阻塞IO模型下,当用户线程请求读取外部设备的数据时,会一直阻塞直到数据拷贝完成,再去使用数据

image.png

具体流程如下:

  1. 用户线程发起系统调用请求读取外部设备数据(进入阻塞)
  2. 用户态切换为内核态准备数据:使用DMA将外部设备数据拷贝到内核缓冲区
  3. 内核进行数据拷贝:将内核缓冲区的数据拷贝到用户缓冲区
  4. 内核态切换为用户态,使用数据

阻塞指的是:在用户线程发起系统调用时,并没有立即返回,而是等到数据拷贝完成被唤醒再使用

同步指的是:在数据拷贝阶段,用户线程是阻塞等待数据完成拷贝的(也可以理解为同步是用户线程主动请求内核进行数据拷贝的)

在同步阻塞IO模型下,由于准备、拷贝数据阶段都会阻塞等待,因此性能并不理想

如果高并发的请求打进来,让等比例的线程数量来等待,资源开销是非常大的,因此现代中间件一般不会采取这种模型

同步非阻塞

同步非阻塞IO模型会频繁发起系统调用来判断数据是否已就绪,如果已就绪则同步阻塞进行拷贝

在这个过程中,准备数据阶段是通过轮询非阻塞的方式实现的,当响应数据就绪时,再发起系统调用同步阻塞进行数据拷贝

image.png

同步非阻塞IO模型虽然在数据准备的阶段不需要阻塞,但会通过轮询的方式一直进行系统调用,产生一定的开销

要求网络通信高效的中间件也不会使用这种模型

多路复用

在多路复用模型中,内核线程能够同时监听多个网络请求的通道

使用前,会将数据通道注册到select上,当使用select时会进行阻塞,直到select监听到数据通道上数据已就绪,此时再请求读取数据,使用read系统调用,同步阻塞直到拷贝完数据

image.png

在多路复用模型中实现还分为三种方式:select、poll、epoll

select就是上述举例流程,缺点是最多监听1024个数据通道,并且阻塞到数据就绪时需要遍历处理O(n)

poll在select基础上,动态调整只要内存够理论上无监听通道数量的上限,但数据就绪时还需要遍历处理

epoll使用事件回调的方式,当数据就绪时不需要再轮询,并且内核维护不再需要将数据拷贝到用户态

在多路复用模型中,由于一个内核线程可以监听多个数据通道,这样即使维护大量的网络数据通道,开销也不会太大

而且有epoll事件回调、不用拷贝的优化性能非常好,大部分的中间件都会选择多路复用模型实现网络通信

信号驱动

在信号驱动模型中,会先发送信号的系统调用(立即返回 非阻塞),当数据准备好后通知,再发送读数据的系统调用(阻塞),让内核完成拷贝数据

image.png

信号驱动避免准备数据时的阻塞,并且不需要轮询发起系统调用,但在数据拷贝时依旧需要同步阻塞

异步

在异步IO模型中,发起请求的系统调用时会携带回调函数,发起系统调用后立即返回(非阻塞)

当数据就绪后,不需要用户线程同步触发,而是由内核主动将数据拷贝到用户缓冲区

image.png

在异步IO(AIO)中完全没有阻塞也不再需要同步

在要求高效的高并发网络通信中,一般使用多路复用模型NIO和异步IO(AIO)

JDK中的NIO指的就是多路复用模型,而NIO2指的就是AIO,后续讲解中间件如何高效处理网络通信时都会出现它们的身影~

零拷贝

聊完IO模型后,我们能够知道使用NIO、AIO能够加快处理流程的速度

处理流程中还存在大量的CPU拷贝,在Linux内核逐步升级后,网卡支持的情况下还可以实现零拷贝

零拷贝指的是不再需要使用CPU进行数据拷贝,而是直接通过DMA进行拷贝

image.png

为什么无法从内核直接拷贝到JVM堆内存?

在传统的流程处理中,需要先从内核拷贝到直接内存,再从直接内存拷贝到JVM堆内存

既然直接内存和JVM堆内存都处于用户态的内存,为什么不从内核直接拷贝到JVM堆内存呢?

这是由于JVM堆内存会发生GC,可能改变位置,内核拷贝到堆内存时无法保证不会GC

而本地内存拷贝到JVM堆内存,HotSpot虚拟机保证不在安全点上,因此不会GC

JVM安全点相关知识感兴趣的同学可以查看这篇文章

总结

IO模型的提出是为了解决CPU在内存的速度与外部设备加载到内存速度的差异

在操作系统中为了安全使用系统资源,IO时会涉及到用户态、内核态的切换

IO阶段通常分为准备数据和拷贝数据,准备数据主要由DMA将外部设备数据拷贝到内核缓冲区,拷贝数据是将内核缓冲区拷贝到用户缓冲区

同步阻塞IO模型(BIO)发起系统调用后会阻塞到数据拷贝完成,不适合处理高并发网络通信的场景

同步非阻塞IO模型使用轮询的方式判断数据是否就绪,就绪再同步阻塞等待数据拷贝

信号驱动模型中数据就绪后通过信号通知应用发起系统调用读取数据,避免同步非阻塞下轮询的开销

多路复用IO模型使用select时,监听多个通道,select阻塞直到监听到通道上数据就绪,再通知应用进行读取发起同步阻塞直到数据拷贝结束

使用select,当多个通道数据同时就绪时,只能轮询处理,并且只能监听1024个通道;使用poll进行优化能够监听无上限通道数量

使用epoll 事件回调的方式避免轮询处理,并且内核维护不需要再进行数据拷贝

异步IO模型使用回调的方式避免数据就绪时同步阻塞进行数据拷贝,Linux下也是使用epoll模拟实现

当网卡支持时使用sendfile零拷贝可以避免大量CPU数据拷贝

最后(不要白嫖,一键三连求求拉~)

本篇文章被收入专栏 后端的网络基石,感兴趣的同学可以持续关注喔

本篇文章笔记以及案例被收入 Gitee-CaiCaiJavaGithub-CaiCaiJava,除此之外还有更多Java进阶相关知识,感兴趣的同学可以starred持续关注喔~

有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~

关注菜菜,分享更多技术干货,公众号:菜菜的后端私房菜

相关文章
|
1月前
|
监控 Linux 测试技术
C++零拷贝网络编程实战:从理论到生产环境的性能优化之路
🌟 蒋星熠Jaxonic,技术宇宙中的星际旅人。深耕C++与零拷贝网络编程,从sendfile到DPDK,实战优化服务器性能,毫秒级响应、CPU降60%。分享架构思维,共探代码星辰大海!
|
4月前
|
C++
基于Reactor模型的高性能网络库之地址篇
这段代码定义了一个 InetAddress 类,是 C++ 网络编程中用于封装 IPv4 地址和端口的常见做法。该类的主要作用是方便地表示和操作一个网络地址(IP + 端口)
224 58
|
4月前
|
网络协议 算法 Java
基于Reactor模型的高性能网络库之Tcpserver组件-上层调度器
TcpServer 是一个用于管理 TCP 连接的类,包含成员变量如事件循环(EventLoop)、连接池(ConnectionMap)和回调函数等。其主要功能包括监听新连接、设置线程池、启动服务器及处理连接事件。通过 Acceptor 接收新连接,并使用轮询算法将连接分配给子事件循环(subloop)进行读写操作。调用链从 start() 开始,经由线程池启动和 Acceptor 监听,最终由 TcpConnection 管理具体连接的事件处理。
111 2
|
4月前
|
Java Linux API
IO模型
BIO、NIO、AIO是Java中处理网络I/O的三种模型。BIO为阻塞式,每个连接需单独线程,高并发下性能受限;NIO通过非阻塞与多路复用提升并发能力,少量线程可处理大量请求;AIO进一步实现异步非阻塞,数据复制时线程可释放,由回调机制处理后续操作。三者适用于不同场景,BIO易用但低效,NIO高效但复杂,AIO理论性能更优但目前在Linux上仍依赖多路复用实现。Java 21引入虚拟线程后,BIO也可兼具高性能与易编写特性。
122 2
|
4月前
基于Reactor模型的高性能网络库之Tcpconnection组件
TcpConnection 由 subLoop 管理 connfd,负责处理具体连接。它封装了连接套接字,通过 Channel 监听可读、可写、关闭、错误等
133 1
|
4月前
|
JSON 监控 网络协议
干货分享“对接的 API 总是不稳定,网络分层模型” 看电商 API 故障的本质
本文从 OSI 七层网络模型出发,深入剖析电商 API 不稳定的根本原因,涵盖物理层到应用层的典型故障与解决方案,结合阿里、京东等大厂架构,详解如何构建高稳定性的电商 API 通信体系。
|
1月前
|
机器学习/深度学习 数据采集 人工智能
深度学习实战指南:从神经网络基础到模型优化的完整攻略
🌟 蒋星熠Jaxonic,AI探索者。深耕深度学习,从神经网络到Transformer,用代码践行智能革命。分享实战经验,助你构建CV、NLP模型,共赴二进制星辰大海。
|
6月前
|
域名解析 网络协议 安全
计算机网络TCP/IP四层模型
本文介绍了TCP/IP模型的四层结构及其与OSI模型的对比。网络接口层负责物理网络接口,处理MAC地址和帧传输;网络层管理IP地址和路由选择,确保数据包准确送达;传输层提供端到端通信,支持可靠(TCP)或不可靠(UDP)传输;应用层直接面向用户,提供如HTTP、FTP等服务。此外,还详细描述了数据封装与解封装过程,以及两模型在层次划分上的差异。
994 13
|
2月前
|
机器学习/深度学习 传感器 算法
【无人车路径跟踪】基于神经网络的数据驱动迭代学习控制(ILC)算法,用于具有未知模型和重复任务的非线性单输入单输出(SISO)离散时间系统的无人车的路径跟踪(Matlab代码实现)
【无人车路径跟踪】基于神经网络的数据驱动迭代学习控制(ILC)算法,用于具有未知模型和重复任务的非线性单输入单输出(SISO)离散时间系统的无人车的路径跟踪(Matlab代码实现)
140 2
|
6月前
|
网络协议 中间件 网络安全
计算机网络OSI七层模型
OSI模型分为七层,各层功能明确:物理层传输比特流,数据链路层负责帧传输,网络层处理数据包路由,传输层确保端到端可靠传输,会话层管理会话,表示层负责数据格式转换与加密,应用层提供网络服务。数据在传输中经过封装与解封装过程。OSI模型优点包括标准化、模块化和互操作性,但也存在复杂性高、效率较低及实用性不足的问题,在实际中TCP/IP模型更常用。
743 10