一分鐘看穿零拷貝,看不懂你打我

想要弄清楚什麼是零拷貝,首先得明確一個問題,這裏的拷貝指的是什麼?我們這裏所描述的 拷貝 指的是在應用程序中將文件從 A 拷貝到 B,其中的 A 和 B 可以是電腦上的磁盤文件,也可以是網絡中的文件。像這樣的拷貝操作在操作系統中經歷了複雜的操作,首先應用程序發起讀取文件操作,讀取到文件後又發起寫入文件操作或者寫到網絡中去。

傳統的數據傳輸

瞭解傳統數據傳輸之前,我們要明確用戶態和內核態 2 個概念:

用戶態 是非特權執行狀態,該狀態下運行的程序被操作系統禁止進行一些危險操作,例如寫入系統配置文件,殺死其他用戶進程,重啓系統,不可直接訪問硬件設備。

內核態 是高級別特權執行狀態,運行在該狀態下的程序通常爲操作系統程序,具有高的特權級別,擁有訪問設備的所有權限。

讀、寫操作,在我們看來是一個完整的操作,其實在操作系統內部將讀操作又進行了細化拆分。

首先操作系統需要從用戶態切換到內核態,在內核態將文件內容從磁盤讀取到內核空間緩衝區中。

然後切換到用戶態,將文件內容從內核空間緩衝區讀取到用戶空間。

而寫操作正好是一個逆向的過程,程序需要先將文件內容寫到內核空間緩衝區,然後從用戶態切換到內核態,再將內容寫到磁盤裏,最後切換回用戶態。圖示爲如下:

 

聰明的寶寶肯定注意到了,這其中涉及到了 4 次的上下文切換和 4 次的數據拷貝操作,其中磁盤與內核態進行的拷貝操作應用了 DMA 技術(全稱 Direct Memory Access,它是一項由硬件設備支持的 IO 技術,應用這項技術可以更好的利用 CPU 資源,在此期間 CPU 可以去做其他事情),而內核態緩衝區到應用程序傳輸數據需要 CPU 的參與,在此期間 CPU 不能做其他工作。

mmap 提升拷貝性能

mmap 是一種內存映射的方法,它可以將磁盤上的文件映射進內存。用戶程序可以直接訪問內存即可達到訪問磁盤文件的效果,這種數據傳輸方法的性能高於將數據在內核空間和用戶之間來回拷貝操作,通常用於高性能要求的應用程序中。

因爲用戶態和內核態的上下文切換以及 CPU 數據拷貝是耗時的操作,所以可以考慮減少數據傳輸過程中的上下文切換和繁多的 CPU 拷貝數據操作,從而來提升數據傳輸性能。採用 mmap 來代替 read 系統調用可以有效減少內核空間到用戶空間之間的 CPU 拷貝數據操作,於是就誕生了如下的工作情形:

 

觀察如上拷貝示意圖,我們可以發現此時上下文切換操作縮減到了 2 次,應用程序發起拷貝操作後切換到內核態,數據直接在內核態完成傳輸而不需要拷貝到用戶態,但是此處仍然進行了 3 次拷貝操作,其中還包含一次耗時的 CPU 拷貝操作。彆着急,接下來我們看看終極版零拷貝。

零拷貝技術

我們知道 DMA 技術是高效的,因此只要去除掉 CPU 拷貝操作即可大大的提升性能。在 Linux 內核 2.1 版本中引入了 sendfile 系統調用,採用這種方式後內核態的緩衝區之間不再進行 CPU 拷貝操作,只需要將源數據的地址和長度告訴目標緩衝區,然後直接採用 DMA 技術將數據傳輸到目的地,如下圖示:

 

如上採用 sendfile 已經剔除了所有耗時的 CPU 拷貝操作,相比於傳統的數據拷貝操作性能更高,這就是所謂的零拷貝技術。

Java 中的零拷貝

使用傳統的文件拷貝方式在 Java 你會看到如下樣板代碼:

try (FileInputStream fis = new FileInputStream("sourceFile.txt");
     FileOutputStream fos = new FileOutputStream("targetFile.txt")) {
  byte datas[] = new byte[1024*8];
  int len = 0;

  while((len = fis.read(datas)) != -1){
    fos.write(datas, 0, len);
  }
}

使用如上方式進行文件拷貝的內在執行原理就如我們開頭的介紹的那樣,經過了多次用戶態和內核態的切換,並且伴隨着耗時的 CPU 拷貝操作,可想而知在遇到大文件拷貝時候效率會比較低下,此時可以考慮使用零拷貝技術。

在Java 1.4 中, FileChannel 的 transferTo 方法即引入了零拷貝技術,讓我們來一起看一下,如何使用它來提升性能吧。

RandomAccessFile sourceFile = new RandomAccessFile("sourceFile.txt", "rw");
FileChannel fromChannel = sourceFile.getChannel();

RandomAccessFile targetFile = new RandomAccessFile("targetFile.txt", "rw");
FileChannel toChannel = targetFile.getChannel();

fromChannel.transferTo(0, fromChannel.size(), toChannel);

如上我們首先獲取 FilleChannel,然後調用 FileChannel 的 transferTo 方法即可實現零拷貝操作。內在執行原理就是使用 sendfile 系統調用,剔除了耗時的 CPU 拷貝操作,同時用戶態和內核態的上下文切換也是最少的,當你遇到文件拷貝的性能問題時,你可以考慮一下 FilleChannel。

FileChannel 中還提供了其他的方法,例如 transferFrom 方法,感興趣的小夥伴可以自己嘗試一下。

以上即爲本期的主題,小夥伴們是否有疑問呢?歡迎留言和我討論。

金三銀四啦,每天一道題目,讓 offer 來得簡單點。

感謝你的閱讀,我爲你準備了一份《高級 Java 面試指南》,點擊在看,關注公衆號,回覆 "禮物" 獲取。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章