网络编程中的直接内存与零拷贝

本篇文章会介绍 JDK 与 Linux 网络编程中的直接内存与零拷贝的相关知识,最后还会介绍一下 Linux 系统与 JDK 对网络通信的实现。

1、直接内存

所有的网络通信和应用程序中(任何语言),每个 TCP Socket 的内核中都有一个发送缓冲区(SO_SNDBUF)和一个接收缓冲区(SO_RECVBUF):

请添加图片描述

应用程序调用 write() 会使内核复制应用程序缓冲区中所有的数据到 Socket 的发送缓冲区,如果后者放不下并且该 Socket 是阻塞式的,应用程序会被投入睡眠。write() 直到应用程序缓冲区的所有数据都复制到 Socket 的发送缓冲区后才会返回,此时可以继续向应用程序缓冲区写入数据,但不表示对端的 TCP 或应用程序已经接收到数据:

请添加图片描述

Java 也遵从这种规则。只不过因为堆、GC 等特性影响,会有一些特殊操作,即使用直接内存(或称堆外内存),下面来阐述原因:

  • 前面说过,要发送的数据会从应用程序的缓冲区被内核拷贝到 Socket 内核的发送缓冲区中。这中间必定有调用 Native 方法将 Java 对象地址通过 JNI 传递给底层 C 库的过程
  • 如果该 Java 对象存在堆中,受 GC 影响该对象可能会在堆中移动,就有可能出现该对象地址在传递给底层前后不同的情况,原地址失效底层就拿不到原本的对象。因此会要求调用 Native 方法之前一定要将数据存在堆外内存,JDK 对此的解决方案是将堆中的数据拷贝到堆外的 DirectBuffer 中
  • 也可以直接使用 DirectBuffer 而不再通过堆,这样可以省去把数据由堆拷贝到 DirectBuffer 的一次拷贝,使用直接内存当然就会快一点
  • 直接内存不受新生代的 Minor GC 影响,只有执行老年代的 Full GC 时才会顺便回收直接内存,整理内存的压力也比将数据放到堆上小

使用堆外内存的好处是减少了 GC(会暂停其他工作)工作、加快了复制速度(相比于堆少了一次数据拷贝);缺点是如果堆外发生内存泄漏难以排查、不适合存很复杂的对象(适合简单对象或扁平化对象)。

请添加图片描述

2、零拷贝

指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。

零拷贝并不是说不需要拷贝,只是说减少冗余的、不必要的(尤其是需要 CPU 干预的)拷贝:

  • 零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之间不必要的中间拷贝次数,从而有效地提高数据传输效率
  • 零拷贝技术减少了用户进程地址空间和内核地址空间之间因为上下文切换而带来的开销

2.1 Linux 的 IO 机制与 DMA

早期用户进程需要读取磁盘数据时都需要 CPU 中断并参与,这样 CPU 的效率低,因为每次 IO 请求都要中断 CPU 带来 CPU 的上下文切换,为了解决这个问题出现了 DMA(Direct Memory Access)。

DMA 不需要依赖 CPU 大量的中断负载就可以与不同速度的硬件装置进行沟通。DMA 控制器接管了数据读写请求,减少了 CPU 的负担,使得 CPU 可以高效工作。现代硬盘基本都支持 DMA,实际的 IO 读取涉及两个过程(都是阻塞的):

  1. DMA 等待数据准备好,把磁盘数据读取到操作系统内核缓冲区
  2. 用户进程,将内核缓冲区的数据 copy 到用户空间

DMA 是物理硬件,也算是一种芯片,磁盘、网卡、键盘等都有自己的 DMA。早期 CPU 会参与 IO 工作,读取磁盘上的数据拷贝到内存当中,由于 IO 读写速度相比于 CPU 的处理速度是很慢的,所以这就相当于浪费了 CPU 的宝贵时间,于是产生了 DMA 设备,在有 IO 需求时,CPU 给 DMA发指令让其读取磁盘数据,DMA 读取后会将数据拷贝到内存中,再通知 CPU 数据拷贝完成,然后 CPU 再用内存中的数据做接下来的操作。这就将 CPU 从低速的 IO 读取工作中解放出来,专心做高速计算。

