天天看點

【作業系統】作業系統IO技術底層機制和ZeroCopy

1.DMA技術詳解

(1)應用程式 從 磁盤讀寫資料 的時序圖(未用DMA技術前)

【作業系統】作業系統IO技術底層機制和ZeroCopy

(2)什麼是DMA 技術 (Direct Memory Access)

  • 直接記憶體通路,直接記憶體通路是計算機科學中的一種記憶體通路技術。
  • DMA之前:要把外設的資料讀入記憶體或把記憶體的資料傳送到外設,一般都要通過CPU控制完成,利用中斷技術。
  • 允許某些硬體系統能夠獨立于CPU直接讀寫作業系統的記憶體,不需要中處理器(CPU)介入處理。
  • 資料傳輸操作在一個DM控制器(DMAC)的控制下進行,在傳輸過程中CPU可以繼續進行其他的工作。
  • 在大部分時間CPU和I/O操作都處于并行狀态,系統的效率更高。
【作業系統】作業系統IO技術底層機制和ZeroCopy

(3)應用程式的讀寫資料

【作業系統】作業系統IO技術底層機制和ZeroCopy
  • 讀本地磁盤
    • 作業系統檢查記憶體緩沖區讀取,如果存在則直接把核心空間的資料copy到使用者空間(CPU負責),應用程式即可使用。
    • 上步沒資料,則從磁盤中讀取到核心緩沖(DMA負責),再把核心空間的資料copy到使用者空間(CPU負責),應用程式即可使用
    • 硬碟->核心緩沖區->使用者緩沖區
  • 寫操作本地磁盤
    • 根據作業系統的寫入方式不一樣,buffer IO 和 direct IO ,寫入磁盤時機不一樣。
    • buffer IO
      • 應用程式把資料從使用者空間copy到核心空間的緩沖區(CPU負責),再把核心緩沖區的資料寫到磁盤(DMA負責)。
    • direct IO
      • 應用程式把資料直接從使用者态位址空間寫入到磁盤中,直接跳過核心空間緩沖區。
      • 減少作業系統緩沖區和使用者位址空間的拷貝次數,降低了CPU和記憶體開銷。
    • 使用者緩沖區->核心緩沖區->硬碟
  • 讀網絡資料
    • 網卡Socket(類似磁盤)中讀取用戶端發送的資料到核心空間(DMA負責)。
    • 把核心空間的資料copy到使用者空間(CPU負責),然後應用程式即可使用。
  • 寫網絡資料
    • 使用者緩沖區中的資料copy到核心緩沖區的Socket Buffer 中(CPU負責)
    • 将核心空間中的Socket Buffer 拷貝到Socket協定棧(網卡裝置)進行傳輸(DMA負責)

(4)DMA的工作總結

  • 從磁盤的緩沖區到核心緩沖區的拷貝工作。
  • 從網卡裝置到核心的socket buffer 的拷貝工作。
  • 從核心緩沖區到磁盤緩沖區的拷貝工作。
  • 從核心的socket buffer到網卡裝置的拷貝工作。
  • 注意:核心緩沖區到使用者緩沖區之間的拷貝工作仍然由CPU負責

(5)DMA技術帶來的性能損耗

【作業系統】作業系統IO技術底層機制和ZeroCopy
  • 上圖應用程式從磁盤讀取資料發送到網絡上的損耗,程式需要兩個指令 先read讀取,再write寫出
  • 四次核心态和使用者态的切換
  • 四次緩沖區的拷貝(2次DMA拷貝、2次CPU拷貝)
    • 讀取:磁盤緩沖區到核心緩沖區(DMA)
    • 讀取:核心緩沖區到使用者緩沖區(CPU)
    • 寫出:使用者緩沖區到核心緩沖區Socket Buffer(CPU)
    • 寫出:核心緩沖區的Socket Buffer到網卡裝置(DMA)

為了解決這種性能的損耗是以就誕生了零拷貝。

2.ZeroCopy零拷貝技術簡介

(1)什麼是零拷貝ZeroCopy

​ 減少不必要的核心緩沖區跟使用者緩沖區之間的拷貝工作,進而減少CPU的開銷和減少kernel和user模式的上下文切換,達到性能的提升。從磁盤中讀取檔案通過網絡發送出去,隻需要拷貝2\3次和2\4的核心态和使用者态的切換即可。

ZeroCopy技術實作方式有兩種(核心态和使用者态切換次數不一樣)

  • 方式一:mmap+write
  • 方式二:sendfile

