java中零拷貝有2種(零拷貝是指沒有CPU拷貝)
1,mmap(內存映射)
2,sendfile
傳統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發現copy了4次,cpu狀態切換三次,
所以要優化;
MMAP優化:
mmap 通過內存映射,將文件映射到內核緩衝區,同時,用戶空間可以共享內核空間的數據。這樣,在進行網絡傳輸時,就可以減少內核空間到用戶控件的拷貝次數。如下圖
此時copy 3次,狀態也是3次,
sendFile 優化
Linux 2.1 版本 提供了 sendFile 函數,其基本原理如下:數據根本不經過用戶態,直接從內核緩衝區進入到 Socket Buffer,同時,由於和用戶態完全無關,就減少了一次上下文切換 示意圖和小結 提示:零拷貝從操作系統角度,是沒有cpu 拷貝
此時是3次拷貝,切換變成了2次;
Linux 在 2.4 版本中,做了一些修改,避免了從內核緩衝區拷貝到 Socket buffer 的操作,直接拷貝到協議棧,從而再一次減少了數據拷貝。具體如下圖和小結: 這裏其實有 一次cpu 拷貝 kernel buffer -> socket buffer 但是,拷貝的信息很少,比如 lenght , offset , 消耗低,可以忽略
上圖中socketbuffer變成灰色,kernel buffer可以通過DMA copy進入 protocol engine;
我們說零拷貝,是從操作系統的角度來說的。因爲內核緩衝區之間,沒有數據是重複的(只有 kernel buffer 有一份數據)。 零拷貝不僅僅帶來更少的數據複製,還能帶來其他的性能優勢,例如更少的上下文切換,更少的 CPU 緩存僞共享以及無 CPU 校驗和計
mmap 和 sendFile 的區別 mmap 適合小數據量讀寫,sendFile 適合大文件傳輸。 mmap 需要 4 次上下文切換,3 次數據拷貝;sendFile 需要 3 次上下文切換,最少 2 次數據拷貝。 sendFile 可以利用 DMA 方式,減少 CPU 拷貝,mmap 則不能(必須從內核拷貝到 Socket 緩衝區)。
例子:
比如下面的零拷貝
public class NewIOClient {
public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 6666));
String filename = "pro*****.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();
}
}
//服務器
public class NewIOServer {
public static void main(String[] args) throws Exception {
InetSocketAddress address = new InetSocketAddress(6666);
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 作廢
}
}
}
}
以及老的拷貝方式:
public class OldIOClient {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("localhost", 6666);
String fileName = "proto***.zip";
InputStream inputStream = new FileInputStream(fileName);
DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
byte[] buffer = new byte[4096];
long readCount;
long total = 0;
long startTime = System.currentTimeMillis();
while ((readCount = inputStream.read(buffer)) >= 0) {
total += readCount;
dataOutputStream.write(buffer);
}
System.out.println("發送總字節數: " + total + ", 耗時: " + (System.currentTimeMillis() - startTime));
dataOutputStream.close();
socket.close();
inputStream.close();
}
}
//java IO 的服務器
public class OldIOServer {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(6666);
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, 0, byteArray.length);
if (-1 == readCount) {
break;
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
old用時:
文件越大,差異體現的更明顯