"零拷貝"這三個字,想必你們多多少少都有聽過吧,這個技術在各類開源組件中都使用了,好比kafka,rocketmq,netty,nginx等等開源框架都在其中引用了這項技術。因此今天想和你們分享一下有關於零拷貝的一些知識。java
在介紹零拷貝以前我想說下在計算機系統中數據傳輸的方式。數據傳輸系統的發展,爲了寫這一部分又祭出了我塵封多年的計算機組成原理:nginx
分散鏈接,串行工做,程序查詢。 在這個階段,CPU就像個保姆同樣,須要手把手的把數據從I/O接口從讀出而後再送給主存。 git
這個階段具體流程是:這種效率很低數據傳輸過程一直佔據着CPU,CPU不能作其餘更有意義的事。github
這一部分介紹的也是咱們後面具體緩存
在馮諾依曼結構中,每一個部件之間均有單獨連線,不只先多,並且致使擴展I/O設備很不容易,咱們上面的早期階段就是這個體系,叫做分散鏈接。擴展一個I/O設備得鏈接不少線。因此引入了總線鏈接方式,將多個設備鏈接在同一組總線上,構成設備之間的公共傳輸通道。 bash
這個也是如今咱們家用電腦或者一些小型計算器的數據交換結構。在這種模式下數據交換採用程序中斷的方式,咱們上面知道咱們啓動I/O設備以後一直在輪詢問I/O設備是否準備好,要是把這個階段去掉了就行了,程序中斷很好的實現了咱們的夙願:網絡
雖然上面的方式雖然提升了CPU的利用率,可是在中斷的時候CPU同樣是被佔用的,爲了進一步解決CPU佔用,又引入了DMA方式,在DMA方式中,主存和I/O設備之間有一條數據通路,這下主存和I/O設備之間交換數據時,就不須要再次中斷CPU。app
通常來講咱們只須要關注DMA和中斷兩種便可,下面介紹的都是用來適合大型計算機的一些,這裏只說簡單的過一下:框架
在小型計算機中採用DMA方式能夠實現高速I/O設備與主機之間組成數據的交換,但在大中型計算機中,I/O配置繁多,數據傳送平凡,若採用DMA方式會出現一系列問題。異步
因此引入了通道,通道用來管理I/O設備以及主存與I/O設備之間交換信息的部件,能夠視爲一種具備特殊功能的處理器。它是從屬於CPU的一個專用處理器,CPU不直接參與管理,故提升了CPU的資源利用率
輸入輸出系統發展到第四階段,出現了I/O處理機。I/O處理機又稱爲外圍處理機,它獨立於主機工做,既能夠完成I/O通道要完成的I/O控制,又完成格式處理,糾錯等操做。具備I/O處理機的輸出系統與CPU工做的並行度更高,這說明I.O系統對主機來講具備更大的獨立性。
咱們能夠看到數據傳輸進化的目標是一直在減小CPU佔有,提升CPU的資源利用率。
先介紹一下今天咱們的需求,在磁盤中有個文件,如今須要經過網絡傳輸出去。 若是是你應該怎麼作?經過上面的一些介紹,相信你心中應該有些想法了吧。
若是咱們用Java代碼實現的話用咱們會有以下的的實現:僞代碼參考以下:
public static void main(String[] args) {
Socket socket = null;
File file = new File("test.file");
byte[] b = new byte[(int) file.length()];
try {
InputStream in = new FileInputStream(file);
readFully(in, b);
socket.getOutputStream().write(b);
} catch (Exception e) {
}
}
private static boolean readFully(InputStream in, byte[] b) {
int size = b.length;
int offset = 0;
int len;
for (; size > 0;) {
try {
len = in.read(b, offset, size);
if (len == -1) {
return false;
}
offset += len;
size -= len;
} catch (Exception ex) {
return false;
}
}
return true;
}
複製代碼
這是咱們傳統的拷貝方式具體的數據流轉圖以下,PS:這裏不考慮Java中傳輸數據時須要先將堆中的數據拷貝到直接內存中。
能夠看見咱們總管須要經歷四個階段,2次DMA,2次CPU中斷,總共四次拷貝,有四次上下文切換,而且會佔用兩次CPU。
優勢:開發成本低,適合一些對性能要求不高的,好比一些什麼管理系統這種我以爲就應該夠了
缺點:屢次上下文切換,佔用屢次CPU,性能比較低。
上面是零拷貝呢?在wiki中的定位:一般是指計算機在網絡上發送文件時,不須要將文件內容拷貝到用戶空間(User Space)而直接在內核空間(Kernel Space)中傳輸到網絡的方式。
在java NIO中FileChannal.transferTo()實現了操做系統的sendFile,咱們能夠同下面僞代碼完成上面需求:
public static void main(String[] args) {
SocketChannel socketChannel = SocketChannel.open();
FileChannel fileChannel = new FileInputStream("test").getChannel();
fileChannel.transferTo(0,fileChannel.size(),socketChannel);
}
複製代碼
咱們經過java.nio中的channel替代了咱們上面的socket和fileInputStream,從而完成了咱們的零拷貝。
上面具體過程以下:
能夠看見咱們根本沒有把數據複製到咱們的應用緩存中,因此這種方式就是零拷貝。可是這種方式依然很蛋疼,雖然減小到了只有三次數據拷貝,可是仍是須要CPU中斷複製數據。爲啥呢?由於DMA須要知道內存地址我才能發送數據啊。因此在Linux2.4內核中作了改進,將Kernel buffer中對應的數據描述信息(內存地址,偏移量)記錄到相應的socket緩衝區當中。 最終造成了下面的過程:
這種方式讓CPU全程不參與拷貝,所以效率是最好的。
在第三方開源框架中Netty,RocketMQ,kafka中都有相似的代碼,你們若是感興趣能夠下來自行搜索。
上面咱們提到了零拷貝的實現,可是咱們只能將數據原封不動的發給用戶,並不能本身使用。因而Linux提供的一種訪問磁盤文件的特殊方式,能夠將內存中某塊地址空間和咱們要指定的磁盤文件相關聯,從而把咱們對這塊內存的訪問轉換爲對磁盤文件的訪問,這種技術稱爲內存映射(Memory Mapping)。 咱們經過這種技術將文件直接映射到用戶態的內存地址,這樣對文件的操做再也不是write/read,而是直接對內存地址的操做。
在Java中依靠MappedByteBuffer進行mmap映射,具體的MappedByteBuffer能夠詳情參照這篇文章:www.jianshu.com/p/f90866dcb… 。
自此,零拷貝的神祕面紗也被揭蓋,零拷貝只是爲了減小CPU的佔用,讓CPU作更多真正業務上的事。經過這篇文章,你們能夠本身下來看看Netty是怎麼作零拷貝的相信將會有更加深入的印象。
最後這篇文章被我收錄於JGrowing,一個全面,優秀,由社區一塊兒共建的Java學習路線,若是您想參與開源項目的維護,能夠一塊兒共建,github地址爲:github.com/javagrowing… 麻煩給個小星星喲。
若是你們以爲這篇文章對你有幫助,或者你有什麼疑問想提供1v1免費vip服務,均可以關注個人公衆號,你的關注和轉發是對我最大的支持,O(∩_∩)O: