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

2021/9/12 1:06:28

本文主要是介绍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() - 简书

 



这篇关于NIO源码解析-FileChannel高阶知识点map和transferTo、transferFrom的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程