NIO源码解析-FileChannel高阶知识点map和transferTo、transferFrom

前言:

    上文我们介绍了下FileChannel的基本API使用。本文中,我们就一起看下FileChannel中的高阶API。

    说是高阶,还真的就是,这些知识点大量利用了操作系统的对文件传输映射的高级玩法,极大的提高了我们操作文件的效率。我们熟知的kafka、rocketMQ等也是用了这些高阶API,才有如此的高效率。

    我们提出一个需求,描述如下:提供一个对外的socket服务,该服务就是获取指定文件目录下的文件,并写出到socket中,最终展现在client端。

1.传统的文件网络传输过程

    按照此需求,常规方式,我们使用如下代码来完成:

File file = new File("D:\\test.txt");
Long size = file.length();
byte[] arr = new byte[size.intValue()];

try {
    // 1.将test.txt文件内容读取到arr中
    FileInputStream fileInputStream = new FileInputStream(file);
    fileInputStream.read(arr);

    // 2.提供对外服务
    Socket socket = new ServerSocket(9999).accept();

    // 3.传输到客户端
    socket.getOutputStream().write(arr);
} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

以上是一个最简单版本的实现。

那么从操作系统的角度,以上传输经历了哪些过程呢?

这中间的过程我们可以分为以下几步:
fileInputStream.read方法对应于:
1)第一次复制:read方法调用,用户态切换到内核态。数据从硬盘拷贝到内核缓冲区,基于DMA自动操作,不需要CPU支持
2)第二次复制:从内核缓冲区拷贝到用户缓冲区(也就是byte[] arr中)。read方法返回,用内核态到用户态的转换。
socket.getOutputStream().write(arr)对应于:
3)第三次复制:从用户缓冲区拷贝数据到socket的内核缓冲区。write方法调用,用户态切换到内核态。
4)数据从socket内核缓冲区,使用DMA拷贝到网络协议引擎。write方法返回,内核态切换到用户态。
从上面的过程我们可以发现,数据发生了四次拷贝,四次上下文切换。
那么还有没有优化方式呢?答案是肯定的,我们接着往下看。

2.mmap优化

    mmap通过内存映射,将文件直接映射到内存中。此时,用户空间和内核空间可以共享这段内存空间的内容。用户对内存内容的修改可以直接反馈到磁盘文件上。
FileChannel提供了map方法来实现mmap功能
File file = new File("D:\\test.txt");
Long size = file.length();
byte[] arr = new byte[size.intValue()];

try {
    // 1.将test.txt文件内容读取到arr中
    RandomAccessFile raFile = new RandomAccessFile(file, "rwd");
    FileChannel channel = raFile.getChannel();
    MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, size);

    // 2.提供对外服务
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

    serverSocketChannel.socket().bind(new InetSocketAddress(9999));
    serverSocketChannel.configureBlocking(false);

    while(true){
        SocketChannel socketChannel =
            serverSocketChannel.accept();

        if(socketChannel != null){
            // 3.传输到客户端
            socketChannel.write(mappedByteBuffer);
        }
    }

} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}
我们直接将file的内容映射到mappedByteBuffer,然后直接将mappedByteBuffer的内容传递出去。
那么从操作系统的角度,以上传输经历了哪些过程呢?

 

参考1中的四个步骤,少了一次内存拷贝,就是将文件从内核缓冲区拷贝到用户进程缓冲区这一步;但是上下文切换并没有减少。

3.sendFile优化(Linux2.1版本)

Linux2.1版本提供了sendFile函数,该函数对本例有哪些优化呢?
就是可以将数据不经过用户态,直接从内核文件缓冲区传输到Socket缓冲区
 
FileChannel提供transferTo(和transferFrom)方法来实现sendFile功能
File file = new File("D:\\test.txt");
Long size = file.length();

try {
    // 1.将test.txt文件内容读取到arr中
    RandomAccessFile raFile = new RandomAccessFile(file, "rwd");
    FileChannel channel = raFile.getChannel();

    // 2.提供对外服务
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

    serverSocketChannel.socket().bind(new InetSocketAddress(9999));
    serverSocketChannel.configureBlocking(false);

    while(true){
        SocketChannel socketChannel =
            serverSocketChannel.accept();

        if(socketChannel != null){
            // 3.使用transferTo方法将文件数据传输到客户端
            channel.transferTo(0, size, socketChannel);
        }
    }
} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}
同2中的代码,只是在最后一步将文件内容传输到socket时,使用了不一样的方法,本例中使用了FileChannel.transferTo方法来传递数据。
那么从操作系统的角度,以上传输经历了哪些过程呢?

 