2.2 传统数据传送机制

以读取文件再用 Socket 发送出去这个过程为例,伪代码如下:

buffer = File.read()
Socket.send(buffer)

这个过程的示意图如下:

请添加图片描述

数据要经过四次拷贝:

  1. 将磁盘中的文件拷贝到操作系统内核缓冲区
  2. 将内核缓冲区数据拷贝到应用程序缓冲区
  3. 将应用程序缓冲区中的数据拷贝到位于操作系统内核缓冲区中的 Socket 网络发送缓冲区
  4. 将 Socket 缓冲区中的数据拷贝到网卡,由网卡进行网络传输

其中 2、3 两次(即图中红线的两次 CPU 拷贝)是“不必要的拷贝”,对于发送网络数据而言属于额外开销,可以优化掉。

此外,read 和 send 都属于系统调用,每次调用都牵涉两次上下文切换,总共就是四次上下文切换:

请添加图片描述

2.3 Linux 常见的零拷贝

零拷贝的目的就是减少不必要的拷贝,需要 OS 支持(需要 kernel 暴露 api)。

mmap 内存映射

将硬盘与应用程序缓冲区进行映射(建立一一对应关系),由于 mmap() 将文件直接映射到用户空间,读取文件时就可以根据该映射关系将文件从硬盘拷贝到用户空间:

请添加图片描述

这样仍有 3 次拷贝,4 次上下文切换。

sendfile

请添加图片描述

sendfile 需要 3 次拷贝,2 次上下文切换:

  • 3 次拷贝如上图所示,当然如果硬件支持的话,红线的 CPU 拷贝是可以省略的。具体做法是文件读取缓冲区将文件的起始位置和长度的描述符传入 Socket 缓冲区,然后 DMA 会根据这个数据从文件读取缓冲区中直接将文件读取到网络设备缓冲区,这样就只需要 2 次拷贝了
  • 用户调用 sendfile 这一个系统调用,仅需两次上下文切换

splice

Linux 在 2.6.17 开始支持的系统调用,使用管道直接将内核缓冲区的数据转换为其他数据 buffer。在 Socket 网络通信的情况下,就是文件读取缓冲区与 SO_SNDBUF 建立 pipe 管道(实际上是管道两侧的缓冲区共用一块物理内存)。这样在无需硬件支持的情况下就省去了 CPU 拷贝:

请添加图片描述

splice 也是需要 2 次拷贝,2 次上下文切换。

总结

零拷贝说法的来源最早出现于 sendfile 系统调用,这是真正操作系统意义上的零拷贝(也称狭义零拷贝)。

但是由于由 OS 内核提供的操作系统意义上的零拷贝发展到现在种类并不是很多,因此随着发展,零拷贝的概念延伸到了,减少不必要的数据拷贝都算作零拷贝的范畴。

3、Linux 和 JDK 对网络通信的实现

3.1 Linux 网络 IO 模型

同步与异步,阻塞与非阻塞

同步与异步关注的是调用方是否主动获取结果:

  • 同步:调用方主动等待结果返回
  • 异步:调用方不用主动等待结果返回,而是通过状态通知、回调函数等手段获取结果

阻塞与非阻塞关注的是调用方在等待结果返回之前的状态:

  • 阻塞:结果返回前,当前线程被挂起不做任何事
  • 非阻塞:结果返回前,线程可以做其他事情,不会被挂起

二者有四种组合:

  1. 同步阻塞:编程中最常见的模型,等待结果并且等待期间不做任何事,效率很低
  2. 同步非阻塞:可以抽象为轮询模式,等待结果期间会做其他事情,但是会时不时地询问是否已经返回结果
  3. 异步阻塞:用的很少,有点像在线程池中 submit 后马上 Future.get(),此时线程其实还是挂起的
  4. 异步非阻塞:常用模型是回调函数

Linux 下的五种 IO 模型