(2)ZeroCopy的實作底層 mmap + write

  • 作業系統都使用虛拟記憶體,虛拟位址通過多級頁表映射實體位址。
  • 多個虛拟記憶體可以指向同一個實體位址,虛拟記憶體的總空間遠大于實體記憶體空間。
  • 如果把核心空間和使用者空間的虛拟位址映射到同一個實體位址,就不需要來回複制資料。
  • mmap系統調用函數會直接把核心緩沖區的資料映射到使用者空間,核心空間和使用者空間就不需要在進行資料拷貝的操作了,節省了CPU開銷。
  • mmap()負責讀取,write()負責寫出
  • 執行流程
    • 應用程式先調用mmap()方法,将資料從磁盤拷貝到核心緩沖區,傳回結束(DMA負責)。在調用write(),核心緩沖區的資料直接拷貝到核心socket buffer (CPU負責),然後把核心緩沖區的Socket Buffer 給直接拷貝給Socket協定線,即網卡裝置中,傳回結束(DMA負責)
【作業系統】作業系統IO技術底層機制和ZeroCopy
  • 采用mmap之後,CPU使用者态和核心态上下文切換依舊是4次和全程有3次資料拷貝
  • 2次DMA拷貝、1次CPU拷貝、4次核心态使用者态切換,減少了1次CPU拷貝

(3)ZeroCopy的實作底層 sendfile

  • Linux kernal 2.1新增發送檔案的系統調用函數sendfile()。
  • 執行流程
    • 替代read()和write()兩個系統調用,減少一次系統調用,即減少2次CPU上下文切換的開銷,調用sendfile(),從磁盤讀取到核心緩沖區,然後直接把核心緩沖區的資料拷貝到socket buffer緩沖區裡,再把核心緩沖區的SocketBuffer給直接拷貝給Socket協定棧,即網卡裝置中(DMA負責)。
【作業系統】作業系統IO技術底層機制和ZeroCopy
  • 采用sendfile後,CPU使用者态和核心态上下文切換是2次 和 全程3次的資料拷貝,2次DMA拷貝、1次的CPU拷貝、2次核心态使用者态切換。
  • Linux 2.4+ 版本之後改進sendfile,利用DMA Gather(帶有收集功能的DMA),變成了真正的零拷貝(沒有CPU Copy)
    • 應用程式先調用sendfile()方法,将資料從磁盤拷貝到核心緩沖區(DMA負責)
    • 把記憶體位址、偏移量的緩沖區fd描述符拷貝到Socket Buffer中去 拷貝很少的資料,可忽略
      • 本質和虛拟記憶體的解決方法思路一樣,就是記憶體位址的記錄
    • 然後把核心緩沖區的Socket Buffer給直接拷貝給Socket協定棧 即網卡裝置中,傳回結束(DMA負責)
【作業系統】作業系統IO技術底層機制和ZeroCopy

3.Java和主流中間件裡的零拷貝技術

(1)Java中有哪些零拷貝技術

  • Java NIO對mmap的實作 fileChannel.map()
  • Java NIO對sendfile的實作 fileChannel.transferTo() 和 fileChannel.transferFrom()

(2)什麼是FileChannel

  • FileChannel是一個連接配接到檔案的通道,可以通過檔案通道讀寫檔案,該常被用于搞笑的網絡/檔案的資料傳輸和大檔案拷貝
  • 應用程式使用FileChannel寫完以後,資料是在PageCache上的,作業系統不定時的把PageCache的資料寫入到磁盤。為了避免當機資料丢失,使用channel.force(true) 把檔案相關的資料強制刷入磁盤上去。
  • 使用之前必須先打開它,但是無法直接new一個FileChannel。
  • 正常通過使用一個InputStream、OutputStream或者RandomAccessFile來擷取一個FileChannel執行個體。
RandomAccessFile randomAccessFile = new RandomAccessFile("檔案路徑","rw");
FileChannel inChannel = randomAccessFile.getChannel();
           

(3)mmap方式實作

  • map方法,把檔案映射成記憶體映射檔案
  • MappedByteBuffer,是抽象類也是ByteBuffer的子類,具體實作子類是DirectByteBuffer,可被通道進行讀寫。
  • 一次map大小要限制在2G内,過大map會增加虛拟記憶體回收和重新配置設定的壓力,直接報錯。
  • FileChannel.java中的map對long size 進行了限制,不能大于Integer.MAX_VALUE,否則就報錯
【作業系統】作業系統IO技術底層機制和ZeroCopy
  • JDK層做限制是因為底層C++的類型,無符号int類型最大是2^31 -1, 2^31 -1 位元組就是 2GB - 1B。
MappedByteBuffer map(int mode,long position,long size)
position:檔案開始位置
size:映射檔案區域大小
mode:通路該記憶體映射檔案的方式,READ_ONLY(隻讀)、READ_WRITE(讀寫)、PRIVATE(建立一個讀寫副本)
           

(4)sendfile方式實作

  • fileChannel.transferTo(long postition,long count,WritableByteChannel target)

  • 将位元組從此通道的檔案傳輸到給定的可寫入位元組通道。
  • 傳回值為真實拷貝的size,最大拷貝2G,超出2G的部分将丢棄。
