支撑百万并发的“零拷贝”技术,你了解吗?(11)


通过 transferTo() 将 fromChannel 中的数据拷贝到 toChannel:

  1. @Test
  2. public void transferTo() throws Exception {
  3. try (FileChannel fromChannel = new RandomAccessFile(
  4. getClassPath(SOURCE_FILE), "rw").getChannel();
  5. FileChannel toChannel = new RandomAccessFile(
  6. getClassPath(TARGET_FILE), "rw").getChannel()) {
  7. long position = 0L;
  8. long offset = fromChannel.size();
  9. fromChannel.transferTo(position, offset, toChannel);
  10. }
  11. }
通过 transferFrom() 将 fromChannel 中的数据拷贝到 toChannel:
  1. @Test
  2. public void transferFrom() throws Exception {
  3. try (FileChannel fromChannel = new RandomAccessFile(
  4. getClassPath(SOURCE_FILE), "rw").getChannel();
  5. FileChannel toChannel = new RandomAccessFile(
  6. getClassPath(TARGET_FILE), "rw").getChannel()) {
  7. long position = 0L;
  8. long offset = fromChannel.size();
  9. toChannel.transferFrom(fromChannel, position, offset);
  10. }
  11. }
下面介绍 transferTo() 和 transferFrom() 方法的底层实现原理,这两个方法也是 java.nio.channels.FileChannel 的抽象方法,由子类 sun.nio.ch.FileChannelImpl.java 实现 。
transferTo() 和 transferFrom() 底层都是基于 Sendfile 实现数据传输的,其中 FileChannelImpl.java 定义了 3 个常量,用于标示当前操作系统的内核是否支持 Sendfile 以及 Sendfile 的相关特性 。
  1. private static volatile boolean transferSupported = true;
  2. private static volatile boolean pipeSupported = true;
  3. private static volatile boolean fileSupported = true;
transferSupported:用于标记当前的系统内核是否支持 sendfile() 调用,默认为 true 。
pipeSupported:用于标记当前的系统内核是否支持文件描述符(fd)基于管道(pipe)的 sendfile() 调用,默认为 true 。
fileSupported:用于标记当前的系统内核是否支持文件描述符(fd)基于文件(file)的 sendfile() 调用,默认为 true 。
下面以 transferTo() 的源码实现为例 。FileChannelImpl 首先执行 transferToDirectly() 方法,以 Sendfile 的零拷贝方式尝试数据拷贝 。
如果系统内核不支持 Sendfile,进一步执行 transferToTrustedChannel() 方法,以 mmap 的零拷贝方式进行内存映射,这种情况下目的通道必须是 FileChannelImpl 或者 SelChImpl 类型 。
如果以上两步都失败了,则执行 transferToArbitraryChannel() 方法,基于传统的 I/O 方式完成读写,具体步骤是初始化一个临时的 DirectBuffer,将源通道 FileChannel 的数据读取到 DirectBuffer,再写入目的通道 WritableByteChannel 里面 。
  1. public long transferTo(long position, long count, WritableByteChannel target)
  2. throws IOException {
  3. // 计算文件的大小
  4. long sz = size();
  5. // 校验起始位置
  6. if (position > sz)
  7. return 0;
  8. int icount = (int)Math.min(count, Integer.MAX_VALUE);
  9. // 校验偏移量
  10. if ((sz - position) < icount)
  11. icount = (int)(sz - position);
  12. long n;
  13. if ((n = transferToDirectly(position, icount, target)) >= 0)
  14. return n;
  15. if ((n = transferToTrustedChannel(position, icount, target)) >= 0)
  16. return n;
  17. return transferToArbitraryChannel(position, icount, target);
  18. }
接下来重点分析一下 transferToDirectly() 方法的实现,也就是 transferTo() 通过 Sendfile 实现零拷贝的精髓所在 。
可以看到,transferToDirectlyInternal() 方法先获取到目的通道 WritableByteChannel 的文件描述符 targetFD,获取同步锁然后执行 transferToDirectlyInternal() 方法 。
  1. private long transferToDirectly(long position, int icount, WritableByteChannel target)
  2. throws IOException {
  3. // 省略从target获取targetFD的过程
  4. if (nd.transferToDirectlyNeedsPositionLock()) {
  5. synchronized (positionLock) {
  6. long pos = position();
  7. try {
  8. return transferToDirectlyInternal(position, icount,
  9. target, targetFD);
  10. } finally {
  11. position(pos);
  12. }
  13. }
  14. } else {
  15. return transferToDirectlyInternal(position, icount, target, targetFD);
  16. }
  17. }
最终由 transferToDirectlyInternal() 调用本地方法 transferTo0() ,尝试以 Sendfile 的方式进行数据传输 。
如果系统内核完全不支持 Sendfile,比如 Windows 操作系统,则返回 UNSUPPORTED 并把 transferSupported 标识为 false 。
如果系统内核不支持 Sendfile 的一些特性,比如说低版本的 Linux 内核不支持 DMA gather copy 操作,则返回 UNSUPPORTED_CASE 并把 pipeSupported 或者 fileSupported 标识为 false 。
  1. private long transferToDirectlyInternal(long position, int icount,


    推荐阅读