零拷贝比传统的IO拷贝性能高很多,主要是减少了内核和用户模式之间的上下文切换次数。零拷贝完全依赖于操作系统。操作系统支持,就有;不支持,就没有。不依赖Java本身。
传统I/O
在Java中,我们可以通过InputStream从源数据中读取数据流到一个缓冲区里,然后再将它们输入到OutputStream里。我们知道,这种IO方式传输效率是比较低的。那么,当使用上面的代码时操作系统会发生什么情况:
这是一个从磁盘文件读取并且通过socket写出的过程,对应的系统调用如下:
read(file,tmp_buf,len)
write(socket,tmp_buf,len)
- 程序使用read()系统调用。系统由用户态转换为内核态(第一次上线文切换),磁盘中的数据有DMA(Direct Memory Access)的方式读取到内核缓冲区(kernel buffer)。DMA过程中CPU不需要参与数据的读写,而是DMA处理器直接将硬盘数据通过总线传输到内存中。
- 系统由内核态转换为用户态(第二次上下文切换),当程序要读取的数据已经完成写入内核缓冲区以后,程序会将数据由内核缓存区,写入用户缓存区),这个过程需要CPU参与数据的读写。
- 程序使用write()系统调用。系统由用户态切换到内核态(第三次上下文切换),数据从用户态缓冲区写入到网络缓冲区(Socket Buffer),这个过程需要CPU参与数据的读写。
- 系统由内核态切换到用户态(第四次上下文切换),网络缓冲区的数据通过DMA的方式传输到网卡的驱动(存储缓冲区)中(protocol engine)
案例是从一个文件中传送数据,如图所示为从CodeForces.rar传数据。
所传送文件的大小如图:是19130字节
客户端NewIOClient.java :
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
public class NewIOClient {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost",4399));
String filename="CodeForces.rar";
//得到文件的channel
FileChannel fileChannel = new FileInputStream(filename).getChannel();
long startTimeMillis = System.currentTimeMillis();
//在Linux下一个transto函数可以完成传输
//在windows下一次调用只能发送8M的文件,需要分段传输文件,而且要注意传送时的位置
//transferto底层用到零拷贝,传送到socketchannel中
long transfercount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
System.out.println("发送的字节数为:"+transfercount+",耗时为:" + (System.currentTimeMillis() - startTimeMillis));
fileChannel.close();
}
}
服务端NewIOServer.java :
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 NewIOServer {
public static void main(String[] args) throws IOException {
InetSocketAddress inetSocketAddress = new InetSocketAddress(4399);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(inetSocketAddress);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (true){
SocketChannel socketChannel = serverSocketChannel.accept();
int readacount=0;
while (-1!=readacount){
try {
readacount = socketChannel.read(byteBuffer);
}catch (Exception e){
break;
}
byteBuffer.rewind(); //将buffer的position置为0,mark作废
}
}
}
}
先运行服务端代码,再运行客户端,发现只用了7ms,而传统IO至少要20ms以上,可用看出零拷贝这种方式是很节约开销的了。