position:檔案中的位置,從此位置開始傳輸,必須非負數
count:要傳輸的最大位元組數,必須非負數
target:目标通道
傳回:實際已傳輸的位元組數,可能為零
           
  • fileChannel.transferFrom(ReadableByteChannel src, long position, long count)

  • 将位元組從給定的可讀取位元組通道傳輸到此通道的檔案中
  • 對比 從源通道讀取并将内容寫入此通道的循環語句相比,此方法更高效
src:源通道
position:檔案中的位置,從此位置開始傳輸,必須非負數
count:要傳輸的最大位元組數, 必須非負數
傳回:實際已傳輸的位元組數,可能為零
           
  • transferFrom允許将一個通道連接配接到另一個通道,不需要在使用者态和核心态來回複制,同時通道的核心态資料也無需複制,transferTo隻有源為FileChannel才支援transfer這種搞笑的複制方式,其他如SocketChannel都不支援transfer模式。
  • 一般可以做FileChannel->FileChannel->FileChannel 和 FileChannel->SocketChannel的transfer零拷貝

4.檔案IO性能對比實戰

實作一個檔案拷貝,對比不同IO方式性能差異,檔案大小 200MB~5GB

編碼實作:

  • 普通java的io流
  • 普通java的帶buffer的io
  • 零拷貝實作之mmap的io
  • 零拷貝實作之sendfile的io

運作環境準備

  • Linux CentOS7.X
  • 安裝JDK11 配置全局環境變量 vi /etc/profile
JAVA_HOME=/usr/local/jdk11
CLASSPATH=$JAVA_HOME/lib/
PATH=$PATH:$JAVA_HOME/bin
export PATH JAVA_HOME CLASSPATH
           
  • 環境變量立刻生效
    • source /etc/profile
  • 檢視安裝情況 java -version
【作業系統】作業系統IO技術底層機制和ZeroCopy
  • 準備1.34G測試檔案
【作業系統】作業系統IO技術底層機制和ZeroCopy

(1)普通java的io驗證

public class IOTest {