五种 IO 模型,前四种是同步的,最后一种是异步的:

  1. 阻塞 IO:调用 IO 函数,会经过系统调用进入内核。应用程序会被阻塞,直到数据被准备好,从内核空间拷贝到用户空间后,IO 函数返回,阻塞才被解除。BIO 中的 bind()、connect()、accept() 都是阻塞方法
    请添加图片描述
  2. 非阻塞 IO:IO 操作无法完成时,不将进程睡眠,而是返回一个错误。这样应用就需要不断测试数据是否已经准备好,如果没有就继续测试直到数据准备好为止。这种不断测试会大量占用 CPU 时间,因此该模型绝对不被推荐
    请添加图片描述
  3. IO 复用(select、poll、epoll):本质上也是阻塞的,只不过将阻塞拆开为 select(或 epoll)和 recvfrom 两个系统调用,前者在有读写事件到来时返回,后者在数据从内核拷贝到用户空间后返回。也就是对一个 IO 端口进行两次系统调用,返回两次结果,这比阻塞 IO 并没有什么优势,甚至相同条件下处理单个连接的效率还要比 BIO 低,但是胜在能同时对多个 IO 端口进行监听
    请添加图片描述
  4. 信号驱动 IO:应用进程向内核注册一个信号处理函数然后继续执行其他内容不会阻塞,当数据到来时,内核发出信号,通过信号处理程序告诉应用进程数据来了,这时应用程序才调用 recvfrom 进入阻塞式获取数据的过程。整个过程有两次调用和两次返回
    请添加图片描述
  5. 异步 IO:当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者的输入输出操作(Linux 下的 AIO 是假的异步,是用 IO 多路复用实现的)
    请添加图片描述

3.2 JDK 对网络通信的实现

JDK 实际上就是对 Linux 的 IO 通信模型进行了一个包装,因此我们先了解 Linux 的通信实现。

Linux 下的阻塞网络编程

Linux 下与 JDK 实现网络通信的一个最大不同是,在服务端,Linux 用的是 socket 而 JDK 用的 ServerSocket,实际上就是 JDK 在 socket 基础上做了一层封装。此外,Linux 下需要通过 listen() 侦听端口,这个大概也被 ServerSocket 封装了。

从 Linux 代码结构看网络通信

分层:应用 API 层、协议层、接口层,应用发送数据是由上至下,接收数据是由下至上,并且接收时还涉及到由网络设备产生的硬中断。

中断、上半部、下半部

内核和设备驱动是通过中断的方式来处理的。所谓中断,可以理解为当设备上有数据到达的时候,会给 CPU 的相关引脚上触发一个电压变化,以通知 CPU 来处理数据。

网卡把数据写入内存后会向 CPU 发出一个中断信号,由操作系统执行网卡中断程序去处理数据。由于网络操作复杂且耗时,如果在中断函数中完成所有处理,会使得中断处理函数(优先级过高)过度占据 CPU,使得 CPU 无法响应其他设备(如鼠标键盘),因此 Linux 将中断处理函数分为上半部和下半部。

上半部只进行最简单的工作,快速处理然后释放 CPU,这样 CPU 就可以让其他中断进来。下半部则慢慢从容的处理绝大部分工作。自 2.4 以后内核采用下半部是软中断,即给内存中的一个变量的二进制赋值以通知中断处理程序;而硬中断则是通过给 CPU 物理引脚施加电压变化。

JDK 的 BIO 实现分析

Socket 和 ServerSocket 内部的 SocketImpl 才是真正实现网络通信的组件(使用了门面模式),这与 Linux 下 CS 两端都使用 socket 是吻合的。

Linux 下的 IO 复用编程

select,poll,epoll 都是 IO 多路复用的机制。所谓 IO 多路复用就是指一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但 select,poll,epoll 本质上都是同步 I/O,他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步 I/O 则无需自己负责进行读写,异步 I/O 的实现会负责把数据从内核拷贝到用户空间。

select 提供了一个函数:

// readfds 读事件 fd 集合,writefds 写事件 fd 集合,exceptfds 异常事件 fd 集合
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

所有的操作系统都支持 select 机制,Linux 下能监控的最大文件描述符数量为 1024,超过该数量性能会急剧下滑。

