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

2023-12-22 08:36

本文主要是介绍深入理解网络 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,订阅一波不再迷路

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

这篇关于深入理解网络 I/O:mmap、sendfile、Direct I/O的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/523289

相关文章

如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别详解

《如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别详解》:本文主要介绍如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别的相关资料,描述了如何使用海康威视设备网络SD... 目录前言开发流程问题和解决方案dll库加载不到的问题老旧版本sdk不兼容的问题关键实现流程总结前言作为

深入解析Spring TransactionTemplate 高级用法(示例代码)

《深入解析SpringTransactionTemplate高级用法(示例代码)》TransactionTemplate是Spring框架中一个强大的工具,它允许开发者以编程方式控制事务,通过... 目录1. TransactionTemplate 的核心概念2. 核心接口和类3. TransactionT

深入理解Apache Airflow 调度器(最新推荐)

《深入理解ApacheAirflow调度器(最新推荐)》ApacheAirflow调度器是数据管道管理系统的关键组件,负责编排dag中任务的执行,通过理解调度器的角色和工作方式,正确配置调度器,并... 目录什么是Airflow 调度器?Airflow 调度器工作机制配置Airflow调度器调优及优化建议最

一文带你理解Python中import机制与importlib的妙用

《一文带你理解Python中import机制与importlib的妙用》在Python编程的世界里,import语句是开发者最常用的工具之一,它就像一把钥匙,打开了通往各种功能和库的大门,下面就跟随小... 目录一、python import机制概述1.1 import语句的基本用法1.2 模块缓存机制1.

深入理解C语言的void*

《深入理解C语言的void*》本文主要介绍了C语言的void*,包括它的任意性、编译器对void*的类型检查以及需要显式类型转换的规则,具有一定的参考价值,感兴趣的可以了解一下... 目录一、void* 的类型任意性二、编译器对 void* 的类型检查三、需要显式类型转换占用的字节四、总结一、void* 的

深入理解Redis大key的危害及解决方案

《深入理解Redis大key的危害及解决方案》本文主要介绍了深入理解Redis大key的危害及解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着... 目录一、背景二、什么是大key三、大key评价标准四、大key 产生的原因与场景五、大key影响与危

SSID究竟是什么? WiFi网络名称及工作方式解析

《SSID究竟是什么?WiFi网络名称及工作方式解析》SID可以看作是无线网络的名称,类似于有线网络中的网络名称或者路由器的名称,在无线网络中,设备通过SSID来识别和连接到特定的无线网络... 当提到 Wi-Fi 网络时,就避不开「SSID」这个术语。简单来说,SSID 就是 Wi-Fi 网络的名称。比如

Java实现任务管理器性能网络监控数据的方法详解

《Java实现任务管理器性能网络监控数据的方法详解》在现代操作系统中,任务管理器是一个非常重要的工具,用于监控和管理计算机的运行状态,包括CPU使用率、内存占用等,对于开发者和系统管理员来说,了解这些... 目录引言一、背景知识二、准备工作1. Maven依赖2. Gradle依赖三、代码实现四、代码详解五

深入理解C++ 空类大小

《深入理解C++空类大小》本文主要介绍了C++空类大小,规定空类大小为1字节,主要是为了保证对象的唯一性和可区分性,满足数组元素地址连续的要求,下面就来了解一下... 目录1. 保证对象的唯一性和可区分性2. 满足数组元素地址连续的要求3. 与C++的对象模型和内存管理机制相适配查看类对象内存在C++中,规

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06