🔭 嗨,您好 👋 我是 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 带有不好的地方有几点,如下:
- mmap 在使用时必须指定好内存映射的大小,它不适合于变长的文件,若映射的文件过大,会消耗大量的内存,内存消耗的增加可能限制了程序的并发性,特别是当多个进程都需要映射大型文件时
- 对映射区域的写入操作会异步将修改的内容刷写到磁盘,若系统崩溃或发生断电宕机的情况下,部分尚未同步到磁盘的数据会丢失导致数据不一致问题
- 不适合随机访问大文件,因为它是虚拟内存映射的,并不是物理上的,还需要经过大量的分段分页寻址的过程,加载大文件时就需要较长的时间
- 不适用所有的场景,对于一次性操作或少量数据访问的场景,根本没必要使用到 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 实际上是指使用 read、write 等系统调用以及相关的文件描述符在用户空间和设备之间直接进行数据传输,绕过了内核的缓冲区 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 的实现机制,主要涉及以下几点:
- 文件描述符标志 — O_DIRECT:使用 O_DIRECT 标志可以告知操作系统进行 Direct I/O,绕过内核缓冲区
- 用户空间和设备直接传输:在 Direct I/O 中,数据直接在用户空间的应用程序缓冲区和设备或文件之间进行传输,绕过了内核缓冲区的中间步骤
- 适用性和限制: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,订阅一波不再迷路
大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!