天天看點

什麼是零拷貝

零拷貝

零拷貝相較于傳統的IO流程擁有更高的資料發送效率,無論是RocketMq,Kafka還是Netty等都用到了零拷貝技術,那究竟什麼是零拷貝呢,零拷貝又是通過什麼方式提升資料發送效率呢?

首先我們要明白,一次資料發送過程就是将磁盤中的目标資料交給網卡傳輸出去的流程。磁盤以及網卡都屬于硬體層。而應用程式是不能直接操作硬體的。如果要操作硬體,需要進行上下文切換從使用者态切換到核心态由作業系統來完成硬體互動。關于使用者态,核心态,上下文切換這些這裡不再贅述。

傳統IO

先看一下傳統IO的資料發送流程:

什麼是零拷貝
  1. 應用程式調用read函數,IO開始。進行上下文切換,從使用者态切換到核心态。
  2. DMA控制器将磁盤中的資料拷貝到OS緩沖區
  3. CPU将OS緩沖區的資料拷貝到使用者緩沖區。進行上下文切換,從核心态切換到使用者态。
  4. 應用程式調用write函數往Socket中寫資料。進行上下文切換,從使用者态切換到核心态。CPU将使用者緩沖區的資料拷貝到Socket緩沖區
  5. DMA控制器将Socket緩沖區中的資料拷貝到網卡中完成資料發送
  6. 進行上下文切換,從核心态切換到使用者态。一次資料發送流程結束。

可以看出傳統的IO流程包括4次上下文的切換,4次拷貝資料(兩次CPU拷貝以及兩次DMA拷貝)。

CPU拷貝和DMA拷貝

資料拷貝流程都是CPU來負責進行拷貝的。但是和磁盤,網卡這種硬體互動時,因為硬體的速度限制,如果CPU全程參與拷貝,那麼就很浪費CPU的時間片。DMA便是用來優化這個過程的,DMA,英文全稱是Direct Memory Access,即直接記憶體通路。DMA本質上是一塊主機闆上獨立的晶片,能在外設裝置和記憶體存儲器之間直接進行IO資料傳輸,CPU隻需發起拷貝指令給DMA,DMA就能完成資料的拷貝。其拷貝過程不需要CPU的參與。 可以簡單的了解為,DMA是硬體為CPU找的一個助手,對于硬體方面較慢的資料拷貝,CPU隻需将指令發給DMA,DMA就能幫忙CPU完成資料拷貝,這樣高效的CPU就能空下來處理其它事情。

這是一個硬體層次的優化,隻需要知道有這個技術即可。

零拷貝技術

零拷貝并不是說不會發生任何資料拷貝,而是不發生OS緩沖區到使用者緩沖區的拷貝。以此減少核心态和使用者态之間切換以及CPU拷貝的次數。目前零拷貝技術有兩種:

  1. mmap+write
  2. sendfile (以及更新版的帶有DMA收集功能的sendFile)

mmap和sendFile各有優缺點,并不是說誰一定優于誰。各有各的适用場景。比如RocketMQ就是用的mmap,而RabbitMQ用的則是sendfile。

mmap+write

mmap通過記憶體映射來減少上下文切換與CPU拷貝。

記憶體映射

記憶體映射即在程序的虛拟位址空間建立一個映射,分為兩種:

檔案映射:檔案支援的記憶體映射,把檔案的一個區間映射到程序的虛拟位址空間,資料源是儲存設備上的檔案。

匿名映射:沒有檔案支援的記憶體映射,把實體記憶體映射到程序的虛拟位址空間,沒有資料源。

即将一個檔案或者其它對象映射到程序的位址空間,實作檔案磁盤位址和程序虛拟位址空間中一段虛拟位址的一一對映關系。實作這樣的映射關系後,程序可以采用指針的方式讀寫操作這一段記憶體,而無需将資料由核心态拷貝到使用者态。且系統會自動回寫髒頁面到對應的檔案磁盤上,即完成了對檔案的操作而不必再調用read,write等系統調用函數。

可以了解為java中的淺拷貝。使用者程序的緩沖指向OS的緩沖,這時使用者程序對記憶體映射對象的操作

mmap就是一種記憶體映射檔案的方法。Java中的實作是MappedByteBuffer,通過channel#map方法得到。

什麼是零拷貝

