摘要:維基百科中對(duì)的解釋是零拷貝技術(shù)是指計(jì)算機(jī)執(zhí)行操作時(shí),不需要先將數(shù)據(jù)從某處內(nèi)存復(fù)制到另一個(gè)特定區(qū)域。維基百科里提到的零拷貝是在硬件和操作系統(tǒng)層面的,而本文主要介紹的是在應(yīng)用層面的優(yōu)化。
維基百科中對(duì) Zero-copy 的解釋是
零拷貝技術(shù)是指計(jì)算機(jī)執(zhí)行操作時(shí),CPU不需要先將數(shù)據(jù)從某處內(nèi)存復(fù)制到另一個(gè)特定區(qū)域。這種技術(shù)通常用于通過(guò)網(wǎng)絡(luò)傳輸文件時(shí)節(jié)省CPU周期和內(nèi)存帶寬。
維基百科里提到的零拷貝是在硬件和操作系統(tǒng)層面的,而本文主要介紹的是Netty在應(yīng)用層面的優(yōu)化。不過(guò)需要注意的是,零拷貝并非字面意義上的沒(méi)有內(nèi)存拷貝,而是避免多余的拷貝操作,即使是系統(tǒng)層的零拷貝也有從設(shè)備到內(nèi)存,內(nèi)存到設(shè)備的數(shù)據(jù)拷貝過(guò)程。
Netty 的零拷貝體現(xiàn)在以下幾個(gè)方面
ByteBuf 的 slice 操作并不會(huì)拷貝一份新的 ByteBuf 內(nèi)存空間,而是直接借用原來(lái)的 ByteBuf ,只是獨(dú)立地保存讀寫(xiě)索引。
Netty 提供了 CompositeByteBuf 類(lèi),可以將多個(gè) ByteBuf 組合成一個(gè)邏輯上的 ByteBuf 。
Netty 的 FileRegion 中包裝了 NIO 的 FileChannel.transferTo()方法,該方法在底層系統(tǒng)支持的情況下會(huì)調(diào)用 sendfile 方法,從而在傳輸文件時(shí)避免了用戶(hù)態(tài)的內(nèi)存拷貝。
Netty 的 PooledDirectByteBuf 等類(lèi)中封裝了 NIO 的 DirectByteBuffer ,而 DirectByteBuffer 是直接在 jvm 堆外分配的內(nèi)存,省去了堆外內(nèi)存向堆內(nèi)存拷貝的開(kāi)銷(xiāo)。
下面來(lái)簡(jiǎn)單介紹下這幾種方式。
slice以下以 AbstractUnpooledSlicedByteBuf 為例講解 slice 的零拷貝原理,至于內(nèi)存池化的實(shí)現(xiàn) PooledSlicedByteBuf ,因?yàn)閮?nèi)存池要通過(guò)引用計(jì)數(shù)來(lái)控制內(nèi)存的釋放,所以代碼里會(huì)出現(xiàn)很多與本文主題無(wú)關(guān)的邏輯,這里就不拿來(lái)舉栗子了。
// 切片ByteBuf的構(gòu)造函數(shù),其中字段adjustment為切片ByteBuf相對(duì)于被切片ByteBuf的偏移 // 量,兩個(gè)ByteBuf共用一塊內(nèi)存空間,字段buffer為實(shí)際存儲(chǔ)數(shù)據(jù)的ByteBuf AbstractUnpooledSlicedByteBuf(ByteBuf buffer, int index, int length) { super(length); checkSliceOutOfBounds(index, length, buffer);//檢查slice是否越界 if (buffer instanceof AbstractUnpooledSlicedByteBuf) { // 如果被切片ByteBuf也是AbstractUnpooledSlicedByteBuf對(duì)象 this.buffer = ((AbstractUnpooledSlicedByteBuf) buffer).buffer; adjustment = ((AbstractUnpooledSlicedByteBuf) buffer).adjustment + index; } else if (buffer instanceof DuplicatedByteBuf) { // 如果被切片ByteBuf為DuplicatedByteBuf對(duì)象,則 // 用unwrap得到實(shí)際存儲(chǔ)數(shù)據(jù)的ByteBuf賦值buffer this.buffer = buffer.unwrap(); adjustment = index; } else { // 如果被切片ByteBuf為一般ByteBuf對(duì)象,則直接賦值buffer this.buffer = buffer; adjustment = index; } initLength(length); writerIndex(length); }
以上為 AbstractUnpooledSlicedByteBuf 類(lèi)的構(gòu)造函數(shù),比較簡(jiǎn)單,就不詳細(xì)介紹了。
下面來(lái)看看 AbstractUnpooledSlicedByteBuf 對(duì) ByteBuf 接口的實(shí)現(xiàn)代碼,以 getBytes 方法為例:
@Override public ByteBuf getBytes(int index, ByteBuffer dst) { checkIndex0(index, dst.remaining());//檢查是否越界 unwrap().getBytes(idx(index), dst); return this; } @Override public ByteBuf unwrap() { return buffer; } private int idx(int index) { return index + adjustment; }
這是 AbstractUnpooledSlicedByteBuf 重載的 getBytes 方法,可以看到 AbstractUnpooledSlicedByteBuf 是直接在封裝的 ByteBuf 上取的字節(jié),但是重新計(jì)算了索引,加上了相對(duì)偏移量。
CompositeByteBuf在有些場(chǎng)景里,我們的數(shù)據(jù)會(huì)分散在多個(gè) ByteBuf 上,但是我們又希望將這些 ByteBuf 聚合在一個(gè) ByteBuf 里處理。這里最直觀的想法是將所有 ByteBuf 的數(shù)據(jù)拷貝到一個(gè) ByteBuf 上,但是這樣會(huì)有大量的內(nèi)存拷貝操作,產(chǎn)生很大的CPU開(kāi)銷(xiāo)。
而 CompositeByteBuf 可以很好地解決這個(gè)問(wèn)題,正如名字一樣,這是一個(gè)復(fù)合 ByteBuf ,內(nèi)部由很多的 ByteBuf 組成,但 CompositeByteBuf 給它們做了一層封裝,可以直接以 ByteBuf 的接口操作它們。
/** * Precondition is that {@code buffer != null}. */ private int addComponent0(boolean increaseWriterIndex, int cIndex, ByteBuf buffer) { assert buffer != null; boolean wasAdded = false; try { // 檢查新增的component的索引是否合法 checkComponentIndex(cIndex); // buffer的長(zhǎng)度 int readableBytes = buffer.readableBytes(); // No need to consolidate - just add a component to the list. @SuppressWarnings("deprecation") // 統(tǒng)一為大端ByteBuf Component c = new Component(buffer.order(ByteOrder.BIG_ENDIAN).slice()); if (cIndex == components.size()) { // 如果索引等于components的大小,則加在components尾部 wasAdded = components.add(c); if (cIndex == 0) { // 如果components中只有一個(gè)元素 c.endOffset = readableBytes; } else { // 如果components中有多個(gè)元素 Component prev = components.get(cIndex - 1); c.offset = prev.endOffset; c.endOffset = c.offset + readableBytes; } } else { // 如果新的ByteBuf是插在components中間 components.add(cIndex, c); wasAdded = true; if (readableBytes != 0) { // 如果components的大小不為0,則依次更新cIndex之后的 // 所有components的offset和endOffset updateComponentOffsets(cIndex); } } if (increaseWriterIndex) { // 如果要更新writerIndex writerIndex(writerIndex() + buffer.readableBytes()); } return cIndex; } finally { if (!wasAdded) { // 如果沒(méi)添加成功,則釋放ByteBuf buffer.release(); } } }
這是添加一個(gè)新的 ByteBuf 的邏輯,核心是 offset 和 endOffset ,分別指代一個(gè) ByteBuf 在 CompositeByteBuf 中開(kāi)始和結(jié)束的索引,它們唯一標(biāo)記了這個(gè) ByteBuf 在 CompositeByteBuf 中的位置。
弄清楚了這個(gè),我們會(huì)發(fā)現(xiàn)上面的代碼無(wú)外乎做了兩件事:
把 ByteBuf 封裝成 Component 加到 components 合適的位置上
使 components 里的每個(gè) Component 的 offset 和 endOffset 值都正確
下面來(lái)看看 CompositeByteBuf 對(duì) ByteBuf 接口的實(shí)現(xiàn)代碼,同樣以 getBytes 方法為例:
@Override public CompositeByteBuf getBytes(int index, ByteBuf dst, int dstIndex, int length) { // 查索引是否越界 checkDstIndex(index, length, dstIndex, dst.capacity()); if (length == 0) { return this; } // 用二分搜索查找index對(duì)應(yīng)的Component在components中的索引 int i = toComponentIndex(index); // 循環(huán)讀直至length為0 while (length > 0) { Component c = components.get(i); ByteBuf s = c.buf; int adjustment = c.offset; // 取length和ByteBuf剩余字節(jié)數(shù)中的較小值 int localLength = Math.min(length, s.capacity() - (index - adjustment)); // 開(kāi)始索引為index - c.offset,而不是0 s.getBytes(index - adjustment, dst, dstIndex, localLength); index += localLength; dstIndex += localLength; length -= localLength; i ++; } return this; } /** * Return the index for the given offset */ public int toComponentIndex(int offset) { checkIndex(offset); for (int low = 0, high = components.size(); low <= high;) { int mid = low + high >>> 1; Component c = components.get(mid); if (offset >= c.endOffset) { low = mid + 1; } else if (offset < c.offset) { high = mid - 1; } else { return mid; } } throw new Error("should not reach here"); }
可以看到 CompositeByteBuf 在處理 index 時(shí)是先將其轉(zhuǎn)換成對(duì)應(yīng) Component 在 components 中的索引,以及在 Component 中的偏移,然后從這個(gè) Component 的這個(gè)偏移開(kāi)始,往后循環(huán)取字節(jié),直到讀完。
NOTE:這里有個(gè)小trick,因?yàn)?components 是有序排列的,所以 toComponentIndex 做索引轉(zhuǎn)換時(shí)沒(méi)有直接遍歷,而是用的二分查找。
今天寫(xiě)得有點(diǎn)累了,這里留個(gè)坑,下一篇再填上。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/67904.html
摘要:系統(tǒng)調(diào)用返回,產(chǎn)生了第四次上下文切換?,F(xiàn)在這個(gè)方法不僅減少了上下文切換,而且消除了參與的數(shù)據(jù)拷貝。 上一篇說(shuō)到了 CompositeByteBuf ,這一篇接著上篇的講下去。 FileRegion 讓我們先看一個(gè)Netty官方的example // netty-netty-4.1.16.Finalexamplesrcmainjavaio ettyexamplefileFileServe...
摘要:根據(jù)對(duì)的定義即所謂的就是在操作數(shù)據(jù)時(shí)不需要將數(shù)據(jù)從一個(gè)內(nèi)存區(qū)域拷貝到另一個(gè)內(nèi)存區(qū)域因?yàn)樯倭艘淮蝺?nèi)存的拷貝因此的效率就得到的提升在層面上的通常指避免在用戶(hù)態(tài)與內(nèi)核態(tài)之間來(lái)回拷貝數(shù)據(jù)例如提供的系統(tǒng)調(diào)用它可以將一段用戶(hù)空間內(nèi)存映射到內(nèi) 根據(jù) Wiki 對(duì) Zero-copy 的定義: Zero-copy describes computer operations in which the C...
摘要:一旦某個(gè)事件觸發(fā),相應(yīng)的則會(huì)被調(diào)用,并進(jìn)行處理。事實(shí)上,內(nèi)部的連接處理協(xié)議編解碼超時(shí)等機(jī)制,都是通過(guò)完成的。開(kāi)啟源碼之門(mén)理解了的事件驅(qū)動(dòng)機(jī)制,我們現(xiàn)在可以來(lái)研究的各個(gè)模塊了。 Netty是什么 大概用Netty的,無(wú)論新手還是老手,都知道它是一個(gè)網(wǎng)絡(luò)通訊框架。所謂框架,基本上都是一個(gè)作用:基于底層API,提供更便捷的編程模型。那么通訊框架到底做了什么事情呢?回答這個(gè)問(wèn)題并不太容易,我們...
摘要:目錄源碼之下無(wú)秘密做最好的源碼分析教程源碼分析之番外篇的前生今世的前生今世之一簡(jiǎn)介的前生今世之二小結(jié)的前生今世之三詳解的前生今世之四詳解源碼分析之零磨刀不誤砍柴工源碼分析環(huán)境搭建源碼分析之一揭開(kāi)神秘的紅蓋頭源碼分析之一揭開(kāi)神秘的紅蓋頭客戶(hù)端 目錄 源碼之下無(wú)秘密 ── 做最好的 Netty 源碼分析教程 Netty 源碼分析之 番外篇 Java NIO 的前生今世 Java NI...
閱讀 2798·2021-11-04 16:15
閱讀 3476·2021-09-29 09:35
閱讀 4067·2021-09-22 15:45
閱讀 1426·2019-08-30 15:55
閱讀 1700·2019-08-30 15:44
閱讀 2737·2019-08-29 12:56
閱讀 2708·2019-08-26 13:30
閱讀 2183·2019-08-23 17:00