一、零拷贝基本介绍
(1)零拷贝是网络编程的关键, 很多性能优化都离不开。
(2)在 Java 程序中,常用的零拷贝有 mmap(内存映射) 和 sendFile。那么,他们在OS里,到底是怎么样的一个的设计? 我们分析 mmap 和 sendFile 这两个零拷贝。
(3)另外我们看下NIO 中如何使用零拷贝。
二、传统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模型
这其实经历了四次状态转换(用户态->内核态->用户态->内核态)和四次拷贝(两次DMA拷贝和两次CPU拷贝)
DMA:Direct Memory Access 直接内存拷贝(不使用CPU)
三、mmap优化
mmap 通过内存映射,将文件映射到内核缓冲区,同时用户空间可以共享内核空间的数据。 这样在进行网络传输时,就可以减少内核空间到用户控件的拷贝次数。
mmap示意图:
这其实经历了四次状态转换(用户态->内核态->用户态->内核态)和三次拷贝(两次DMA拷贝和一次CPU拷贝)
四、sendFile 优化
Linux2.1版本
Linux 2.1 版本提供了sendFile函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer, 同时由于和用户态完全无关, 就减少了一次上下文切换。
示意图:
Linux2.4版本
Linux 在 2.4 版本中, 做了一些修改,避免了从内核缓冲区拷贝到 Socket Buffer 的操作,直接拷贝到协议栈, 从而再一次减少了数据拷贝。 具体如下图和小结:
这里其实有 一次cpu 拷贝kernel buffer -> socket buffer但是, 拷贝的信息很少, 比如lenght , offset , 消耗低, 可以忽略。
零拷贝的再次理解
(1)我们说零拷贝, 是从操作系统的角度来说的。 因为内核缓冲区之间, 没有数据是重复的(只有 kernel buffer 有一份数据)。
(2)零拷贝不仅仅带来更少的数据复制, 还能带来其他的性能优势, 例如更少的上下文切换, 更少的 CPU 缓存伪共享以及无 CPU 校验和计算。
注意:零拷贝从操作系统角度, 是指没有cpu 拷贝。
五、mmap 和 sendFile 的区别
(1)mmap 适合小数据量读写, sendFile 适合大文件传输。
(2)mmap 需要 4 次上下文切换, 3 次数据拷贝; sendFile 需要 3 次上下文切换, 最少 2 次数据拷贝。
(3)sendFile 可以利用 DMA 方式, 减少 CPU 拷贝, mmap 则不能(必须从内核拷贝到 Socket 缓冲区) 。
六、零拷贝案例
1、传统IO方式
服务器端:
//Java IO 的服务器
public class OldServer {
public static void main(String[] args) throws Exception{
ServerSocket serverSocket = new ServerSocket(7001);
while (true) {
Socket socket = serverSocket.accept();
DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());;
try {
byte[] byteArray = new byte[4096];
while (true) {
int readCount = dataInputStream.read(byteArray);
if (-1 == readCount) {
break;
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
客户端:
public class OldClient {
public static void main(String[] args) throws Exception{
Socket socket = new Socket("127.0.0.1", 7001);
//String fileName = "protoc-3.6.1-win32.zip";
String fileName = "剑指offer.zip";
FileInputStream fileInputStream = new FileInputStream(fileName);
DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
byte[] buffer = new byte[4096];
long startTime = System.currentTimeMillis();
long readCount = 0;
long total = 0;
while ((readCount = fileInputStream.read(buffer)) >= 0) {
total += readCount;
dataOutputStream.write(buffer);
}
System.out.println("发送总字节数: " + total + ", 耗时: " + (System.currentTimeMillis() - startTime));
dataOutputStream.close();
socket.close();
fileInputStream.close();
}
}
2、使用NIO 零拷贝方式传递(transferTo)一个大文件
服务器端:
//NIO 服务器
public class NewIOServer {
public static void main(String[] args) throws Exception{
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress address = new InetSocketAddress(7001);
serverSocketChannel.bind(address);
ServerSocket serverSocket = serverSocketChannel.socket();
//创建 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 作废
}
}
}
}
客户端:
public class NewIOClient {
public static final long FILE_SIZE = 8 * 1014 * 1024;
public static void main(String[] args) throws Exception{
SocketChannel socketChannel = SocketChannel.open();
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 7001);
socketChannel.connect(address);
//String fileName = "protoc-3.6.1-win32.zip";
String fileName = "剑指offer.zip";
//得到一个文件channel
FileChannel fileChannel = new FileInputStream(fileName).getChannel();
//准备发送
long startTime = System.currentTimeMillis();
System.out.println(fileChannel.size());
//在linux下一个transferTo 方法就可以完成传输
//在windows 下 一次调用 transferTo 只能发送8m , 就需要分段传输文件, 而且要主要
//传输时的位置 =》 课后思考...
//transferTo 底层使用到零拷贝
//long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
long transferCount = 0;
if (fileChannel.size() > FILE_SIZE) {
transferCount = transfertoGT8M(fileChannel, socketChannel);
} else {
transferCount = transferToLT8M(fileChannel, socketChannel);
}
System.out.println("发送的总的字节数 = " + transferCount + " 耗时:" + (System.currentTimeMillis() - startTime));
//关闭
fileChannel.close();
}
private static long transferToLT8M(FileChannel src, WritableByteChannel target) throws IOException {
return src.transferTo(0, src.size(), target);
}
private static long transfertoGT8M(FileChannel src, WritableByteChannel target) throws IOException {
long fileSize = src.size();
long count = fileSize % FILE_SIZE == 0 ? fileSize / FILE_SIZE : fileSize / FILE_SIZE + 1;
System.out.println(count);
long transferCount = 0;
for (int i = 0; i < count; i++) {
transferCount += src.transferTo(i * FILE_SIZE, FILE_SIZE, target);
}
return transferCount;
}
}
通过上面的案例可以发现,使用了零拷贝的情况,可以提高60%左右的性能。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· 面试官:你是如何进行SQL调优的?