前言
從字面意思了解就是資料不需要來回的拷貝,大大提升了系統的性能;這個詞我們也經常在java nio,netty,kafka,rocketmq等架構中聽到,經常作為其提升性能的一大亮點;下面從i/o的幾個概念開始,進而在分析零拷貝。
i/o概念
緩沖區是所有i/o的基礎,i/o講的無非就是把資料移進或移出緩沖區;程序執行i/o操作,就是向作業系統送出請求,讓它要麼把緩沖區的資料排幹(寫),要麼填充緩沖區(讀);下面看一個java程序發起read請求加載資料大緻的流程圖:
程序發起read請求之後,核心接收到read請求之後,會先檢查核心空間中是否已經存在程序所需要的資料,如果已經存在,則直接把資料copy給程序的緩沖區;如果沒有核心随即向磁盤控制器發出指令,要求從磁盤讀取資料,磁盤控制器把資料直接寫入核心read緩沖區,這一步通過dma完成;
接下來就是核心将資料copy到程序的緩沖區;如果程序發起write請求,同樣需要把使用者緩沖區裡面的資料copy到核心的socket緩沖區裡面,然後再通過dma把資料copy到網卡中,發送出去;
你可能覺得這樣挺浪費空間的,每次都需要把核心空間的資料拷貝到使用者空間中,是以零拷貝的出現就是為了解決這種問題的;關于零拷貝提供了兩種方式分别是:mmap+write方式,sendfile方式;
所有現代作業系統都使用虛拟記憶體,使用虛拟的位址取代實體位址,這樣做的好處是:
1.一個以上的虛拟位址可以指向同一個實體記憶體位址
2.虛拟記憶體空間可大于實際可用的實體位址;
利用第一條特性可以把核心空間位址和使用者空間的虛拟位址映射到同一個實體位址,這樣dma就可以填充對核心和使用者空間程序同時可見的緩沖區了,大緻如下圖所示:
省去了核心與使用者空間的往來拷貝,java也利用作業系統的此特性來提升性能,下面重點看看java對零拷貝都有哪些支援。
使用mmap+write方式代替原來的read+write方式,mmap是一種記憶體映射檔案的方法,即将一個檔案或者其它對象映射到程序的位址空間,實作檔案磁盤位址和程序虛拟位址空間中一段虛拟位址的一一對映關系
這樣就可以省掉原來核心read緩沖區copy資料到使用者緩沖區,但是還是需要核心read緩沖區将資料copy到核心socket緩沖區,大緻如下圖所示:
sendfile系統調用在核心版本2.1中被引入,目的是簡化通過網絡在兩個通道之間進行的資料傳輸過程。sendfile系統調用的引入,不僅減少了資料複制,還減少了上下文切換的次數,大緻如下圖所示:
資料傳送隻發生在核心空間,是以減少了一次上下文切換;但是還是存在一次copy,能不能把這一次copy也省略掉,linux2.4核心中做了改進,将kernel buffer中對應的資料描述資訊(記憶體位址,偏移量)記錄到相應的socket緩沖區當中,這樣連核心空間中的一次cpu copy也省掉了;
java零拷貝
java nio提供的filechannel提供了map()方法,該方法可以在一個打開的檔案和mappedbytebuffer之間建立一個虛拟記憶體映射,mappedbytebuffer繼承于bytebuffer,類似于一個基于記憶體的緩沖區,隻不過該對象的資料元素存儲在磁盤的一個檔案中
調用get()方法會從磁盤中擷取資料,此資料反映該檔案目前的内容,調用put()方法會更新磁盤上的檔案,并且對檔案做的修改對其他閱讀者也是可見的;下面看一個簡單的讀取執行個體,然後在對mappedbytebuffer進行分析:
主要通過filechannel提供的map()來實作映射,map()方法如下:
分别提供了三個參數,mapmode,position和size;分别表示:mapmode:映射的模式,可選項包括:read_only,read_write,private;position:從哪個位置開始映射,位元組數的位置;size:從position開始向後多少個位元組;
重點看一下mapmode,請兩個分别表示隻讀和可讀可寫,當然請求的映射模式受到filechannel對象的通路權限限制,如果在一個沒有讀權限的檔案上啟用read_only,将抛出nonreadablechannelexception;
private模式表示寫時拷貝的映射,意味着通過put()方法所做的任何修改都會導緻産生一個私有的資料拷貝并且該拷貝中的資料隻有mappedbytebuffer執行個體可以看到;該過程不會對底層檔案做任何修改,而且一旦緩沖區被施以垃圾收集動作(garbage collected),那些修改都會丢失;大緻浏覽一下map()方法的源碼:
大緻意思就是通過native方法擷取記憶體映射的位址,如果失敗,手動gc再次映射;最後通過記憶體映射的位址執行個體化出mappedbytebuffer,mappedbytebuffer本身是一個抽象類,其實這裡真正執行個體化出來的是directbytebuffer;
directbytebuffer繼承于mappedbytebuffer,從名字就可以猜測出開辟了一段直接的記憶體,并不會占用jvm的記憶體空間;上一節中通過filechannel映射出的mappedbytebuffer其實際也是directbytebuffer,當然除了這種方式,也可以手動開辟一段空間:
如上開辟了100位元組的直接記憶體空間;
經常需要從一個位置将檔案傳輸到另外一個位置,filechannel提供了transferto()方法用來提高傳輸的效率,首先看一個簡單的執行個體:
通過filechannel的transferto()方法将檔案資料傳輸到system.out通道,接口定義如下:
幾個參數也比較好了解,分别是開始傳輸的位置,傳輸的位元組數,以及目标通道;transferto()允許将一個通道交叉連接配接到另一個通道,而不需要一個中間緩沖區來傳遞資料;注:這裡不需要中間緩沖區有兩層意思:第一層不需要使用者空間緩沖區來拷貝核心緩沖區,另外一層兩個通道都有自己的核心緩沖區,兩個核心緩沖區也可以做到無需拷貝資料;
netty零拷貝
netty提供了零拷貝的buffer,在傳輸資料時,最終處理的資料會需要對單個傳輸的封包,進行組合和拆分,nio原生的bytebuffer無法做到,netty通過提供的composite(組合)和slice(拆分)兩種buffer來實作零拷貝;看下面一張圖會比較清晰:
tcp層http封包被分成了兩個channelbuffer,這兩個buffer對我們上層的邏輯(http處理)是沒有意義的。但是兩個channelbuffer被組合起來,就成為了一個有意義的http封包,這個封包對應的channelbuffer,才是能稱之為”message”的東西,這裡用到了一個詞”virtual buffer”。可以看一下netty提供的compositechannelbuffer源碼:
components用來儲存的就是所有接收到的buffer,indices記錄每個buffer的起始位置,lastaccessedcomponentid記錄上一次通路的componentid;compositechannelbuffer并不會開辟新的記憶體并直接複制所有channelbuffer内容,而是直接儲存了所有channelbuffer的引用,并在子channelbuffer裡進行讀寫,實作了零拷貝。
其他零拷貝
rocketmq的消息采用順序寫到commitlog檔案,然後利用consume queue檔案作為索引;rocketmq采用零拷貝mmap+write的方式來回應consumer的請求;同樣kafka中存在大量的網絡資料持久化到磁盤和磁盤檔案通過網絡發送的過程,kafka使用了sendfile零拷貝方式;
總結
零拷貝如果簡單用java裡面對象的機率來了解的話,其實就是使用的都是對象的引用,每個引用對象的地方對其改變就都能改變此對象,永遠隻存在一份對象。