什么是零拷贝
通过一张图片我们来分析 IO模式的内存分析零拷贝 大概的理解就是在操作数据时, 不需要将数据 从一个内存区域拷贝到另一个内存区域. 因为少了内存的拷贝, 因此 CPU 的效率就得到的提升,同时它是操作系统层面上的操作LINUX与 WINDOWS 操作的区别相当大 我们来LINUX系统下的操作,如果操作系统提供则有,如果操作系统没提供,java 是无法提供任何相关的操作
1.User space 是 用户空间 Kernel space 是内核空间 Hardware 是磁盘
2. jvm 通过底层read 方法 去请求内核空间 ,然后内核空间 ask 请求磁盘 read数据
3.磁盘 将数据 拷贝到 内核空间 通过DMA直接内存访问 ,然后 从内核空间 将数据拷贝到 用户空间
4.然后程序进行 数据的读取 在将数据拷贝 到 内核空间,内核空间再把数据 写入到 客户端Hardware 图中的write 中Hardware 与read 不是一个 不要混淆
5.操作完成 write 方法返回 结束
上面的IO模式内存分析 进行了四次拷贝 四次上下文的切换
我们在看看NIO 内存分析
1. jvm 发送一个 sendfile(Linux系统调用) 到内核空间 ,内核空间ask 到磁盘
2.磁盘 将数据 写入到 内核空间的缓冲区 ,内核空间的数据 在写入到 socket 缓冲区中
3.内核空间将数据 写入到 网络的客户端中
4.结束
上面的IO模式内存分析 进行了两次拷贝 两次上下文的切换
这样就数据的操作都在内核空间里进行的 就是一种零拷贝
后来 又进行改进 但是需要底层系统的支持
1. jvm 发送一个 sendfile(Linux系统调用) 到内核空间 ,内核空间ask 到磁盘
2.磁盘 通过直接内存的方式将数据 写入到 内核空间的缓冲区或者通过 scatter/gather来读取到内核空间缓冲区
对于scatter/gather这种方式是需要底层操作系统来支持的,另外 在硬件或内核空间操作系统层面上,对于某些缓冲区增加了文件描述符这种概念,在这个文件描述符中可以描述一些特性,比如内存空间 基本特定 长度 偏移量多少,这样也可以减少数据在内核空间中的复制过程,换句话说直接从磁盘中获取数据直接写入到对应的缓冲区中
此时也有一个问题: 对于用户来说 无法直接参与 数据的读取等,如果此时用户需要操作 文件的内容的修改 我们该怎么办,可以使用MappedByteBuffer 内存映射,所谓的内存映射就是 将操作系统中磁盘上的文件映射到内存中,直接修改内存就表示修改磁盘上的文件,前几篇文章有介绍 ,它可以减少不必要的内存拷贝,直接操作内核空间
我们来通过代码对比一下IO 与NIO 文件传输速率
老方式:
package com.nio;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class OldServer {
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8899);
while (true) {
Socket socket =serverSocket.accept();
DataInputStream datainputStream = new DataInputStream(socket.getInputStream());
byte[] byteArray = new byte[4096];
while (true) {
int readCount = datainputStream.read(byteArray);
if(-1 == readCount){
break;
}
}
}
}
}
package com.nio;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class OldClinet {
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost",8899);
FileInputStream inputSteam = new FileInputStream("C:/Users/lm/Desktop/Netty权威指南 第2版 带书签目录 完整版.pdf.zip");
DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
byte[] byteArray = new byte[4096];
int readCount = 0;
long total = 0;
long startTime = System.currentTimeMillis();
while((readCount = inputSteam.read(byteArray)) >=0){
total += readCount;
dataOutputStream.write(byteArray);
}
dataOutputStream.close();
System.out.println("发送总字节数 :"+total +",耗时:"+( System.currentTimeMillis() - startTime));
}
}
发送总字节数 :102621561,耗时:3934
新方式:
package com.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class NewServer {
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
ServerSocket serverSocket=serverSocketChannel.socket();
serverSocket.setReuseAddress(true);
serverSocket.bind(new InetSocketAddress(8899));
ByteBuffer by = ByteBuffer.allocate(4096);
while(true){
SocketChannel socketChannel =serverSocketChannel.accept();
socketChannel.configureBlocking(true);
int readCount = 0;
while(-1!=readCount){
readCount = socketChannel.read(by);
by.rewind();
}
}
}
}
package com.nio;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
public class NewClient {
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
SocketChannel socket = SocketChannel.open();
socket.connect(new InetSocketAddress("localhost",8899));
socket.configureBlocking(true);
FileInputStream inputSteam = new FileInputStream("C:/Users/lm/Desktop/Netty权威指南 第2版 带书签目录 完整版.pdf.zip");
FileChannel filechannel =inputSteam.getChannel();
long startTime = System.currentTimeMillis();
long transCount = filechannel.transferTo(0, filechannel.size(), socket);
System.out.println("发送总字节数 :"+transCount +",耗时:"+( System.currentTimeMillis() - startTime));
socket.close();
}
}
发送总字节数 :8388608,耗时:171
总结:从数据耗时上来看 明显零拷贝的方式要快一些
下面对一些方法解释一下:
serverSocket.setReuseAddress(true);
对于这个方法大概的意思就是 当一个TCP链接被关闭的时候,链接会保持一段时间超时的状态,通常称为TIME_WAIT状态(socket的状态),对于一个应用尝试去绑定一个服务器上的某一个端口号上,当服务器这个端口号上的这个链接处于timeout状态时,那么这个应用是不能绑定到这个端口号上,如果启用了setReuseAddress 那么socket链接就可以绑定到这个端口号上,即便这个链接处于timeout状态。也就是说之前有一个socket链接被关闭掉了,但是并不是说 关闭掉的链接的端口号马上就可以被其他socket链接所使用了,相反这个端口号会在被关闭后的一小段时间内处于超时的状态也叫TIME_WAIT状态,如果有一个新的socket想要绑定到这个端口号上时 就会提示端口被占用。
我们再来看:
我们再来看这张时序图:从liunx2.4版本之后就是采用这种方式进行零拷贝filechannel.transferTo(long position,long count,WritableByteChannel target);
这个方法的含义:将从通道关联的文件,将文件中的字节传递到给定可写的btye channle中,这个方法比简单的循环效率要高,很多操作系统可以传输字节直接从文件系统缓存中直接传输,而不是去复制他们。
从liunx2.4之后 socket有文件描述符,并进行了细微的修改并利用到Gatter。
1. jvm 发送一个 sendfile(Linux系统调用) 到内核空间 ,内核空间ask 到磁盘
2.磁盘 通过直接内存的方式将数据 写入到 内核空间的缓冲区,并且将内核空间缓冲区的一个文件描述符写到
socket buffer 中,描述符中包含 kemel buffer 长度大小和地址的引用,而不是把数据本身copy到socket buffer中,这时协议引擎就可以进行真正的数据发送,这时需要从两个地方开始读取,一个是从socket buffer获取长度地址,一个是根据长度/地址获取数据的本身kemel buffer,这样就再也不会出现 从kemel buffer 将数据copy到socket buffer,这才是真正的零拷贝
我们再来介绍一下MappedByteBuffer 通过javadoc文档:
它是一个直接缓存区是一个文件内存映射区域,MappedByteBuffer 它是可以通过Filechanle.map来实现的,MappedByteBuffer 是一种允许java程序直接在内存中访问的特殊的文件,可以将整个文件或者文件的一部分映射到内存中,并有操作系统来完成内容的修改并写入到文件当中,我们的应用程序只需要处理内存的数据,这样可以迅速的处理IO操作,用于内存映射文件的内存本身是在java堆的外面也叫堆外内存。
例子:
package nio; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; public class NioTest7 { /** * @param args * @throws IOException */ public static void main(String[] args) throws IOException { RandomAccessFile randomAccessFile = new RandomAccessFile("NioTest9.txt","rw"); FileChannel fileChannel = randomAccessFile.getChannel(); MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 5); mappedByteBuffer.put(0,(byte)'a'); //只需操作内存 不需要写入文件 怎么把数据写入文件当中 都是由操作系统来完成 不需要我们去管 mappedByteBuffer.put(3,(byte)'b'); randomAccessFile.close(); } }
我们再来看看Buffer的Scattering与Gathering:
分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“分散(scatter)”到多个Buffer中。
聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel 将多个Buffer中的数据“聚集(gather)”后发送到Channel。
scatter / gather经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的buffer中,这样你可以方便的处理消息头和消息体.
之前我们read或write都是操作一个buffer 装满之后重新定义position位置重新在往里面读或写,而Scattering 不仅可以传一个buffer 还可以传buffer数组 比如我们一个buffer[0] 长度2 buffer[1] 长度5 buffer[2] 长度9,只有把第一个读满 在去读第二个,而Gathering 写也可以传buffer数组。
那么什么时候会这么使用呢:
比如我们在网络操作的时候 自定义协议 第一个传递过来的请求数据 第一个 buffer[1] 长度5 buffer[2] 长度9 作为Header buffer[3] 作为 Body
例子:
package nio; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Arrays; /** * 关于Buffer的Scattering与Gathering */ public class NioTest9 { public static void main(String[] args) throws IOException{ ServerSocketChannel serverSocketChannel =ServerSocketChannel.open(); InetSocketAddress address = new InetSocketAddress(8899); serverSocketChannel.socket().bind(address); int messageLenth = 2 + 3 +4; ByteBuffer[] buffers = new ByteBuffer[3]; buffers[0] = ByteBuffer.allocate(2); buffers[1] = ByteBuffer.allocate(3); buffers[2] = ByteBuffer.allocate(4); SocketChannel socketChannel = serverSocketChannel.accept(); while (true){ int byteRead = 0; // 如果读到的字节数小于总的 继续读 while (byteRead <messageLenth){ long r = socketChannel.read(buffers); byteRead += r; System.out.println("byteRead :"+byteRead); Arrays.asList(buffers).stream().map(buffer -> "position:"+ buffer.position()+",limit"+buffer.limit()).forEach(System.out::println); } Arrays.asList(buffers).forEach(buffer ->{ buffer.flip(); }); long bytesWritee =0; while (bytesWritee < messageLenth){ long r = socketChannel.write(buffers); bytesWritee +=r; } Arrays.asList(buffers).forEach(buffer ->{ buffer.clear(); }); System.out.println("byteRead :"+byteRead+ ",bytesWritee:"+bytesWritee+",messageLenth:"+messageLenth); } } }
我们使用telnet localhost 8899
输入hellowor+回车 正好是9个字节
byteRead :9 position:2,limit2 position:3,limit3 position:4,limit4 byteRead :9,bytesWritee:9,messageLenth:9
如果输入 hello+回车 是6个字节
byteRead :6 position:2,limit2 position:3,limit3 position:1,limit4