深入理解网络 I/O:mmap、sendfile、Direct I/O

在这里插入图片描述

🔭 嗨,您好 👋 我是 vnjohn,在互联网企业担任 Java 开发,CSDN 优质创作者
📖 推荐专栏:Spring、MySQL、Nacos、Java,后续其他专栏会持续优化更新迭代
🌲文章所在专栏:网络 I/O
🤔 我当前正在学习微服务领域、云原生领域、消息中间件等架构、原理知识
💬 向我询问任何您想要的东西,ID:vnjohn
🔥觉得博主文章写的还 OK,能够帮助到您的,感谢三连支持博客🙏
😄 代词: vnjohn
⚡ 有趣的事实:音乐、跑步、电影、游戏

目录

  • 前言
  • mmap
    • 实现机制
    • 图解分析
    • 缺点
  • sendfile
    • 实现机制
    • 图解分析
    • 使用
    • 缺点
  • Direct I/O
    • 实现机制
    • 缺点
  • 总结

前言

在上一篇文章介绍以下三个类的特征及使用:

深入理解网络 I/O:FileOutputStream、BufferFileOutputStream、ByteBuffer

在 ByteBuffer 中围绕三个子类进行了展开:HeapByteBuffer、MappedByteBuffer、DirectByteBuffer

HeapByteBuffer 使用的是 JVM 堆内的内存进行文件 I/O 操作
DirectByteBuffer 使用的 Java 进程内的堆内存进行文件 I/O 操作
MappedByteBuffer 使用的是用户空间与内核空间之间映射出一块内存区域进行文件 I/O 操作

同时,MappedByteBuffer 也是作为了 DirectByteBuffer 的父类,这两者并没有直接的继承关系,都只是作为 ByteBuffer 类的不同实现

MappedByteBuffer 在操作系统内核中使用的 mmap 函数进行了用户空间与内核空间之间的虚拟内存区域映射的,采用此方式可以减少用户态和内核态之间的拷贝次数以及上下文切换次数

本文还会介绍 sendfile 函数、Direct I/O 的作用以及应用场景的区别.

mmap

void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);

mmap(Memory-mapped Files)是一种操作系统提供的机制,允许将文件的一部分或全部内容直接映射到进程的虚拟地址空间中,这种技术使得文件内容可以被视为内存的一部分,从而实现了文件与内存之间的无缝衔接

将内核态与用户态内存映射在一起,避免来回的拷贝,采用指针的方式读写操作一段内存,即完成了对文件的操作而不必再调用 read、write 等系统调用函数

实现机制

mmap 在 Java 中基于 MappedByteBuffer 类实现,它是 Java NIO 中用于内存映射文件(Memory-mapped Files)的一种缓冲区,它的实现机制涉及内存映射文件的操作和底层操作系统的支持

1、MappedByteBuffer 使用操作系统提供的内存映射文件机制,将文件的部分或全部内容直接映射到进程的虚拟内存空间

2、通过 FileChannel#map 方法可以创建一个 MappedByteBuffer 对象,操作系统会为文件的指定区域分配虚拟内存地址,文件与虚拟内存地址建立映射关系

3、一旦文件映射到内存,通过 MappedByteBuffer 提供的方法可以直接访问文件内容,读取或写入 MappedByteBuffer 中的数据,实际上是在修改虚拟内存中的数据,而不是直接对文件进行 I/O 操作

4、MappedByteBuffer 的修改可以自动同步到底层文件系统,或者手动调用 force 方法强制将修改的内容刷写到磁盘中的文件.

图解分析

在这里插入图片描述

如上图,使用了 mmap 基于用户态、内核态共享一块虚拟内存区域的情况下,用户态、内核态来回切换的方式就减少了,比如:客户端要读取服务端的数据时,可以直接读取内存区域的数据,无须再切换为用户态进行数据拷贝,这就是避免了切换的次数也就是数据拷贝的次数,意义上的零拷贝

