首先介绍普通拷贝,其流程图如下:
当一个用户线程发起要读取磁盘上的某个文件的请求,其大致流程如上图所示:
-
- 用户线程发送系统调用read(),由于read()是系统调用,当前线程切换到内核空间。
- 然后,请求文件数据,文件数据从硬件磁盘缓存到内核空间的缓冲区(Kernel Buffer),通过DMA机制(Direct Memory Access)。
- 接着,将内核空间缓存的数据复制到用户空间缓存,由内核空间切换到用户空间。
- 最后将用户空间的缓存数据复制到内存,用户线程的read()调用结束。
当用户线程发起write请求时,大致流程如下:
-
- 用户线程发送系统调用write(),由于write()是系统调用,当前线程切换到内核空间。
- 然后,将需要write的数据从用户空间复制到内核空间的缓冲区(Kernel Buffer)。
- 接着,将内核空间缓存的数据写入到硬件磁盘上。
- 最后发送done信号,由内核空间切换到用户空间,用户线程的write()调用结束
-
- 注意:以上的read,write过程各经历了(用户空间 -> 内核空间,内核空间 -> 用户空间)的切换,共计4次上下文切换,以及各一次额外的数据拷贝,(read():磁盘 -> 内核空间 -> 用户空间 -> 调用返回;write():用户空间 -> 内核空间 -> 磁盘 -> 调用返回),这都是额外的调用开销。
代码演示:
1 import java.io.DataInputStream;
2 import java.io.IOException;
3 import java.net.ServerSocket;
4 import java.net.Socket;
5
6 public class NonZeroServer {
7
8 public static void main(String[] args) throws IOException {
9 ServerSocket serverSocket=new ServerSocket(2233);
10 while(true) {
11 Socket socket=serverSocket.accept();
12 DataInputStream dataInputStream=new DataInputStream(socket.getInputStream());
13 byte[] byteArray=new byte[4096];
14 while(true) {
15 int readCnt=dataInputStream.read(byteArray,0,byteArray.length);
16 if(readCnt==-1) break;
17 }
18 }
19 }
20 }
1 import java.io.DataOutputStream;
2 import java.io.File;
3 import java.io.FileInputStream;
4 import java.io.IOException;
5 import java.io.InputStream;
6 import java.net.Socket;
7 import java.net.UnknownHostException;
8
9 public class NonZeroCopyClient {
10
11 public static void main(String[] args) throws UnknownHostException, IOException {
12 Socket socket=new Socket("localhost",2233);
13 String fileName="e:/onos-tutorial-1.0.0r161-ovf.zip";
14 InputStream inputStream=new FileInputStream(new File(fileName));
15 DataOutputStream dataOutputStream=new DataOutputStream(socket.getOutputStream());
16 byte[] buffer=new byte[4096];
17 long readCnt=0;
18 long total=0;
19 long startTime=System.currentTimeMillis();
20 while((readCnt=inputStream.read(buffer))!=-1) {
21 total+=readCnt;
22 dataOutputStream.write(buffer);
23 }
24 System.out.println("send "+total/1000.0+" KB,"+"cost "+(System.currentTimeMillis()-startTime)+" ms");
25
26 dataOutputStream.close();
27 socket.close();
28 inputStream.close();
29 }
30
31 }
下面介绍零拷贝:
上图演示的是操作系统层面的零拷贝(写数据,下图是零拷贝的读数据),相比普通的read,write调用,避免了在用户空间进行缓存,上下文切换有一般的4次变为2次,所有的数据操作都是在内核空间进行的(磁盘 <-> 内核空间 <-> 调用返回)。系统调用sendfile(),各操作系统的实现可能不一样,有的操作系统可能不支持零拷贝机制。
Java NIO中的MappedByteBuffer,可以将磁盘上的一个文件映射到内存中(仿佛文件存在于用户空间一样,也能减少系统上下文切换),适合有用户交互的文件读写。
代码演示:
1 import java.io.IOException;
2 import java.net.InetSocketAddress;
3 import java.net.ServerSocket;
4 import java.nio.ByteBuffer;
5 import java.nio.channels.ServerSocketChannel;
6 import java.nio.channels.SocketChannel;
7
8 public class ZeroCopyServer {
9
10 public static void main(String[] args) throws IOException {
11 ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
12 ServerSocket serverSocket=serverSocketChannel.socket();
13 serverSocket.setReuseAddress(true);
14
15 serverSocket.bind(new InetSocketAddress(2233));
16
17 ByteBuffer byteBuffer=ByteBuffer.allocate(4096);
18 while(true) {
19 SocketChannel socketChannel=serverSocketChannel.accept();
20 socketChannel.configureBlocking(true);
21 int readCnt=0;
22 while(readCnt!=-1) {
23 try {
24 readCnt=socketChannel.read(byteBuffer);//接受client的数据
25 }catch (IOException e) {
26 e.printStackTrace();
27 byteBuffer.clear();
28 break;
29 }
30 byteBuffer.rewind();//相比flip(),不修改limit值,limit始终与capacity值一致
31 }
32 }
33 }
34
35 }
1 import java.io.File;
2 import java.io.FileInputStream;
3 import java.io.IOException;
4 import java.net.InetSocketAddress;
5 import java.net.UnknownHostException;
6 import java.nio.channels.FileChannel;
7 import java.nio.channels.SocketChannel;
8
9 public class ZeroCopyClient {
10
11 public static void main(String[] args) throws UnknownHostException, IOException {
12 SocketChannel socketChannel=SocketChannel.open();
13 socketChannel.connect(new InetSocketAddress("localhost",2233));
14 socketChannel.configureBlocking(true);
15 String fileName="e:/onos-tutorial-1.0.0r161-ovf.zip";
16 FileChannel fileChannel=new FileInputStream(new File(fileName)).getChannel();
17
18 long startTime=System.currentTimeMillis();
19 long total=0;
20 long transferCnt=0;
21 while(total<fileChannel.size()) {
22 transferCnt=fileChannel.transferTo(total, fileChannel.size(), socketChannel);//transferTo()会调用操作系统的零拷贝
23 total+=transferCnt;
24 }
25
26 System.out.println("send "+total/1000.0+" KB,"+"cost "+(System.currentTimeMillis()-startTime)+" ms");
27
28 // System.out.println(fileChannel.size());
29 fileChannel.close();
30
31 }
32
33 }
上述普通拷贝与零拷贝(传输的文件大小为2.49GB),运行结果:
传统IO:send 2504169.281 KB,cost 8414 ms
零拷贝:send 2504169.281 KB,cost 1547 ms
可见零拷贝有着相当大的性能提升。