NIO中的內存映射和零拷貝 及Netty中的零拷貝

傳統IO

在這裏插入圖片描述

這傳統io讀取數據併發送的流程圖。

在這裏插入圖片描述
這傳統io讀取數據併發送的過程中發生的上下文切換過程,與拷貝的對應過程。

1、DMA模塊從磁盤中讀取文件內容,內核通過sys_read()(或等價的方法)從文件讀取數據,並將其存儲在內核空間的緩衝區內,完成了第1次複製。(系統調用read導致了從用戶空間到內核空間的上下文切換。)
2、數據從內核空間緩衝區複製到用戶空間緩衝區,read()方法返回導致上下文從內核態切換到用戶態。此時,需要的數據已存放在指定的用戶空間緩衝區內(參數tmp_buf)。

3、write()調用導致上下文從用戶態切換到內核態。第三次拷貝數據從用戶空間重新拷貝到內核空間緩衝區。但是,這一次,數據被寫入一個不同的緩衝區,一個與目標套接字相關聯的緩衝區。
4、 系統調用返回,導致了第4次上下文切換。第4次複製在DMA模塊將數據從內核空間緩衝區傳遞至協議引擎的時候發生,這與我們的代碼的執行是獨立且異步發生的。

sendFile

sendfile系統調用在內核版本2.1中被引入,目的是簡化通過網絡在兩個本地文件之間進行的數據傳輸過程。sendfile系統調用的引入,不僅減少了數據複製,還減少了上下文切換的次數。FileChannel 的write 和 read 方法均是線程安全的,實現了數據直接從內核的讀緩衝區傳輸到套接字緩衝區,避免了用戶態(User-space) 與內核態(Kernel-space) 之間的數據拷貝。它內部通過一把 private final Object positionLock = new Object(); 鎖來控制併發。

在這裏插入圖片描述
sendFile()在linux 2.1 到2.4 之間的 系統調用流程圖。

在這裏插入圖片描述
transferTo方法調用觸發DMA引擎將文件上下文信息拷貝到內核讀緩衝區,接着內核將數據從內核緩衝區拷貝到與套接字相關聯的緩衝區。
DMA引擎將數據從內核套接字緩衝區傳輸到協議引擎(第三次數據拷貝)。

在內核版本2.4中,socket緩衝區描述符結構發生了改動,以適應聚合操作的要求——這就是Linux中所謂的"零拷貝“。這種方式不僅減少了多個上下文切換,而且消除了數據冗餘。從用戶層應用程序的角度來開,沒有發生任何改動,所有代碼仍然是類似下面的形式:sendfile(socket, file, len);
在這裏插入圖片描述

linux 2.4 以後 執行sendFile() 的流程。

在這裏插入圖片描述

linux 2.4 以後 執行sendFile() 對應執行過程以及對應上下文切換。

1、sendFile()方法調用觸發DMA引擎將文件上下文信息拷貝到內核緩衝區。
2、數據不會被拷貝到套接字緩衝區,只有數據的描述符(包括數據位置和長度)被拷貝到套接字緩衝區。DMA 引擎直接將數據從內核緩衝區拷貝到協議引擎,這樣減少了最後一次需要消耗CPU的拷貝操作。

示例代碼:

//    public static void main(String[] args) throws IOException {
//        long startTime = System.currentTimeMillis();
//       File toFile = new File("C:\\Users\\Administrator\\Desktop\\fileTest\\xcd_buffer.zip");
//        File fromFile = new File("C:\\Users\\Administrator\\Downloads\\xcd.zip");
//        /*  fileCopyWithFileChannel(fromFile,toFile);*/
//        bufferedCopy(fromFile, toFile);
//        long endTime = System.currentTimeMillis();
//        System.out.println(endTime - startTime);
//    }


    /**
     * fileChannel進行文件複製(零拷貝)
     *
     * @param fromFile 源文件
     * @param toFile   目標文件
     */
    public static void fileCopyWithTransfer(File fromFile, File toFile) {
        try (
                // 得到fileInputStream的文件通道
             FileChannel fileChannelInput = new FileInputStream(fromFile).getChannel();
             // 得到fileOutputStream的文件通道
             FileChannel fileChannelOutput = new FileOutputStream(toFile).getChannel()) {

            //將fileChannelInput通道的數據,寫入到fileChannelOutput通道
            fileChannelInput.transferTo(0, fileChannelInput.size(), fileChannelOutput);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    static final int BUFFER_SIZE = 1024;
    /**
     * BufferedInputStream進行文件複製(用作對比實驗)
     *
     * @param fromFile 源文件
     * @param toFile   目標文件
     */
    public static void bufferedCopy(File fromFile,File toFile)  {
        try(BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fromFile));
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(toFile))){
            byte[] buf = new byte[BUFFER_SIZE];
            while ((bis.read(buf)) != -1) {
                bos.write(buf);
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }

其實可以看到在使用java 調用的時候,是直接使用transferTo 而不是sendfile(socket, file, len);。那我這裏通過transferTo查看這個調用過程。

 /* * natures and states of the channels.  Fewer than the requested number of
     * bytes are transferred if this channel's file contains fewer than
     * <tt>count</tt> bytes starting at the given <tt>position</tt>, or if the
     * target channel is non-blocking and it has fewer than <tt>count</tt>
     * bytes free in its output buffer.
     *
     * <p> This method does not modify this channel's position.  If the given
     * position is greater than the file's current size then no bytes are
     * transferred.  If the target channel has a position then bytes are
     * written starting at that position and then the position is incremented
     * by the number of bytes written.
     *
     * <p> This method is potentially much more efficient than a simple loop
     * that reads from this channel and writes to the target channel.  Many
     * operating systems can transfer bytes directly from the filesystem cache
     * to the target channel without actually copying them.  </p>
     *
     */
    public abstract long transferTo(long position, long count,
                                    WritableByteChannel target)
        throws IOException;

從上面看
Many operating systems can transfer bytes directly from the filesystem cache
to the target channel without actually copying the 從上面看出,當系統支持零拷貝的,這個纔會支持。不然也同樣會走傳統方式。

public long transferTo(long var1, long var3, WritableByteChannel var5) throws IOException {
        this.ensureOpen();
     。。。(省略部分代碼)
     // 如果內核支持,採用直接傳送的方式
                if ((var9 = this.transferToDirectly(var1, var8, var5)) >= 0L) {
                    return var9;
                } else {
                //this.transferToTrustedChannel(var1, (long)var8, var5))    嘗試使用mmap傳送方式
                    return (var9 = this.transferToTrustedChannel(var1, (long)var8, var5)) >= 0L ? var9 :
                    //傳統的傳送方式
                     this.transferToArbitraryChannel(var1, var8, var5);
                }
            }
        } else {
            throw new IllegalArgumentException();
        }
    }
 private long transferToDirectly(long var1, int var3, WritableByteChannel var4) throws IOException {
      。。。。//省略部分代碼
            if (var5 == null) {
                return -4L;
            } else {
                int var19 = IOUtil.fdVal(this.fd);
                int var7 = IOUtil.fdVal(var5);
                if (var19 == var7) {
                    return -4L;
                } else if (this.nd.transferToDirectlyNeedsPositionLock()) {
                    Object var8 = this.positionLock;
                    synchronized(this.positionLock) {
                        long var9 = this.position();

                        long var11;
                        try {
                        //進行只真正文件傳輸
                            var11 = this.transferToDirectlyInternal(var1, var3, var4, var5);
                        } finally {
                            this.position(var9);
                        }

                        return var11;
                    }
                } else {
                 //進行只真正文件傳輸
                    return this.transferToDirectlyInternal(var1, var3, var4, var5);
                }
            }
        }
    }
 private long transferToDirectlyInternal(long var1, int var3, WritableByteChannel var4, FileDescriptor var5) throws IOException {
        assert !this.nd.transferToDirectlyNeedsPositionLock() || Thread.holdsLock(this.positionLock);

。。。//省略部分代碼(可以看到,java實際調用是transferTo0)
            do {
                var6 = this.transferTo0(this.fd, var1, (long)var3, var5);
            } while(var6 == -3L && this.isOpen());

           。。。

        return var9;
    }

最終transferTo()方法還是需要委託給native的方法transferTo0()來完成調用,此方法的源碼依然在FileChannelImpl.c中:

JNIEXPORT jlong JNICALL
Java_sun_nio_ch_FileChannelImpl_transferTo0(JNIEnv *env, jobject this,
                                            jobject srcFDO,
                                            jlong position, jlong count,
                                            jobject dstFDO)
{
    jint srcFD = fdval(env, srcFDO);
    jint dstFD = fdval(env, dstFDO);

#if defined(__linux__)
    off64_t offset = (off64_t)position;
    // 內部確實是sendfile()系統調用
    jlong n = sendfile64(dstFD, srcFD, &offset, (size_t)count);
   。。。
    return n;
#elif defined (__solaris__)
    sendfilevec64_t sfv;
    size_t numBytes = 0;
    jlong result;

    sfv.sfv_fd = srcFD;
    sfv.sfv_flag = 0;
    sfv.sfv_off = (off64_t)position;
    sfv.sfv_len = count;
    // 內部確實是sendfile()系統調用
    result = sendfilev64(dstFD, &sfv, 1, &numBytes);

    /* Solaris sendfilev() will return -1 even if some bytes have been
     * transferred, so we check numBytes first.
     */
。。。
    return result;

mmap

它可以將一段用戶空間內存映射到內核空間, 當映射成功後, 用戶對這段內存區域的修改可以直接反映到內核空間;同樣地, 內核空間對這段區域的修改也直接反映用戶空間。省去了從內核緩衝區複製到用戶空間的過程,文件中的位置在虛擬內存中有了對應的地址,可以像操作內存一樣操作這個文件,這樣的文件讀寫文件方式少了數據從內核緩存到用戶空間的拷貝,效率很高。

tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);
在這裏插入圖片描述
mmp的流程圖。
在這裏插入圖片描述
mmp中上下文切換流程圖。

1、mmap系統調用導致文件的內容通過DMA模塊被複制到內核緩衝區中,該緩衝區之後與用戶進程共享,這樣就內核緩衝區與用戶緩衝區之間的複製就不會發生。

2、.write系統調用導致內核將數據從內核緩衝區複製到與socket相關聯的內核緩衝區中。

3、 DMA模塊將數據由socket的緩衝區傳遞給協議引擎時,第3次複製發生。

MMAP 使用時必須實現指定好內存映射的大小,mmap 在 Java 中一次只能映射 1.5~2G 的文件內存,其中RocketMQ 中限制了單文件1G來避免這個問題
MMAP 可以通過 force() 來手動控制,但控制不好也會有大麻煩
MMAP 的回收問題,當 MappedByteBuffer 不再需要時,可以手動釋放佔用的虛擬內存,但使用方式非常的麻煩

示例代碼:
寫:

public static void main(String[] args) {
 File file = new File("C:\\Users\\Administrator\\Desktop\\fileTest\\a.txt");
    try (FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel();) {
        //MappedByteBuffer 便是MMAP的操作類(獲得一個 1.5k 的文件)
        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, (int)(1.5 * 1024));
// write
       // byte[] data = new byte[];
        byte[] data = new String("你們好123").getBytes("utf-8");
        System.out.println(data.length);
        int position = 8;
//從當前 mmap 指針的位置寫入 的數據
        mappedByteBuffer.put(data);
//指定 position 寫入 數據
        //Creates a new byte buffer whose content is a shared subsequence of
        //     this buffer's content.
        MappedByteBuffer subBuffer = (MappedByteBuffer) mappedByteBuffer.slice();
        subBuffer.position(position);
        subBuffer.put(data);



    } catch (Exception e) {
        e.printStackTrace();
    }
}

讀:

public static void main(String[] args) {

    File file = new File("C:\\Users\\Administrator\\Desktop\\fileTest\\a.txt");
    try (FileChannel fileChannel = new RandomAccessFile(file, "r").getChannel();) {
        //MappedByteBuffer 便是MMAP的操作類(獲得一個 1.5k 的文件)
        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, (int)(1.5 * 1024));
// write
         byte[] data = new byte[12];

        int position = 8+12;
//從當前 mmap 指針的位置寫入 的數據
//指定 position 寫入 數據
        //Creates a new byte buffer whose content is a shared subsequence of
        //     this buffer's content.
        MappedByteBuffer subBuffer = (MappedByteBuffer) mappedByteBuffer.slice();
        subBuffer.position(position);
        subBuffer.get(data);

        System.out.println(new String(data, "utf-8"));

    } catch (Exception e) {
        e.printStackTrace();
    }
}
 public static void fileReadWithMmap(File fileIn,File fileOut) {

        long begin = System.currentTimeMillis();
        byte[] b = new byte[BUFFER_SIZE];
        int len = (int) fileIn.length();
        try ( FileChannel channelIn=new RandomAccessFile(fileIn, "r").getChannel();
              FileChannel channelOut=new RandomAccessFile(fileOut, "rw").getChannel();) {
            // 將文件所有字節映射到內存中。返回MappedByteBuffer
            MappedByteBuffer mappedByteBufferInt = channelIn.map(FileChannel.MapMode.READ_ONLY, 0, len);
            MappedByteBuffer mappedByteBufferOut =channelOut.map(FileChannel.MapMode.READ_WRITE, 0, len);

            for (int offset = 0; offset < len; offset += BUFFER_SIZE) {
                if (len - offset > BUFFER_SIZE) {
                    mappedByteBufferInt.get(b);
                    mappedByteBufferOut.put(b);
                } else {
                    byte[] bytes = new byte[len - offset];
                    mappedByteBufferInt.get(bytes);
                    mappedByteBufferOut.put(bytes);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("time is:" + (end - begin));
    }

我們可以看到我mmap在代碼裏面調用map()方法。我們這裏查看map的源碼分析

public MappedByteBuffer map(MapMode var1, long var2, long var4) throws IOException {
        this.ensureOpen();
    。。。(省略部分代碼)

  

                        if (var4 != 0L) {
                            var12 = (int)(var2 % allocationGranularity);
                            long var34 = var2 - (long)var12;
                            long var15 = var4 + (long)var12;

                            try {
                            // 實際調用的是調用map0方法
                                var7 = this.map0(var6, var34, var15);
                            } catch (OutOfMemoryError var30) {
                                System.gc();

                            。。。(省略部分代碼)

                return (MappedByteBuffer)var10;
            }
        }
    }
JNIEXPORT jlong JNICALL
Java_sun_nio_ch_FileChannelImpl_map0(JNIEnv *env, jobject this,
                                     jint prot, jlong off, jlong len)
{
   。。。(省略部分代碼)
    // 內部果然是通過mmap系統調用來實現的
    mapAddress = mmap64(
        0,                    /* Let OS decide location */
        len,                  /* Number of bytes to map */
        protections,          /* File permissions */
        flags,                /* Changes are shared */
        fd,                   /* File descriptor of mapped file */
        off);                 /* Offset into file */

    if (mapAddress == MAP_FAILED) {
        if (errno == ENOMEM) {
            JNU_ThrowOutOfMemoryError(env, "Map failed");
            return IOS_THROWN;
        }
        return handle(env, -1, "Map failed");
    }

    return ((jlong) (unsigned long) mapAddress);
}

Netty 通過CompositeByteBuf實現零拷貝之Buffer合併

在這裏插入圖片描述

兩個真實的buffer,邏輯合併成一個CompositeByteBuf,但CompositeByteBuf並只是指向原來真實的兩個buffer,而只是兩個buffer邏輯上合併成的一個數組。

示例代碼:

    @Test
    public void compositeTest() {
        ByteBuf buffer1 = Unpooled.buffer(3);
        buffer1.writeByte(1);
        ByteBuf buffer2 = Unpooled.buffer(3);
        buffer2.writeByte(4);
        CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer();
        CompositeByteBuf newBuffer = compositeByteBuf.addComponents(true, buffer1, buffer2);
        System.out.println(newBuffer);
    }

在這裏插入圖片描述
可以看出實際components 代表實際這個兩個實際buffer組合而成。

Netty 通過通過slice操作實現零拷貝之Buffer拆分

用slice方法產生buffer的過程是沒有拷貝操作的,兩個buffer對象在內部其實是共享了byteBuf存儲空間的不同部分而已 。
在這裏插入圖片描述

示例代碼:

  @Test
    public void sliceTest() {
        ByteBuf buffer1 = Unpooled.wrappedBuffer("你好123".getBytes());
        ByteBuf newBuffer = buffer1.slice(1, 2);
        //ByteBuf newBuffer = buffer1.slice();
        newBuffer.unwrap();
        System.out.println(newBuffer.toString());
    }

在這裏插入圖片描述

可以看出整個完整buffer大小爲9,但獲取到拆分之後buffer爲2,拆分之後的buffer依然指向的原來的buffer

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