虽然 mmap 为应用程序与操作系统减少了负担,但也会带来一些问题,因为这块虚拟内存区域是基于操作系统的页缓存 page cache 机制实现的,换言之,基于此,它会有丢失数据的风险

关于 page cache 介绍可以阅读博主的另外一篇文章:

深入了解 Linux PageCache 页缓存:优化文件系统的性能、效率

缺点

mmap 带有不好的地方有几点,如下:

  1. mmap 在使用时必须指定好内存映射的大小,它不适合于变长的文件,若映射的文件过大,会消耗大量的内存,内存消耗的增加可能限制了程序的并发性,特别是当多个进程都需要映射大型文件时
  2. 对映射区域的写入操作会异步将修改的内容刷写到磁盘,若系统崩溃或发生断电宕机的情况下,部分尚未同步到磁盘的数据会丢失导致数据不一致问题
  3. 不适合随机访问大文件,因为它是虚拟内存映射的,并不是物理上的,还需要经过大量的分段分页寻址的过程,加载大文件时就需要较长的时间
  4. 不适用所有的场景,对于一次性操作或少量数据访问的场景,根本没必要使用到 mmap

sendfile

它应用在基于网络 I/O 文件描述符之间传输数据.

ssize_t sendfile(int out_fd, int in_fd, 
off_t *offset, size_t count);

sendfile 在一个文件描述符与另外一个文件描述符之间复制数据,这种复制的操作是之间内核态完成的,无须进行用户态与内核态之间切换
sendfile 对比于 read、write 组合更有效,后者需要在用户空间与内核之间进行切换后传输数据

in_fd:为读打开的文件描述符
out_fd:为写打开的文件描述符

sendfile 是一个系统调用,它基于操作系统内核提供的特性实现数据传输,具体的说:sendfile 利用操作系统的零拷贝机制进行文件数据传输

实现机制

sendfile 实现的主要机制包括:DMA、内核缓冲区、传输描述符、零拷贝机制

1、sendfile 利用 DMA 中断技术,使得传输数据可以直接在设备(磁盘)与内存之间进行,无需 CPU 的参与,它允许设备直接访问系统内存,从而避免了数据从外设到 CPU 再到内核的复制过程

2、sendfile 利用操作系统内核中的内核缓冲区,在内核空间中暂存待传输的数据,数据从文件描述符对应的内核缓冲区出直接传输到另一个文件描述符的内核缓冲区,无须用户空间的参与

3、sendfile 通过传输文件描述符(Socket)实现数据的直接传输,内核负责将文件描述符所指向的数据通过网络传输到另一端,而无需将数据从内核空间复制到用户空间再进行传输

4、sendfile 利用零拷贝的特性,尽量减少了数据的复制,避免了数据在内核空间和用户空间之间的多次复制;数据从一个内核缓冲区到另外一个内核缓冲区,减少了不必要的数据拷贝过程

若只是传输数据,并不对数据作任何处理,譬如服务器存储的静态文件,入:html、css、js 发送客户端用于浏览器渲染,在这种场景下,若依然进行多次的数据拷贝和上下文切换,简直是丧心病狂!这种情况下就可以使用 sendfile 的方式,只做文件传输,而不经过用户态进行干预

图解分析

在这里插入图片描述

如上图,通过用户态调用 sendfile,让内核将数据进行文件描述符之间的拷贝,而无须用户态的参与,直接能让客户端与服务端完成数据的传输

数据拷贝 3 次:设备*(磁盘) —> 内核 —> Socket
上下文切换 2 次:一次 —> 用户态—内核态、一次 —> 内核态—设备

使用

在 Java 应用程序使用 sendfile,涉及到的关键类仍然是上一篇提及到的 FileChannel,它里面提供了两个方法:

1、FileChannel#transferTo:将指定字节数从该 channel 的文件传输到给定可写的 channel 中

public abstract long transferTo(long position, long count, WritableByteChannel target)

2、FileChannel#transferFrom:将指定字节数从可读的 channel 中传输到该 channel 的文件中