参照1中的4个过程,少了用户空间的参与,那么就不存在用户态与内核态的切换。
所以,总结下来,就是减少了两次上下文切换,同时,减少了一次数据拷贝。
注意:剩下的是哪两次上下文切换呢?用户进程调用transferTo方法,用户态切换到内核态;调用方法返回,内核态切换到用户态。

4.sendFile优化(Linux2.4版本)

    在Linux2.4版本,sendFile做了一些优化,避免了从内核文件缓冲区拷贝到Socket缓冲区的操作,直接拷贝到网卡,再次减少了一次拷贝。
代码同3,只是具体实现时的操作系统不太一样而已。
那么从操作系统的角度,其传输经历了哪些过程呢?

 

参照1中的4个操作过程,同样少了用户空间的参与,也不存在用户态与内核态的切换。

所以总结下来,就是两次数据拷贝,两次上下文切换(相比较3就是减少了内核文件缓冲区到内核socket缓冲区的拷贝)

总结:

    下面我们通过一个图表来展示下以上四种传输方式的异同

传输方式上下文切换次数数据拷贝次数
传统IO方式44
mmap方式43
sendFile(Linux2.1)23
sendFile(Linux2.4)22
实际,以上sendFile的数据传输方式就是我们常说的零拷贝
可能会有些疑问,哪怕Linux2.4版本的sendFile函数不也是有两次数据拷贝嘛,为什么会说是零拷贝呢?
笔者拷贝了一段话,解释的蛮有意思的:
首先我们说零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(只有 kernel buffer 有一份数据,
sendFile 2.1 版本实际上有 2 份数据,算不上零拷贝)。例如我们刚开始的例子,内核缓存区和 Socket 缓冲区的数据就是重复的。

而零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的 CPU 缓存伪共享以及无 CPU 校验和计算。
再稍微讲讲 mmap 和 sendFile 的区别。

参考:

linux下的mmap和零拷贝技术 - 简书

mmap与sendfile() - 简书

 

  • 5
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
Java NIO(New IO)是Java 1.4版本提供的一种新的IO API,它提供了与传统IO API不同的IO处理方式,包括了通道(channel)和缓冲区(buffer)的概念。Java NIO的目标是提供比传统IO更快、更高效的IO操作方式。 Java NIO源码解析需要深入了解Java NIO的核心概念,主要包括以下几个部分: 1. 缓冲区(Buffer):Java NIO中的缓冲区是一个对象,它包含了一定数量的数据元素,并且提供了对这些数据元素的基本操作方法。Java NIO中的所有数据都是通过缓冲区来传输的。 2. 通道(Channel):Java NIO中的通道是一种对象,它可以用来读取和写入数据。通道类似于流,但是它们可以被双向读写,并且可以同时处理多个线程。 3. 选择器(Selector):Java NIO中的选择器是一种对象,它可以用来监视多个通道的事件(如读写就绪),从而实现单线程处理多个通道的能力。 4. 文件处理:Java NIO中提供了一组文件处理的API,包括了文件读写、文件锁、文件映射等功能。 Java NIO源码解析需要深入研究Java NIO的实现细节,包括了缓冲区的实现、通道的实现、选择器的实现等。其中,缓冲区的实现是Java NIO的核心,也是最复杂的部分。Java NIO中的缓冲区是通过JNI(Java Native Interface)和Java堆内存来实现的,它提供了高效的数据传输方式,但是也带来了一些复杂性。通道的实现是基于底层的操作系统文件描述符来实现的,它提供了高效的IO操作方式,但是也需要考虑系统平台的差异性。选择器的实现是基于操作系统提供的事件驱动机制来实现的,它可以实现单线程同时处理多个通道的能力,但是也需要考虑系统平台的差异性。 总之,Java NIO源码解析需要深入理解Java NIO的核心概念和实现细节,这样才能更好地理解Java NIO的工作机制,并且能够在实际开发中灵活运用Java NIO的各种功能。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

恐龙弟旺仔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值