文章目录
1. 简介
零拷贝基本介绍(所谓零拷贝就是从操作系统角度出发不经过CPU拷贝
)
- 零拷贝是网络编程的关键,很多性能优化都离不开
- 在java程序中,常用的零拷贝有
mmap(内存映射)
和sendFile
。
2. 零拷贝的演化过程
2.1 传统IO数据读写
Java 传统 IO 和 网络编程的一段代码
File file = new File("test.txt");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
byte[] arr = new byte[(int) file.length()];
raf.read(arr);
Socket socket = new ServerSocket(8080).accept();
socket.getOutputStream().write(arr);
传统IO拷贝需要经历3次用户态与内核态切换,4次数据拷贝
DMA: direct memory access 直接内存拷贝(不使用CPU)
经过了 3次用户态与内核态切换,4次拷贝
硬件设备Hard Driver -------->系统内核(Kernel buffer) -------------------- DMA拷贝
系统内核(Kernel buffer) ---------> 用户内核(user buffer) ----------------- CPU拷贝
用户内核(user buffer) ---------> socket buffer ------------------------------- CPU拷贝
socket buffer --------->协议栈(protocol engine) ----------------------------- DMA拷贝
2.2 mmap优化
3次切换 3次拷贝
mmap优化通过内核映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间数据,这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数
3次切换 3次拷贝,相对之前少了一次cpu拷贝
硬件设备Hard Driver -------->系统内核(Kernel buffer) -------------------- DMA拷贝
系统内核(Kernel buffer) ---------> 用户内核(user buffer)
系统内核(Kernel buffer) ---------> socket buffer ---------------------------- CPU拷贝
socket buffer --------->协议栈(protocol engine) ----------------------------- DMA拷贝
2.3 SendFile优化(linux 2.1版本)
2次切换 3次拷贝
提供了sendFile函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到SocketBuffer,同时,由于和用户态完全无关,就减少一次上下文切换
2次切换 3次拷贝,相对之前少了一次cpu拷贝一次切换
硬件设备Hard Driver -------->系统内核(Kernel buffer) -------------------- DMA拷贝
系统内核(Kernel buffer) ---------> socket buffer ---------------------------- CPU拷贝
socket buffer --------->协议栈(protocol engine) ----------------------------- DMA拷贝
2.4 SendFile优化(linux 2.4版本)
2次切换 2次拷贝
Linux 在 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。具体如下图和小结:
这里其实有 一次cpu 拷贝 kernel buffer -> socket buffer
但是,拷贝的信息很少,比如 lenght , offset , 消耗低,可以忽略
硬件设备Hard Driver -------->系统内核(Kernel buffer) -------------------- DMA拷贝
系统内核(Kernel buffer) ---------> 协议栈(protocol engine) -------------- DMA拷贝
小结:
- 我们说的零拷贝,是从操作系统的角度来说的,因为内核缓冲区之间,没有数据是重复的(只有 kernel buffer 有一份数据)
- 零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,更少的上下文切换,更少的cpu缓存为共享以及无cpu校验和计算
mmap和sendFile的区别
-
mmap适合小数据量读写,sendFile适合大文件传输
-
mmap需要3次上下文切换,3次数据拷贝 sendFile需要2次上下文切换,最少2次数据拷贝
-
sendFile 可以利用DMA方式,减少Cpu拷贝,mmap不能(必须从内核拷贝到socket缓冲区)
3. 代码演示零拷贝
客户端:
import java.io.FileInputStream;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
public class NewIOClient {
public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 7001));
String filename = "protoc-3.6.1-win32.zip";
//得到一个文件channel
FileChannel fileChannel = new FileInputStream(filename).getChannel();
//准备发送
long startTime = System.currentTimeMillis();
//在linux下一个transferTo 方法就可以完成传输
//在windows 下 一次调用 transferTo 只能发送8m ,
// 就需要分段传输文件, 而且要主要
//传输时的位置 =》 课后思考...
//transferTo 底层使用到零拷贝
long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
System.out.println("发送的总的字节数 =" + transferCount + " 耗时:" + (System.currentTimeMillis() - startTime));
//关闭
fileChannel.close();
}
}
服务端:
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 Exception {
InetSocketAddress address = new InetSocketAddress(7001);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(address);
//创建buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(4096);
while (true) {
SocketChannel socketChannel = serverSocketChannel.accept();
int readcount = 0;
while (-1 != readcount) {
try {
readcount = socketChannel.read(byteBuffer);
}catch (Exception ex) {
// ex.printStackTrace();
break;
}
//
byteBuffer.rewind(); //倒带 position = 0 mark 作废
}
}
}
}