public abstract long transferFrom(ReadableByteChannel src,long position, long count)

通过以下源代码来测试模拟 sendfile 写入文件内容和从文件进行内容的读取

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;/*** @author vnjohn* @since 2023/12/21*/
public class SendfileIO {static String SOURSE_PATH = "/opt/io/sendfile/source.txt";static String TARGET_PATH = "/opt/io/sendfile/target.txt";public static void main(String[] args) {switch (args[0]) {case "0":transferTo();break;case "1":transferFrom();break;}}public static void transferTo() {String host = "172.16.249.10";int port = 8090;try (SocketChannel socketChannel = SocketChannel.open();FileInputStream fileInputStream = new FileInputStream(SOURSE_PATH);FileChannel fileChannel = fileInputStream.getChannel()) {socketChannel.connect(new InetSocketAddress(host, port));// 将文件内容直接读取到 SocketChannel(模拟 sendfile)fileChannel.transferTo(0, fileChannel.size(), socketChannel);System.in.read();} catch (IOException e) {e.printStackTrace();}}public static void transferFrom() {try (FileInputStream fis = new FileInputStream(SOURSE_PATH);FileOutputStream fos = new FileOutputStream(TARGET_PATH);FileChannel sourceChannel = fis.getChannel();FileChannel targetChannel = fos.getChannel()) {targetChannel.transferFrom(sourceChannel, 0, sourceChannel.size());System.in.read();} catch (IOException e) {e.printStackTrace();}}
}

在 /opt/io/sendfile 目录下新建一个文件内容为:Hello 的 source.txt 文件、一个文件内容为:vnjohn 的 target.txt 文件

然后通过 nc -l localhost 8090 开启一个服务端

将以上的代码进行编译依次运行运行:

1、strace -ff -o sendfile java SendfileIO 0
2、strace -ff -o sendfile java SendfileIO 1

当执行第一条命令时,服务端窗口输出如下:

在这里插入图片描述

strace 日志输出调用了 sendfile 函数:sendfile(4, 5, [0] => [7], 7)

当执行第二条命令时,文件 target.txt 的内容直接就是 source.txt 的内容了.

缺点

1、仅适用于网络传输,sendfile 主要将文件内容发送到网络套接字中,因此它的应用范围有限,不能用于一般的文件读写操作

2、不支持数据修改,sendfile 通常用于只读操作,无法在传输过程中修改数据

Direct I/O

之前的 mmap 可以让用户态与内核态共用一个内存空间来减少拷贝,还有一种方式就是硬件数据不经过内核态的空间,直接到用户态的内存中,这种方式就是 Direct I/O.

Direct I/O 不会经过内核,而是用户态与设备的直接交互,用户态的写入就是直接写入磁盘,不会再经过操作系统进行刷盘处理

实现机制

Direct I/O 实际上是指使用 readwrite 等系统调用以及相关的文件描述符在用户空间和设备之间直接进行数据传输,绕过了内核的缓冲区 page cache

在 Linux 等系统中,Direct I/O 可以通过系统调用 open 时使用 O_DIRECT 标志来实现,这样可以告诉操作系统绕过缓存,直接将数据传输到磁盘上

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

flags:标志包含了O_SYNC、O_APPEND、O_ASYNC、O_CREAT、O_PATH、 O_DIRECT (Since Linux 2.4.10)

采用此方式可以尽量减少进出该文件到 I/O 缓存效果,一般这种会降低性能,但在特殊情况下很有用,既然绕过了内核的缓冲区,应用程序自身就要维护缓存,文件 I/O 直接进出用户空间缓冲区,O_DIRECT 标志本身致力于同步传输数据

mode 参数标志必须包含以下几种访问模式之一:O_RDONLY、O_WRONLY 或 O_RDWR,这三种方式分别要求打开只读、只写或读/写文件

对于 Direct I/O 的实现机制,主要涉及以下几点:

  1. 文件描述符标志 — O_DIRECT:使用 O_DIRECT 标志可以告知操作系统进行 Direct I/O,绕过内核缓冲区
  2. 用户空间和设备直接传输:在 Direct I/O 中,数据直接在用户空间的应用程序缓冲区和设备或文件之间进行传输,绕过了内核缓冲区的中间步骤
  3. 适用性和限制:Direct I/O 适用于某些特定的场景,例如数据库 MySQL、文件传输等需要高性能和低延迟的应用,然而它也存在一些限制和特殊情况,比如一些文件系统不支持 Direct I/O,或者需要特定的对齐等

缺点

1、性能波动,对于小文件和随机访问,Direct I/O 性能可能不如预期,因为 Direct I/O 一般更适用于大文件和顺序读写,而在小文件或随机访问的情况下,由于额外的处理和数据对齐要求,性能可能不稳定或下降

Kafka 中使用了 sendfile 作为顺序读写的操作,后续在 Kafka 专栏展开说说

2、对齐要求:Direct I/O 可能对数据的对齐有一定的要求,如果数据没有按照特定的方式传输,可能会导致性能下降,因此对于一些应用来说,确保对齐可能额外的处理

3、由于 Direct I/O 是用户空间与磁盘设备之间直接交互的,所以会忽略 Linux page cache,由应用程序自身来花费空间来维护缓存以及数据一致性问题、Dirty 脏刷写等一系列复杂的问题.

总结

在这里插入图片描述

如上图,性能对比

JVM 堆 < Java 进程堆 < MappedByteBuffer

该篇博文围绕 mmap、sendfile、Direct I/O 进行了技术点的展开讲解,mmap 由 FileChannel#map 映射出一个 MappedByteBuffer(应用空间与内核空间共享一块内存区域,不会触发系统调用)它适用于文件操作 I/O;sendfile 通过一次系统调用以后,它会在内核态完成数据的拷贝过程,无须用户态的参与,它适用于网络传输;Direct I/O 是由用户空间直接与磁盘设备之间交互,无须内核态的参与,它交由用户程序自身来维护缓存以及数据一致性、Dirty 等问题,希望博文你能够喜欢,感谢三连支持❤️

mmap:由用户空间与内核空间共享同一块内存区域,用户空间对其进行操作同样反映到内核空间,内核空间对其进行操作同样反映到用户空间
sendfile:由用户空间触发一次系统调用后,数据的拷贝过程由内核自身来完成,从一个 Socket Buffer 缓冲区拷贝到另外一个 Socket Buffer 缓冲区
Direct I/O:由用户空间与磁盘设备之间直接交互,无须内核态的参与

mmap、sendfile 依然绕不开内核的 page cache 体系,它基于内存,在极端情况下仍然会丢失数据.

🌟🌟🌟愿你我都能够在寒冬中相互取暖,互相成长,只有不断积累、沉淀自己,后面有机会自然能破冰而行!

博文放在 网络 I/O 专栏里,欢迎订阅,会持续更新!

如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!

推荐专栏:Spring、MySQL,订阅一波不再迷路

大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!

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

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

相关文章

React网页转换为pdf并下载|使用jspdf html2canvas

checkout 分支后突然报错&#xff0c;提示&#xff1a; Cant resolve jspdf in ... Cant resolve html2canvas in ... 解决方法很简单&#xff0c;重新 yarn install 就好了&#xff0c;至于为什么&#xff0c;我暂时也不知道&#xff0c;总之解决了。 思路来源&#xff1a; 先…

华清远见作业第十四天

思维导图 1、顺序表按元素删除 代码&#xff1a; int delete_num_delete(sqlist *list,datatype key) {int indexseek_num(list,key);//元素查找函数if(index-1){return -1;}delete_index(list,index);return 0; } 2、顺序表按照元素修改 代码&#xff1a; //顺序表按照元…

使用Flask逐步搭建Web应用程序

大家好&#xff0c;Flask是一个使用Python编写的轻量级Web应用框架。它被设计成简单、易于学习和使用的&#xff0c;同时具备足够的灵活性和扩展性&#xff0c;以满足各种规模的Web应用开发需求。本文我们将介绍一个使用Flask逐步搭建Web应用程序的简单入门示例。 1.安装Flask…

C语言---井字棋(三子棋)

Tic-Tac-Toe 1 游戏介绍和随机数1.1 游戏介绍1.2 随机数的生成1.3 棋盘大小和符号 2 设计游戏2.1 初始化棋盘2.2 打印棋盘2.3 玩家下棋2.4 电脑下棋2.5 判断输赢2.6 game()函数2.7 main()函数 3 完整三子棋代码3.1 Tic_Tac_Toe.h3.2 Tic_Tac_Toe.c3.3 Test.c 4 游戏代码的缺陷 …

Tomcat报404问题解决方案大全(包括tomcat可以正常运行但是报404)

文章目录 Tomcat报404问题解决方案大全(包括tomcat可以正常运行但是报404)1、正确的运行页面2、报错404问题分类解决2.1、Tomcat未配置环境变量2.2、IIs访问权限问题2.3、端口占用问题2.4、文件缺少问题解决办法&#xff1a; Tomcat报404问题解决方案大全(包括tomcat可以正常运…

【Docker】基于华为 openEuler 应用 Docker 镜像体积压缩

书接 openEuler 系列文章&#xff08;可以翻看测试系列&#xff09;&#xff0c;本次跟大家说说如何将 Java 包轻量化地构建到 openEuler 镜像中且保持镜像内操作系统是全补丁状态。 之前我们都是使用现成的 jdk 镜像进行构建的&#xff0c;如下图&#xff1a; FROM ibm-seme…

ROS笔记之rosbag的快速切片(C++实现)

ROS笔记之rosbag的快速切片(C实现) —— 杭州 2023-12-21 夜 code review 文章目录 ROS笔记之rosbag的快速切片(C实现)1.运行效果2.文件结构3.fast_rosbag_slice.cpp4.CMakeLists.txt5.package.xml6.对fast_rosbag_slice.cpp进行函数封装 正常该功能是ROS官方命令行&#xff1a…

安全、效率、成本:混合云数据库管理的三重挑战!

随着业务需求的不断演变&#xff0c;数据在多云平台之间流动&#xff0c;给数据库管控带来了新的层次和复杂性。这给数据库管控带来了前所未有的挑战。企业可能面临着一系列问题&#xff0c;包括安全性挑战、管理复杂性、性能与效率问题、成本控制难题、缺乏统一的管理视图以及…

Hardhat环境搭建(六)---无需翻墙

Hardhat环境搭建 官方地址 node环境 npm环境 git环境 安装hardhat npm init npminit是什么 在node开发中使用npm init会生成一个pakeage.json文件&#xff0c;这个文件主要是用来记录这个项目的详细信息的&#xff0c;它会将我们在项目开发中所要用到的包&#xff0c;以…

7.串口通信uart编写思路及自定义协议

前言&#xff1a; 串口是很重要的&#xff0c;有许多模块通信接口就是串口&#xff0c;例如gps模块&#xff0c;蓝牙模块&#xff0c;wifi模块还有一些精度比较高的陀螺仪模块等等&#xff0c;所以学会了串口之后&#xff0c;这些听起来很牛批的模块都能够用起来了。此外&#…

【Midjourney】Midjourney根据prompt提示词生成黑白色图片

目录 &#x1f347;&#x1f347;Midjourney是什么&#xff1f; &#x1f349;&#x1f349;Midjourney怎么用&#xff1f; &#x1f514;&#x1f514;提示词格式 &#x1f34b;&#x1f34b;应用示例——“秘密花园”式涂色书配图生成 &#x1f34c;&#x1f34c;例子1…

pycharm下执行conda命令提示无法识别解决方案

1 问题描述 win10环境命令行执行conda命令&#xff0c;报命令无法识别&#xff0c;错误信息如下&#xff1a; PS D:\code\cv> conda activate pt conda : 无法将“conda”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写&#xff0c;如果包括路径&a…