零拷貝主要是優化內核緩沖區和用戶緩存區的之間拷貝次數
怎么出現一步一步出現零拷貝的呢,下面跟大家講一下。
下圖是當用戶發出讀寫請求到操作系統進行交互的簡單流程圖
傳統模式
從上圖描述,把數據從內核緩沖區拷貝到用戶緩沖區只是一次讀操作,但是在網絡編程中,該操作需要4次拷貝,4次上下文切換,因此性能低。
零拷貝模式(主要有mmap和sendFlie)
先了解一個概念:
DMA拷貝(direct memory access):直接內存拷貝,不使用CPU
mmap優化
mmap通過內存映射,將內存緩沖區中的數據映射到用戶緩沖區中,這樣用戶空間可以共享內核空間的數據。在進行網絡傳輸時,就可以減少內核空間到用戶空間的CPU拷貝次數。但是還需要將內核緩沖區中的數據通過CPU拷貝到socket緩沖區中。該操作有3次拷貝,4次上下文切換。
sendFile優化(有兩個版本)
1、在Linux2.1版本中使用sendFile系統調用函數時,把用戶緩沖區去掉了,直接將內核緩沖區通過CPU拷貝到socket緩沖區中(由於把用戶緩沖區去掉了,因此就減少了兩次上下文切換)。該操作3次拷貝,2次上下文切換。
2、在Linux在2.4版本中真正實現了零拷貝,對2.1版本進行了修改,避免了將內核緩沖區中的數據通過CPU拷貝到socket緩沖區的操作,而是直接將內核緩沖區中的數據通過DMA拷貝到網卡中。該操作有2次拷貝,2次上下文切換。
1、與傳統模式相比,零拷貝實現了更少的數據復制、更少的上下文切換、更少的CPU緩存偽共享以及CPU校驗和計算。
2、sendFile可以利用DMA方式來減少CPU拷貝;mmap不可以利用DMA的方式來減少CPU拷貝,而是必須從內核緩沖區將數據通過CPU拷貝到socket緩沖區。
在NIO中,使用fileChannel.transferTo就可以實現零拷貝
- 在linux一次調用transferTo方法就可以完成傳輸
- 在windows每次調用transferTo只能發送8MB,就需要分段傳輸文件
案例如下
NewIOServer.java服務端
服務端代碼
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 作廢
}
}
}
}
NewIOClient.java客戶端
客戶端代碼
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();
}
}