poll 也提供了一个函数,将 select() 参数中的三个描述符合为一个:

int poll (struct pollfd *fds, unsigned int nfds, int timeout);

fds 也是不能超过 1024 个,否则性能会急剧下降(因为是轮询 Socket 通道获取事件,数量多了自然性能就下降了)。

epoll 有三个函数,也就是三个系统调用:

// 创建 epoll 的文件描述符,类似于 JDK NIO 的 Selector.open()
int epoll_create(int size);
// 注册、增加、删除、修改关注的事件,类似于 JDK NIO 的 ServerChannel.register()
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 等待,看是否有事件发生,类似于 JDK NIO 的 Selector.select()
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

实际上 JDK 的 NIO 就是对 Linux epoll 的包装。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/bicheng/81479.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

TransmittableThreadLocal使用场景

🚀 为什么要用 TransmittableThreadLocal?一文读懂线程上下文传递问题 在 Java Web 开发中,我们经常用 ThreadLocal 来保存每个请求的用户信息,例如 userId。但当我们使用线程池或异步方法(如 Async)时&am…

Milvus(24):全文搜索、文本匹配

1 全文搜索 全文搜索是一种在文本数据集中检索包含特定术语或短语的文档,然后根据相关性对结果进行排序的功能。该功能克服了语义搜索的局限性(语义搜索可能会忽略精确的术语),确保您获得最准确且与上下文最相关的结果。此外&…

2000 元以下罕见的真三色光源投影仪:雷克赛恩Cyber Pro1重新定义入门级投影体验

当性价比遇上技术瓶颈 在 2000元以下的1080P投影仪,单LCD 技术长期主导。而三色光源的DLP和3LCD真1080P都在4000元以上。 单LCD投影为纯白光光源,依赖CF滤光膜导致光效低下,普遍存在" 色彩失真 " 等问题。数据显示,该价…

Maven 下载安装与配置教程

## 1. Maven 简介 Maven 是一个项目管理和构建自动化工具,主要用于 Java 项目。Maven 可以帮助开发者管理项目的构建、报告和文档,简化项目依赖管理。 ## 2. 下载 Maven 1. 访问 Maven 官方网站 [https://maven.apache.org/download.cgi](https://maven.…

C# 深入理解类(从类的外部访问静态成员)

从类的外部访问静态成员 在前一章中,我们看到使用点运算符可以从类的外部访问public实例成员。点运算符由实 例名、点和成员名组成。 就像实例成员,静态成员也可以使用点运算符从类的外部访问。但因为没有实例,所以最常 用的访问静态成员的方…

Java在微服务架构中的最佳实践:从设计到部署

在2025年的云计算和分布式系统时代,微服务架构已成为构建高可扩展、高可用系统的标准方法,广泛应用于电商、金融和物联网等领域。Java凭借其成熟的生态系统、强大的并发支持和跨平台能力,是微服务开发的首选语言。例如,我们的订单…

文件读取漏洞路径与防御总结