    public static void main(String[] args) {
        String inputFilePath = args[0];
        String outputFilePath = args[1];

       	long start = System.currentTimeMillis();
        try (
                FileInputStream fis = new FileInputStream(inputFilePath);
                FileOutputStream fos = new FileOutputStream(outputFilePath)
        ) {
            byte[] buf = new byte[1];
            while(fis.read(buf) != -1){
                fos.write(buf);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("普通IO耗時:"+(end-start));
    }
           
【作業系統】作業系統IO技術底層機制和ZeroCopy

(2)普通java的帶buffer的io

public class BufferIOTest {
    public static void main(String[] args) {
        String inputFilePath = args[0];
        String outputFilePath = args[1];
        long start = System.currentTimeMillis();
        try (
                BufferedInputStream bis = new BufferedInputStream(new FileInputStream(inputFilePath));
                BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(outputFilePath))
        ) {
            byte[] buf = new byte[1];
            while(bis.read(buf) != -1){
                bos.write(buf);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("Buffer IO耗時:"+(end-start));
    }
}
           
【作業系統】作業系統IO技術底層機制和ZeroCopy

(3)零拷貝實作之mmap的io

  • 一次 map 最大支援2GB,超過2GB會報錯
public class MmapIOTest {
    public static void main(String[] args) {

        String inputFilePathStr = args[0];
        String outputFilePathStr = args[1];

        long start = System.currentTimeMillis();
        try (
                FileChannel channelIn = new FileInputStream(inputFilePathStr).getChannel();
                FileChannel channelOut = new RandomAccessFile(outputFilePathStr, "rw").getChannel()
        ) {
            long size = channelIn.size();
            System.out.println("mappedFile:"+size);
            MappedByteBuffer mbbi = channelIn.map(FileChannel.MapMode.READ_ONLY, 0, size);
            MappedByteBuffer mbbo = channelOut.map(FileChannel.MapMode.READ_WRITE, 0, size);
            for (int i = 0; i < size; i++) {
                byte b = mbbi.get(i);
                mbbo.put(i,b);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("mmap 零拷貝 IO 耗時:"+(end-start));
    }
}
           
【作業系統】作業系統IO技術底層機制和ZeroCopy

(4)零拷貝實作之sendfile的io

  • 最大拷貝2G,超出2G的部分将丢棄,最終拷貝的檔案大小隻有2GB多點,超過2GB可以考慮多次執行
public class SendFileIOTest {
    public static void main(String[] args) {

        String inputFilePathStr = args[0];
        String outputFilePathStr = args[1];

        long start = System.currentTimeMillis();
        try (
                FileChannel channelIn = new FileInputStream(inputFilePathStr).getChannel();
                FileChannel channelOut = new FileOutputStream(outputFilePathStr).getChannel()
        ) {
            // 代碼一:針對小于2GB的問題,傳回值為真實拷貝的size,最大拷貝2G,超出2G的部分将丢棄,最終拷貝檔案大小隻有2GB
            //channelIn.transferTo(0,channelIn.size(),channelOut);

            // 代碼二:針對大于2GB的檔案
            long size = channelIn.size();
            for (long left = size;left>0;){
                //transferSize所拷貝過去的真實長度,size - left 計算出下次要拷貝的位置
                long transferSize = channelIn.transferTo((size - left),left,channelOut);
                System.out.println("總大小:"+size+",拷貝大小:"+transferSize);
                //left剩餘位元組多少
                left = left - transferSize;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("sendfile 零拷貝 IO 耗時:"+(end-start));
    }
}
           
【作業系統】作業系統IO技術底層機制和ZeroCopy

(5)測試結果分析

  • 1~2GB的檔案
  • 普通拷貝
    • 普通java的io流【慢】3973924秒
    • 普通java的帶buffer的io【快】33196秒
  • 零拷貝
    • 零拷貝實作之mmap的io【快】7131秒
    • 零拷貝實作之sendfile的io【快】1784秒
  • 分析原因之前,我們先來了解一下局部性原理
局部性原理:指計算機在執行某個程式時,傾向于使用最近使用的資料
時間局部性:如果程式中的某條指令一旦被執行,則不久的将來該指令可能再次被執行
空間局部性:一旦程式通路了某個存儲單元,在不久的将來附近的存儲單元也有可能被通路
           
  • 普通的IO和Buffer IO,為什麼帶有Buffer的IO要比普通的IO性能高?
每次讀取資料的時候,系統根據局部性原理,通過DMA會讀入更多的資料到核心緩沖區裡面
OS根據局部性原理會在一次read(),系統調用過程中預讀更多的檔案資料緩存在核心IO緩沖區中
當繼續通路的檔案資料在緩沖區中時便直接拷貝資料到程序緩沖區,避免了再次的抵消磁盤IO操作
OS已經幫減少磁盤IO操作次數,提高了性能
           
  • 兩種零拷貝的方式對比
(1)sendfile

無法在調用過程中修改資料,隻适用于應用程式不需要對所通路資料進行處理修改情況,适合靜态檔案傳輸,MQ的Broker發送消息給消費者。适合大檔案傳輸,2次上下文切換,最少2次資料拷貝。

(2)mmap

在mmap調用可以在應用程式中直接修改Page Cache中的資料,使用的是mmap+write兩步。調用比sendfile成本高,但由于傳統的拷貝方式,适用于多個線程以隻讀的方式同時通路同一個檔案,mmap機制下多線程共享同一個實體記憶體空間,節約記憶體。适合小資料量續寫,4次上下文切換,3次資料拷貝。
           

5.主流中間件中用到的ZeroCopy技術

(1)Nginx使用的是sendfile 零拷貝

  • WebServer處理靜态頁面請求時,是從磁盤中讀取網頁的内容,因為sendfile不能在應用程式中修改資料,是以最适合靜态檔案伺服器或者是直接轉發資料的代理伺服器。

(2)rocketmq主要是mmap,也有小部分使用sendfile

  • rocketMQ在消息存盤和網絡發送使用mmap, 單個CommitLog檔案大小預設1GB
    • 要在使用者程序内處理資料,然後再發送出去的話,使用者空間和核心空間的資料傳輸就是不可避免的

(3)Kafka主要是sendfile,也有小部分使用mmap

  • kafka 在用戶端和 broker 進行資料傳輸時,broker 使用 sendfile 系統調用,類似 【FileChannel.transferTo】 API,将磁盤檔案讀到 OS 核心緩沖區後,直接轉到 socket buffer 進行網絡發送,即 Linux 的 sendfile。

    中讀取網頁的内容,因為sendfile不能在應用程式中修改資料,是以最适合靜态檔案伺服器或者是直接轉發資料的代理伺服器。

(2)rocketmq主要是mmap,也有小部分使用sendfile

  • rocketMQ在消息存盤和網絡發送使用mmap, 單個CommitLog檔案大小預設1GB
    • 要在使用者程序内處理資料,然後再發送出去的話,使用者空間和核心空間的資料傳輸就是不可避免的

(3)Kafka主要是sendfile,也有小部分使用mmap

  • kafka 在用戶端和 broker 進行資料傳輸時,broker 使用 sendfile 系統調用,類似 【FileChannel.transferTo】 API,将磁盤檔案讀到 OS 核心緩沖區後,直接轉到 socket buffer 進行網絡發送,即 Linux 的 sendfile。

繼續閱讀