可以这么理解,能减少不必要的数据拷贝次数,就算是“零拷贝”。
Linux2.4内核新增sendfile系统调用。磁盘数据通过DMA(direct memory access)拷贝到内核Buffer,直接通过DMA拷贝到NIC (network interface controller)Buffer,无需CPU拷贝,这是操作系统意义上的零拷贝。
用户进程需要读取磁盘数据,需要CPU中断,发起IO请求,每次的IO中断,都带来CPU的上下文切换
为了解决CPU的上下文切换,出现了DMA(显卡、网卡、声卡都是支持 DMA ),直接内存读取。可以理解为,让硬件直接跳过CPU的调度,直接访问主内存
直接上代码
//服务端读取 html 里的内容后变成字节数组
File file = new File("index.html");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
byte[] arr = new byte[(int) file.length()];
raf.read(arr);
//监听 8080 端口,接收请求处理
Socket socket = new ServerSocket(8080).accept();
//html 里的字节流写到 socket 中
socket.getOutputStream().write(arr);
读写流程图
备注:上半部分表示用户态和内核态的上下文切换。下半部分表示数据复制操作
总结:传统IO,发生了4次数据拷贝,3次上下文切换
目的:减少 IO 流程中不必要的拷贝
零拷贝需要 OS 支持,也就是需要 kernel 暴露 api,虚拟机不能操作内核。
Linux 支持的(常见)零拷贝
mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户控件的拷贝次数。
直接贴图:
如上图,user buffer 和 kernel buffer 共享 index.html。如果你想把硬盘的 index.html 传输到网络中,再也不用拷贝到用户空间,再从用户空间拷贝到 socket 缓冲区。
现在,你只需要从内核缓冲区拷贝到 socket 缓冲区即可,这将减少一次内存拷贝(从 4 次变成了 3 次),但不减少上下文切换次数。
Linux 2.1 版本提供了 sendFile 函数
其基本原理:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换。
如上图,我们进行 sendFile 系统调用时,
Scatter/Gather 可以看作是 sendfile 的增强版,批量 sendfile。
简单直接-直接从内核缓冲区拷贝到网络协议栈,避免了从内核缓冲区拷贝到 socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。
上图
现在,index.html 要从文件进入到网络协议栈,只需 2 次拷贝:
Java NlO 中 的 Channel (通道)就相当于操作系统中的内核缓冲区,有可能是读缓冲区,也有可能是网络缓冲区,而 Buffer 就相当于操作系统中的用户缓冲区
MappedByteBuffer mappedByteBuffer = new RandomAccessFile(file, "r")
.getChannel()
.map(FileChannel.MapMode.READ_ONLY, 0, len);
NIO 中的 FileChannel.map() 方法其实就是采用了操作系统中的内存映射方式,底层就是调用 Linux mmap() 实现的
// 使用sendfile:读取磁盘文件,并网络发送
FileChannel sourceChannel = new RandomAccessFile(source, "rw").getChannel();
SocketChannel socketChannel = SocketChannel.open(sa);
sourceChannel.transferTo(0, sourceChannel.size(), socketChannel);
ZeroCopyFile实现文件复制:
class ZeroCopyFile {
public void copyFile(File src, File dest) {
try (FileChannel srcChannel = new FileInputStream(src).getChannel();
FileChannel destChannel = new FileInputStream(dest).getChannel()) {
srcChannel.transferTo(0, srcChannel.size(), destChannel);
} catch (IOException e) {
e.printStackTrace();
}
}
}
Java NIO 提供的 FileChannel.transferTo 和 transferFrom 并不保证一定能使用零拷贝。实际上是否能使用零拷贝与操作系统相关,如果操作系统提供 sendfile 这样的零拷贝系统调用,则这两个方法会通过这样的系统调用充分利用零拷贝的优势,否则并不能通过这两个方法本身实现零拷贝
Netty 中也用到了 FileChannel.transferTo 方法,所以 Netty 的零拷贝也包括上面讲的操作系统级别的零拷贝。
传统的 ByteBuffer,如果需要将两个 ByteBuffer 中的数据组合到一起,我们需要首先创建一个size=size1+size2 大小的新的数组,然后将两个数组中的数据拷贝到新的数组中。但是使用 Netty 提供的组合 ByteBuf,就可以避免这样的操作,因为 CompositeByteBuf 并没有真正将多个 Buffer 组合起来,而是保存了它们的引用,从而避免了数据的拷贝,实现了零拷贝。
CompositeByteBuf:将多个缓冲区显示为单个合并缓冲区的虚拟缓冲区。
建议使用 ByteBufAllocator.compositeBuffer() 或者 Unpooled.wrappedBuffer(ByteBuf…),而不是显式调用构造函数。
源码:
public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter {
public static final ByteToMessageDecoder.Cumulator MERGE_CUMULATOR = new ByteToMessageDecoder.Cumulator() {
public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
ByteBuf var5;
try {
ByteBuf buffer;
if (cumulation.writerIndex() <= cumulation.maxCapacity() - in.readableBytes() && cumulation.refCnt() <= 1 && !cumulation.isReadOnly()) {
buffer = cumulation;
} else {
buffer = ByteToMessageDecoder.expandCumulation(alloc, cumulation, in.readableBytes());
}
buffer.writeBytes(in);
var5 = buffer;
} finally {
in.release();
}
return var5;
}
};
// 可以看出来这里用了ByteBufAllocator 来分配readable的空间,并写入累积器中
static ByteBuf expandCumulation(ByteBufAllocator alloc, ByteBuf cumulation, int readable) {
ByteBuf oldCumulation = cumulation;
cumulation = alloc.buffer(cumulation.readableBytes() + readable);
cumulation.writeBytes(oldCumulation); // 将原始累积器的数据copy到新的累积器
oldCumulation.release(); // 释放原始的累积器
return cumulation;
}
...
}
写文件 Region
从这里我们可以看出 netty 也调用了 FileChannelDe tansferTo 方法:
public class DefaultFileRegion extends AbstractReferenceCounted implements FileRegion {
private FileChannel file;
public long transferTo(WritableByteChannel target, long position) throws IOException {
long count = this.count - position;
if (count >= 0L && position >= 0L) {
if (count == 0L) {
return 0L;
} else if (this.refCnt() == 0) {
throw new IllegalReferenceCountException(0);
} else {
this.open();
long written = this.file.transferTo(this.position + position, count, target);
if (written > 0L) {
this.transferred += written;
} else if (written == 0L) {
validate(this, position);
}
return written;
}
} else {
throw new IllegalArgumentException("position out of range: " + position + " (expected: 0 - " + (this.count - 1L) + ')');
}
}
...
}