文件读取漏洞路径与防御总结 文件读取漏洞允许攻击者通过路径遍历等手段访问未授权的文件。以下是Linux和Windows系统中常见敏感路径的归纳及防御建议: Linux 系统常见敏感路径 系统关键文件: /etc/passwd:用户账户信息(可被用来…

react-router基本写法

1. 创建项目并安装所有依赖 npx create-react-app react-router-pro npm i 2. 安装所有的 react router 包 npm i react-router-dom 3. 启动项目 npm run start router/index.js // 创建路由实例 绑定path elementimport Layout from "/pages/Layout"; import…

uni-app 开发HarmonyOS的鸿蒙影视项目分享:从实战案例到开源后台

最近,HBuilderX 新版本发布,带来了令人兴奋的消息——uni-app 现在支持 Harmony Next 平台的 App 开发。这对于开发者来说无疑是一个巨大的福音,意味着使用熟悉的 Vue 3 语法和开发框架,就可以为鸿蒙生态贡献自己的力量。 前言 作…

纯css实现蜂窝效果

<!DOCTYPE html><html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>蜂窝效果</title><style>body {margin: 0…

JAVA EE_HTTP

为什么意气风发的少年&#xff0c;总是听不进去别人的劝解。 ​​​​​​​ ​​​​​​​ ----------陳長生. ❀主页&#xff1a;陳長生.-CSDN博客❀ &#x1f4d5;上一篇&#xff1a;JAVA EE_网络原理_数据链路层-CSDN博客 1.HTTP 1.1.HTTP是什么 H…

存储扇区分配表:NAND Flash与SD NAND(贴片式SD卡)的架构差异

NAND Flash 和 SD 卡&#xff08;SD NAND&#xff09;的存储扇区分配表在原理上有相似之处&#xff0c;但由于二者的结构和应用场景不同&#xff0c;也存在一些差异。 相同点&#xff1a; 基本功能&#xff1a;NAND Flash 和 SD 卡&#xff08;SD NAND&#xff09;的存储扇区分…

界面控件DevExpress WinForms中文教程:Banded Grid View - API

DevExpress WinForms拥有180组件和UI库&#xff0c;能为Windows Forms平台创建具有影响力的业务解决方案。DevExpress WinForms能完美构建流畅、美观且易于使用的应用程序&#xff0c;无论是Office风格的界面&#xff0c;还是分析处理大批量的业务数据&#xff0c;它都能轻松胜…

4G物联网模块实现废气处理全流程数据可视化监控配置

一、项目背景 随着工业化进程的加速&#xff0c;工业废气的排放对环境造成了严重影响&#xff0c;废气处理厂应运而生。然而&#xff0c;废气处理厂中的设备众多且分散&#xff0c;传统的人工巡检和数据记录方式效率低下&#xff0c;难以及时发现问题。为了实现对废气处理设备…

Kubernetes控制平面组件:Kubelet详解(四):gRPC 与 CRI gRPC实现

云原生学习路线导航页&#xff08;持续更新中&#xff09; kubernetes学习系列快捷链接 Kubernetes架构原则和对象设计&#xff08;一&#xff09;Kubernetes架构原则和对象设计&#xff08;二&#xff09;Kubernetes架构原则和对象设计&#xff08;三&#xff09;Kubernetes控…

【数据结构】线性表--队列

【数据结构】线性表--队列 一.什么是队列二.队列的实现1.队列结构定义&#xff1a;2.队列初始化函数&#xff1a;3.队列销毁函数&#xff1a;4.入队列函数&#xff08;尾插&#xff09;&#xff1a;5.出队列函数&#xff08;头删&#xff09;&#xff1a;6.取队头元素&#xff…

C语言—再学习(结构体)

一、建立结构体 用户自己建立由不同类型数据组成的组合型的数据结构&#xff0c;它称为结构体。 struct Student { int num; //学号char name[20]; //名字为字符串char sex; //性别int age; //年纪float score; //分数char addr[30]; 地址为字符…

【前端基础】10、CSS的伪元素(::first-line、::first-letter、::before、::after)【注:极简描述】

一、伪元素的作用 选取某个特定的元素。 二、::first-line、::first-letter ::first-line&#xff1a;针对首行文本设置属性 ::first-letter&#xff1a;针对首字母设置属性 三、::before、::after 在一个元素之前&#xff08;::before&#xff09;或者之后&#xff08;…

系统漏洞扫描服务:维护网络安全的关键与服务原理?

系统漏洞扫描服务是维护网络安全的关键措施&#xff0c;能够迅速发现系统中的潜在风险&#xff0c;有效预防可能的风险和损失。面对网络攻击手段的日益复杂化&#xff0c;这一服务的重要性日益显著。 服务原理 系统漏洞扫描服务犹如一名恪尽职守的安全守护者。它运用各类扫描…

从 Excel 到 Data.olllo:数据分析师的提效之路

背景&#xff1a;Excel 的能力边界 对许多数据分析师而言&#xff0c;Excel 是入门数据处理的第一工具。然而&#xff0c;随着业务数据量的增长&#xff0c;Excel 的一些固有限制逐渐显现&#xff1a; 操作容易出错&#xff0c;难以审计&#xff1b; 打开或操作百万行数据时&…