通過記憶體映射,IO流程就變成了這樣

  1. 應用程式調用mmap函數,IO開始。進行上下文切換,從使用者态切換到核心态。
  2. DMA控制器将磁盤中的資料拷貝到OS緩沖區
  3. mmap函數建立記憶體映射完畢。進行上下文切換,從核心态切換到使用者态。
  4. 應用程式調用write函數通過記憶體映射往Socket中寫資料。進行上下文切換,從使用者态切換到核心态。
  5. CPU将OS緩沖區的資料拷貝到Socket緩沖區。
  6. DMA控制器将Socket緩沖區中的資料拷貝到網卡中完成資料發送
  7. 進行上下文切換,從核心态切換到使用者态。一次資料發送流程結束。

關鍵就在于3,4,5三步。 mmap無需像傳統IO一樣操作資料需要通過CPU拷貝将OS緩沖區的資料拷貝到使用者緩沖區。是以在第3步和第4步也就無需進行CPU的資料拷貝。發起write指令後,因為資料實際是在OS緩沖區的,是以CPU可以直接将資料從OS緩沖區拷貝到Socket緩沖區。

可以看到mmap+write的方式包括4次上下文的切換,3次拷貝資料(一次CPU拷貝以及兩次DMA拷貝)。

sendFile

Java對sendfile的支援是NIO中的FileChannel.transferTo()或者transferFrom()。

sendFile可以在兩個檔案描述符之間傳輸資料,整個傳輸過程在核心态完成,避免了資料在OS緩沖區和使用者緩沖區之間的拷貝,而且減少了上下文切換次數:

什麼是零拷貝

sendFile流程是這樣的:

  1. 應用程式調用sendFile函數指出源描述符和目标描述符,IO開始。進行上下文切換,從使用者态切換到核心态。
  2. DMA控制器将磁盤中的資料拷貝到OS緩沖區
  3. CPU将OS緩沖區(源描述符)的資料拷貝到Socket緩沖區(目标描述符)。
  4. DMA控制器将Socket緩沖區中的資料拷貝到網卡中完成資料發送
  5. 進行上下文切換,從核心态切換到使用者态。一次資料發送流程結束。

可以發現,sendfile實作的零拷貝僅僅發生了2次上下文切換以及3次拷貝(2次DMA拷貝+1次CPU拷貝)

sendfile +DMA scatter/gather實作的零拷貝

linux2.4 版本,sendfile進行了優化更新, 引入SG-DMA技術。其實就是對DMA拷貝加入了scatter/gather操作,可以讓DMA在多個緩沖區實作一個簡單的IO操作,比如從通道中讀取資料到多個緩沖區,或者從多個緩沖區寫入資料到通道。這使得資料可以直接從OS緩沖區到網卡。徹底避免了CPU拷貝。

什麼是零拷貝

流程如下:

  1. 應用程式調用sendFile函數指出源描述符和目标描述符,IO開始。進行上下文切換,從使用者态切換到核心态。
  2. DMA控制器将磁盤中的資料拷貝到OS緩沖區
  3. CPU将OS緩沖區的檔案描述資訊(包括記憶體位址和偏移量)發送到socket緩沖區
  4. DMA通過檔案描述資訊直接将資料從OS緩沖區拷貝到網卡。
  5. 進行上下文切換,從核心态切換到使用者态。一次資料發送流程結束。

可以發現,sendfile +DMA scatter/gather實作的零拷貝僅僅發生了2次上下文切換以及2次拷貝(2次DMA拷貝),實作了真正意義上不通過CPU搬運資料的零拷貝。

kafka和RocketMQ的零拷貝

從上面的流程來看sendFile是明顯比mmap更高效的。但是因為sendFile相當于原汁原味的讀寫,直接将硬碟上的資料發送給網卡,如果需要對硬碟的資料做一定的修改再發送給網卡的話,就不适合使用sendFile了。

在基于這種差別,資料發送上,Kafka與RocketMQ采用了不同方式的零拷貝。Kafka采用了sendFile,而RocketMQ則采用了mmap.

RocketMQ為了寫入的速率,是将所有的隊列資料統一寫入同一個CommitLog來實作順序寫。這就是導緻消費時需要讀出CommitLog進行應用層過濾,是以就不能用到sendFile+DMA的零拷貝,而隻能使用mmap.

kafka則是同一個隊列的資料存在一起,發送時無需過濾,是以可以使用sendFile來獲得更高的發送效率。