摘要:前言從字面意思理解就是數據不需要來回的拷貝,大大提升了系統的性能這個詞我們也經常在,,,等框架中聽到,經常作為其提升性能的一大亮點下面從的幾個概念開始,進而在分析零拷貝。
前言
從字面意思理解就是數據不需要來回的拷貝,大大提升了系統的性能;這個詞我們也經常在java nio,netty,kafka,RocketMQ等框架中聽到,經常作為其提升性能的一大亮點;下面從I/O的幾個概念開始,進而在分析零拷貝。
I/O概念 1.緩沖區緩沖區是所有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 nio提供的FileChannel提供了map()方法,該方法可以在一個打開的文件和MappedByteBuffer之間建立一個虛擬內存映射,MappedByteBuffer繼承于ByteBuffer,類似于一個基于內存的緩沖區,只不過該對象的數據元素存儲在磁盤的一個文件中;調用get()方法會從磁盤中獲取數據,此數據反映該文件當前的內容,調用put()方法會更新磁盤上的文件,并且對文件做的修改對其他閱讀者也是可見的;下面看一個簡單的讀取實例,然后在對MappedByteBuffer進行分析:
public class MappedByteBufferTest { public static void main(String[] args) throws Exception { File file = new File("D://db.txt"); long len = file.length(); byte[] ds = new byte[(int) len]; MappedByteBuffer mappedByteBuffer = new FileInputStream(file).getChannel().map(FileChannel.MapMode.READ_ONLY, 0, len); for (int offset = 0; offset < len; offset++) { byte b = mappedByteBuffer.get(); ds[offset] = b; } Scanner scan = new Scanner(new ByteArrayInputStream(ds)).useDelimiter(" "); while (scan.hasNext()) { System.out.print(scan.next() + " "); } } }
主要通過FileChannel提供的map()來實現映射,map()方法如下:
public abstract MappedByteBuffer map(MapMode mode, long position, long size) throws IOException;
分別提供了三個參數,MapMode,Position和size;分別表示:
MapMode:映射的模式,可選項包括:READ_ONLY,READ_WRITE,PRIVATE;
Position:從哪個位置開始映射,字節數的位置;
Size:從position開始向后多少個字節;
重點看一下MapMode,請兩個分別表示只讀和可讀可寫,當然請求的映射模式受到Filechannel對象的訪問權限限制,如果在一個沒有讀權限的文件上啟用READ_ONLY,將拋出NonReadableChannelException;PRIVATE模式表示寫時拷貝的映射,意味著通過put()方法所做的任何修改都會導致產生一個私有的數據拷貝并且該拷貝中的數據只有MappedByteBuffer實例可以看到;該過程不會對底層文件做任何修改,而且一旦緩沖區被施以垃圾收集動作(garbage collected),那些修改都會丟失;大致瀏覽一下map()方法的源碼:
public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException { ...省略... int pagePosition = (int)(position % allocationGranularity); long mapPosition = position - pagePosition; long mapSize = size + pagePosition; try { // If no exception was thrown from map0, the address is valid addr = map0(imode, mapPosition, mapSize); } catch (OutOfMemoryError x) { // An OutOfMemoryError may indicate that we"ve exhausted memory // so force gc and re-attempt map System.gc(); try { Thread.sleep(100); } catch (InterruptedException y) { Thread.currentThread().interrupt(); } try { addr = map0(imode, mapPosition, mapSize); } catch (OutOfMemoryError y) { // After a second OOME, fail throw new IOException("Map failed", y); } } // On Windows, and potentially other platforms, we need an open // file descriptor for some mapping operations. FileDescriptor mfd; try { mfd = nd.duplicateForMapping(fd); } catch (IOException ioe) { unmap0(addr, mapSize); throw ioe; } assert (IOStatus.checkAll(addr)); assert (addr % allocationGranularity == 0); int isize = (int)size; Unmapper um = new Unmapper(addr, mapSize, isize, mfd); if ((!writable) || (imode == MAP_RO)) { return Util.newMappedByteBufferR(isize, addr + pagePosition, mfd, um); } else { return Util.newMappedByteBuffer(isize, addr + pagePosition, mfd, um); } }
大致意思就是通過native方法獲取內存映射的地址,如果失敗,手動gc再次映射;最后通過內存映射的地址實例化出MappedByteBuffer,MappedByteBuffer本身是一個抽象類,其實這里真正實例話出來的是DirectByteBuffer;
2.DirectByteBufferDirectByteBuffer繼承于MappedByteBuffer,從名字就可以猜測出開辟了一段直接的內存,并不會占用jvm的內存空間;上一節中通過Filechannel映射出的MappedByteBuffer其實際也是DirectByteBuffer,當然除了這種方式,也可以手動開辟一段空間:
ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(100);
如上開辟了100字節的直接內存空間;
3.Channel-to-Channel傳輸經常需要從一個位置將文件傳輸到另外一個位置,FileChannel提供了transferTo()方法用來提高傳輸的效率,首先看一個簡單的實例:
public class ChannelTransfer { public static void main(String[] argv) throws Exception { String files[]=new String[1]; files[0]="D://db.txt"; catFiles(Channels.newChannel(System.out), files); } private static void catFiles(WritableByteChannel target, String[] files) throws Exception { for (int i = 0; i < files.length; i++) { FileInputStream fis = new FileInputStream(files[i]); FileChannel channel = fis.getChannel(); channel.transferTo(0, channel.size(), target); channel.close(); fis.close(); } } }
通過FileChannel的transferTo()方法將文件數據傳輸到System.out通道,接口定義如下:
public abstract long transferTo(long position, long count, WritableByteChannel target) throws IOException;
幾個參數也比較好理解,分別是開始傳輸的位置,傳輸的字節數,以及目標通道;transferTo()允許將一個通道交叉連接到另一個通道,而不需要一個中間緩沖區來傳遞數據;
注:這里不需要中間緩沖區有兩層意思:第一層不需要用戶空間緩沖區來拷貝內核緩沖區,另外一層兩個通道都有自己的內核緩沖區,兩個內核緩沖區也可以做到無需拷貝數據;
netty提供了零拷貝的buffer,在傳輸數據時,最終處理的數據會需要對單個傳輸的報文,進行組合和拆分,Nio原生的ByteBuffer無法做到,netty通過提供的Composite(組合)和Slice(拆分)兩種buffer來實現零拷貝;看下面一張圖會比較清晰:
TCP層HTTP報文被分成了兩個ChannelBuffer,這兩個Buffer對我們上層的邏輯(HTTP處理)是沒有意義的。 但是兩個ChannelBuffer被組合起來,就成為了一個有意義的HTTP報文,這個報文對應的ChannelBuffer,才是能稱之為”Message”的東西,這里用到了一個詞”Virtual Buffer”。
可以看一下netty提供的CompositeChannelBuffer源碼:
public class CompositeChannelBuffer extends AbstractChannelBuffer { private final ByteOrder order; private ChannelBuffer[] components; private int[] indices; private int lastAccessedComponentId; private final boolean gathering; public byte getByte(int index) { int componentId = componentId(index); return components[componentId].getByte(index - indices[componentId]); } ...省略...
components用來保存的就是所有接收到的buffer,indices記錄每個buffer的起始位置,lastAccessedComponentId記錄上一次訪問的ComponentId;CompositeChannelBuffer并不會開辟新的內存并直接復制所有ChannelBuffer內容,而是直接保存了所有ChannelBuffer的引用,并在子ChannelBuffer里進行讀寫,實現了零拷貝。
其他零拷貝RocketMQ的消息采用順序寫到commitlog文件,然后利用consume queue文件作為索引;RocketMQ采用零拷貝mmap+write的方式來回應Consumer的請求;
同樣kafka中存在大量的網絡數據持久化到磁盤和磁盤文件通過網絡發送的過程,kafka使用了sendfile零拷貝方式;
零拷貝如果簡單用java里面對象的概率來理解的話,其實就是使用的都是對象的引用,每個引用對象的地方對其改變就都能改變此對象,永遠只存在一份對象。
參考<
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/77506.html
摘要:上一篇你不知道的筆記寫在前面這是年第一篇博客,回顧去年年初列的學習清單,發現僅有部分完成了。當然,這并不影響年是向上的一年在新的城市穩定連續堅持健身三個月早睡早起游戲時間大大縮減,學會生活。 上一篇:《你不知道的javascript》筆記_this 寫在前面 這是2019年第一篇博客,回顧去年年初列的學習清單,發現僅有部分完成了。當然,這并不影響2018年是向上的一年:在新的城市穩定、...
摘要:反對者在某些領域對此予以否認。下面再引用一段來自維基百科中關于的歷史。類的更嚴格的定義是由某種特定的元數據所組成的內聚的包。類還可以有運行時表示形式元對象,它為操作與類相關的元數據提供了運行時支持。 在開始部分,請看官非常非常耐心地閱讀下面幾個枯燥的術語解釋,本來這不符合本教程的風格,但是,請看官諒解,因為列位將來一定要閱讀枯燥的東西的。這些枯燥的屬于解釋,均來自維基百科。 1、問題...
摘要:什么是零拷貝我們首先來認識一下傳統的操作。因為在這套體系里,不僅僅提供了非阻塞的編程模型,而且提供了類似零拷貝,內存映射這樣的新技術對于操作系統來說早就有了。 什么是零拷貝?我們首先來認識一下傳統的I/O操作。假如說用戶進程現在要把一個文件復制到另一個地方。那么用戶程序必須先把這個文件讀入內存,然后再把內存里的數據寫入另一個文件。不過文件讀入內存也不是直接讀入用戶進程的內存,而是先讀入...
摘要:專題系列共計篇,主要研究日常開發中一些功能點的實現,比如防抖節流去重類型判斷拷貝最值扁平柯里遞歸亂序排序等,特點是研究專題之函數組合專題系列第十六篇,講解函數組合,并且使用柯里化和函數組合實現模式需求我們需要寫一個函數,輸入,返回。 JavaScript 專題之從零實現 jQuery 的 extend JavaScritp 專題系列第七篇,講解如何從零實現一個 jQuery 的 ext...
摘要:存儲所有經過不可逆函數后生成的值列表存儲的是已經被消費的中的隨機數生成的值。為和分別生成隨機數和將的公鑰設置到里面去,代表收款人是。至此,的匿名交易流程形成了閉環。在這里應用到了零知識證明,它的代碼是根據理論完成的,同時也參考了。 作者:林冠宏 / 指尖下的幽靈 掘金:juejin.im/user/587f0d… 博客:www.cnblogs.com/linguanh/ GitH...
閱讀 1317·2021-10-27 14:14
閱讀 3574·2021-09-29 09:34
閱讀 2477·2019-08-30 15:44
閱讀 1715·2019-08-29 17:13
閱讀 2569·2019-08-29 13:07
閱讀 867·2019-08-26 18:26
閱讀 3342·2019-08-26 13:44
閱讀 3210·2019-08-26 13:37