1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        面試官:談談你對零拷貝的理解~

        共 16853字,需瀏覽 34分鐘

         ·

        2021-03-31 23:11

        點擊上方 好好學java ,選擇 星標 公眾號

        重磅資訊,干貨,第一時間送達

        今日推薦:14 個 github 項目!

        個人原創(chuàng)100W +訪問量博客:點擊前往,查看更多

        前言

        從字面意思理解就是數(shù)據(jù)不需要來回的拷貝,大大提升了系統(tǒng)的性能;這個詞我們也經(jīng)常在java nio,netty,kafka,RocketMQ等框架中聽到,經(jīng)常作為其提升性能的一大亮點;下面從I/O的幾個概念開始,進而在分析零拷貝。

        I/O概念

        1.緩沖區(qū)

        緩沖區(qū)是所有I/O的基礎,I/O講的無非就是把數(shù)據(jù)移進或移出緩沖區(qū);進程執(zhí)行I/O操作,就是向操作系統(tǒng)發(fā)出請求,讓它要么把緩沖區(qū)的數(shù)據(jù)排干(寫),要么填充緩沖區(qū)(讀);下面看一個java進程發(fā)起read請求加載數(shù)據(jù)大致的流程圖:

        圖片

        進程發(fā)起read請求之后,內(nèi)核接收到read請求之后,會先檢查內(nèi)核空間中是否已經(jīng)存在進程所需要的數(shù)據(jù),如果已經(jīng)存在,則直接把數(shù)據(jù)copy給進程的緩沖區(qū);如果沒有內(nèi)核隨即向磁盤控制器發(fā)出命令,要求從磁盤讀取數(shù)據(jù),磁盤控制器把數(shù)據(jù)直接寫入內(nèi)核read緩沖區(qū),這一步通過DMA完成;

        接下來就是內(nèi)核將數(shù)據(jù)copy到進程的緩沖區(qū);

        如果進程發(fā)起write請求,同樣需要把用戶緩沖區(qū)里面的數(shù)據(jù)copy到內(nèi)核的socket緩沖區(qū)里面,然后再通過DMA把數(shù)據(jù)copy到網(wǎng)卡中,發(fā)送出去;

        你可能覺得這樣挺浪費空間的,每次都需要把內(nèi)核空間的數(shù)據(jù)拷貝到用戶空間中,所以零拷貝的出現(xiàn)就是為了解決這種問題的;

        關于零拷貝提供了兩種方式分別是:mmap+write方式,sendfile方式;

        2.虛擬內(nèi)存

        所有現(xiàn)代操作系統(tǒng)都使用虛擬內(nèi)存,使用虛擬的地址取代物理地址,這樣做的好處是:

        1. 一個以上的虛擬地址可以指向同一個物理內(nèi)存地址,

        2. 虛擬內(nèi)存空間可大于實際可用的物理地址;

        利用第一條特性可以把內(nèi)核空間地址和用戶空間的虛擬地址映射到同一個物理地址,這樣DMA就可以填充對內(nèi)核和用戶空間進程同時可見的緩沖區(qū)了,大致如下圖所示:

        圖片

        省去了內(nèi)核與用戶空間的往來拷貝,java也利用操作系統(tǒng)的此特性來提升性能,下面重點看看java對零拷貝都有哪些支持。

        3.mmap+write方式

        使用mmap+write方式代替原來的read+write方式,mmap是一種內(nèi)存映射文件的方法,即將一個文件或者其它對象映射到進程的地址空間,實現(xiàn)文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對映關系;這樣就可以省掉原來內(nèi)核read緩沖區(qū)copy數(shù)據(jù)到用戶緩沖區(qū),但是還是需要內(nèi)核read緩沖區(qū)將數(shù)據(jù)copy到內(nèi)核socket緩沖區(qū),大致如下圖所示:

        圖片

        4.sendfile方式

        sendfile系統(tǒng)調(diào)用在內(nèi)核版本2.1中被引入,目的是簡化通過網(wǎng)絡在兩個通道之間進行的數(shù)據(jù)傳輸過程。sendfile系統(tǒng)調(diào)用的引入,不僅減少了數(shù)據(jù)復制,還減少了上下文切換的次數(shù),大致如下圖所示:

        圖片

        數(shù)據(jù)傳送只發(fā)生在內(nèi)核空間,所以減少了一次上下文切換;但是還是存在一次copy,能不能把這一次copy也省略掉,Linux2.4內(nèi)核中做了改進,將Kernel buffer中對應的數(shù)據(jù)描述信息(內(nèi)存地址,偏移量)記錄到相應的socket緩沖區(qū)當中,這樣連內(nèi)核空間中的一次cpu copy也省掉了;推薦:250期面試題匯總

        Java零拷貝

        1.MappedByteBuffer

        java nio提供的FileChannel提供了map()方法,該方法可以在一個打開的文件和MappedByteBuffer之間建立一個虛擬內(nèi)存映射,MappedByteBuffer繼承于ByteBuffer,類似于一個基于內(nèi)存的緩沖區(qū),只不過該對象的數(shù)據(jù)元素存儲在磁盤的一個文件中;調(diào)用get()方法會從磁盤中獲取數(shù)據(jù),此數(shù)據(jù)反映該文件當前的內(nèi)容,調(diào)用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()來實現(xiàn)映射,map()方法如下:

        public abstract MappedByteBuffer map(MapMode mode, long position, long size) throws IOException;  

        分別提供了三個參數(shù),MapMode,Position和size;分別表示:

        • MapMode:映射的模式,可選項包括:READ_ONLY,READ_WRITE,PRIVATE;

        • Position:從哪個位置開始映射,字節(jié)數(shù)的位置;

        • Size:從position開始向后多少個字節(jié);

        重點看一下MapMode,請兩個分別表示只讀和可讀可寫,當然請求的映射模式受到Filechannel對象的訪問權限限制,如果在一個沒有讀權限的文件上啟用READ_ONLY,將拋出NonReadableChannelException;PRIVATE模式表示寫時拷貝的映射,意味著通過put()方法所做的任何修改都會導致產(chǎn)生一個私有的數(shù)據(jù)拷貝并且該拷貝中的數(shù)據(jù)只有MappedByteBuffer實例可以看到;該過程不會對底層文件做任何修改,而且一旦緩沖區(qū)被施以垃圾收集動作(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方法獲取內(nèi)存映射的地址,如果失敗,手動gc再次映射;最后通過內(nèi)存映射的地址實例化出MappedByteBuffer,MappedByteBuffer本身是一個抽象類,其實這里真正實例話出來的是DirectByteBuffer;

        2.DirectByteBuffer

        DirectByteBuffer繼承于MappedByteBuffer,從名字就可以猜測出開辟了一段直接的內(nèi)存,并不會占用jvm的內(nèi)存空間;上一節(jié)中通過Filechannel映射出的MappedByteBuffer其實際也是DirectByteBuffer,當然除了這種方式,也可以手動開辟一段空間:

        ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(100);  

        如上開辟了100字節(jié)的直接內(nèi)存空間;推薦:250期面試題匯總

        3.Channel-to-Channel傳輸

        經(jīng)常需要從一個位置將文件傳輸?shù)搅硗庖粋€位置,F(xiàn)ileChannel提供了transferTo()方法用來提高傳輸?shù)男?,首先看一個簡單的實例:

        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()方法將文件數(shù)據(jù)傳輸?shù)絊ystem.out通道,接口定義如下:

        public abstract long transferTo(long position, long count, WritableByteChannel target) throws IOException;  

        幾個參數(shù)也比較好理解,分別是開始傳輸?shù)奈恢?,傳輸?shù)淖止?jié)數(shù),以及目標通道;transferTo()允許將一個通道交叉連接到另一個通道,而不需要一個中間緩沖區(qū)來傳遞數(shù)據(jù);

        注:這里不需要中間緩沖區(qū)有兩層意思:第一層不需要用戶空間緩沖區(qū)來拷貝內(nèi)核緩沖區(qū),另外一層兩個通道都有自己的內(nèi)核緩沖區(qū),兩個內(nèi)核緩沖區(qū)也可以做到無需拷貝數(shù)據(jù);

        Netty零拷貝

        netty提供了零拷貝的buffer,在傳輸數(shù)據(jù)時,最終處理的數(shù)據(jù)會需要對單個傳輸?shù)膱笪模M行組合和拆分,Nio原生的ByteBuffer無法做到,netty通過提供的Composite(組合)和Slice(拆分)兩種buffer來實現(xiàn)零拷貝;看下面一張圖會比較清晰:

        圖片

        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并不會開辟新的內(nèi)存并直接復制所有ChannelBuffer內(nèi)容,而是直接保存了所有ChannelBuffer的引用,并在子ChannelBuffer里進行讀寫,實現(xiàn)了零拷貝。

        其他零拷貝

        RocketMQ的消息采用順序?qū)懙絚ommitlog文件,然后利用consume queue文件作為索引;RocketMQ采用零拷貝mmap+write的方式來回應Consumer的請求;

        同樣kafka中存在大量的網(wǎng)絡數(shù)據(jù)持久化到磁盤和磁盤文件通過網(wǎng)絡發(fā)送的過程,kafka使用了sendfile零拷貝方式;

        總結

        零拷貝如果簡單用java里面對象的概率來理解的話,其實就是使用的都是對象的引用,每個引用對象的地方對其改變就都能改變此對象,永遠只存在一份對象。

        參考

        《java_nio》

        作者:kosamino

        juejin.cn/post/6844903815913668615

        推薦文章

        更多項目源碼


        瀏覽 42
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            欧美精品性视频 | 国产乱伦a片视频 | 国产精品偷窥熟女精品视频步 | 高清无码内射视频 | 欧美午夜性春猛XXX交 | 蜜臀精品一区二区三区 | 好吊操免费视频 | 日韩欧美一二三区 | 久久草在线视频 | 太粗大了潘金莲受不了桃 |