零拷貝-zero copy


Efficient data transfer through zero copy
Zero Copy I: User-Mode Perspective

0. 前言

在閱讀RocketMQ的官方文檔時,發現Chapter6.1中關於零拷貝的敘述中有點不理解,因此查閱了相關資料,來解釋文中的說法。

Consumer消費消息過程,使用了零拷貝,零拷貝包含以下兩種方式

  1. 使用mmap + write方式 優點:即使頻繁調用,使用小塊文件傳輸,效率也很高 缺點:不能很好的利用DMA方式,會比sendfile多消耗CPU,內存安全性控制復雜,需要避免JVM Crash問題。
  2. 使用sendfile方式 優點:可以利用DMA方式,消耗CPU較少,大塊文件傳輸效率高,無內存安全新問題。 缺點:小塊文件效率低於mmap方式,只能是BIO方式傳輸,不能使用NIO。
    RocketMQ選擇了第一種方式,mmap+write方式,因為有小塊數據傳輸的需求,效果會比sendfile更好。

為什么mmap會多消耗CPU?
為什么mmap比sendfile內存安全性控制復雜,為什么mmap會引起JVM Crash?
為什么sendfile只能是BIO的,不能使用NIO?

為什么mmap對於小塊數據傳輸的需求效果更好?

  • 這個問題其實很好解答,如果上一個說法成立,mmap支持NIO,sendfile只能BIO傳輸,那么NIO的特性本身就會對數據塊小、請求個數多的傳輸需求有很好的支持。

1. 零拷貝 Zero copy

1.1 no zero-copy

Web應用程序通常提供大量靜態內容,例如使用聊天工具向好友發送了一張本地圖片,應用程序需要從磁盤讀取圖片數據,並將完全相同的數據寫到響應socket中,通過網絡發送給對方。這個操作看起來貌似不需要占用過多的CPU資源,因為沒有計算的需求,但仍然效率較低:內核從磁盤讀取數據並將其推送到應用程序,然后應用程序將其推回到內核寫到套接字。這種場景下,應用程序充當了一個低效的中介,它將數據從磁盤文件獲取到套接字。

1.2 zero copy

每次數據經過用戶內核邊界時,都必須復制一次數據,這會消耗CPU周期和內存帶寬。zero-copy技術的出現就是通過減少復制次數來消除這些副本。使用零拷貝請求的應用程序,內核將數據直接從磁盤文件復制到套接字,而不用 無需通過應用程序。零拷貝極大地提高了應用程序性能,並減少了內核和用戶模式之間的上下文切換次數。

1.3 Java中的zero copy

Java類庫通過java.nio.channels.FileChannel中的transferTo()方法在LinuxUNIX系統上支持零拷貝。使用transferTo()法將字節直接從調用它的通道傳輸到另一個可寫字節通道,而不需要數據流經應用程序。
本文先解釋下傳統復制,然后介紹zero copy的幾種機制,最后解釋前言中的疑問。

1.4 類比舉例

在干貨之前,先喝口湯壓壓驚。舉個通俗點的例子來類比描述傳統的 no zero-copy的做法,A用左手拿筷子要吃飯,B告訴你A,你需要用右手拿筷子,然后A把筷子從左手給了B,然后B又把筷子塞到A的右手里,A開始吃飯。

A和B這里可以看成兩個上下文,A的左手傳遞筷子給B之后,切換到了B的上下文,B傳遞給A的右手,又切換回A的上下文,這個代價其實是非常昂貴的。
為了減少這種昂貴的代價,我們可以想象一些場景來逐步降低事情的復雜度。
最直觀最簡單的方法,B只需要告訴A,也就是說B發出一條指令,A接收指令之后,自己把筷子從左手換到右手,就可以既減少了上下文的切換,減輕了B的壓力,又減少了傳遞的次數和溝通代價。然而這需要A具備這樣的功能,計算機中某些硬件可以提供這樣的支持,但是如果A是一個不滿3歲的孩子,他可能聽不懂你的話,又或者不明白如何把筷子從左手轉到右手。
這個時候B只需要扶住A的左手,幫他把筷子換到右手里。這樣,也縮減了這個過程中的代價。

2. 傳統傳輸方式

Linux標准訪問文件方式
在Linux中,訪問文件的方式是通過兩個系統調用實現的:read()write()

當應用程序調用read()系統調用讀取一塊數據的時候:

  • 如果該塊數據已經在內存中,就直接從內存中讀取數據並返回給應用程序;
  • 如果該塊數據不在內存中,name數據會被從磁盤上讀取到頁緩存中,再從頁緩存中拷貝到用戶地址空間中去。

如果一個進程讀取某個文件,那么其他進程就都不可以讀取或者更改該文件;對於寫操作,當一個進程調用了write()系統調用往某個文件中寫數據的時候,數據會先從用戶地址空間拷貝到操作系統內核地址空間的頁緩存中,然后才被寫到磁盤上。

對於這種標准的訪問文件方式,在數據被寫到頁緩存中時,write()系統調用就算執行完成,並不會等數據完全寫入到磁盤上。

Linux在這里采用的是延遲寫機制。

一般情況下,應用程序采取的寫操作機制有三種:

  1. 同步寫(Synchronous Writes),數據會立即從緩存頁寫回磁盤,應用程序會一直等待到寫入磁盤的結束。
  2. 異步寫(Asynchronous Writes),數據寫入緩存頁后,操作系統會定期將頁緩存中的數據刷到磁盤上,在寫入磁盤結束后,系統會通知應用程序寫入已完成。
  3. 延遲寫(Deferred Writes),數據寫入緩存頁后立即返回應用程序寫入成功,操作系統定期將頁緩存中是數據刷入磁盤,由於寫入頁緩存時已經返回吸入成功,寫入磁盤之后不會通知應用程序。因此延遲寫機制是存在數據丟失的風險的。

2.1 傳輸過程

圖1. 傳統方式數據傳輸示意圖

如上圖所示,數據按箭頭的方向流動,從本地終端的硬盤存儲中讀取數據,經過4次copy,最終到達NIC buffer,通過網卡再發送給其他終端。
這種傳輸方式實際上是一種經過優化的設計,雖然看起來效率比較低下,但是內核緩沖區的存在使得整個流程的性能得到了提升。內核緩沖區的引入充當了預讀緩存的角色,使得數據並不是直接從硬盤到用戶緩沖區,而是允許應用程序在未請求的情況下,內核緩沖區中已經存在了相應的數據。
內核空間內,內存與硬件存儲之間的數據傳輸使用了DMA直接內存存取的復制方式,這種方式不需要CPU的參與,並且提高讀取速度。CPU也因此可以趁機去完成其他的工作。

例如用戶緩沖區大小為4K,內核緩沖區大小為8K,文件總大小為40K,每次用戶請求讀取4K數據時,內核緩沖區中已經預讀存入了相應的數據。
硬盤到內存(內核緩沖區)的數據傳輸速度是比較慢的,尤其是SSD應用之前,而內核緩沖區到用戶緩沖區這種內存到內存的復制相對較快,用戶緩沖區就不用等待硬盤數據傳輸到內存。廣義上也是一種空間換時間的做法。

然而,一些情況下內核緩沖區也無法完全跟上應用程序的步伐,比如用戶緩沖區的大於內核緩沖區的大小。預讀的數據無法滿足需要,仍然需要等待硬盤到內存的緩慢傳輸。此時性能將會大打折扣。

盡管做了很多優化,如圖所示,數據已經被復制了至少四次,並且執行了多次的用戶和內核上下文的切換。實際上這個過程比圖示要復雜得多。

2.2 上下文切換和數據復制過程

圖2.上下文切換和數據復制

圖2所示是傳統方式數據傳輸(圖1)時上下文切換和數據復制的過程。上半部分表示上下文切換,下半部分表示數據復制流程。

step 1

系統調用讀操作時,上下文會從用戶模式切換到內核模式。在內核空間中,DMA引擎執行了第一次數據拷貝,將數據從硬盤等其他存儲設備上導入到內核緩沖區。

step 2

數據從內核緩沖區拷貝到用戶緩沖區,系統調用讀操作結束並返回。調用的返回會導致又一次的上下文切換,上下文從內核又切換到用戶模式。

step 3

系統寫操作開始調用,進行第三次數據復制,將數據從用戶緩沖區寫回內核的socket緩沖區,此時回引起一次從用戶模式到內核模式的上下文切換。

step 4

寫操作的系統調用返回,引起第四次上下文切換。開始第四次數據復制,數據從內核緩沖區復制到協議引擎。這次復制是異步並且獨立的,系統不保證數據一定會傳輸,這次返回只是任務提交成功,數據包進入了隊列,等待傳輸。就像線程池模型中任務提交時,任務只是成功提交到任務隊列,何時開始執行上游調用程序並不知情。

summary

傳統的傳輸方式會存在大量的數據復制和上下文切換,如果這些重復可以消除一部分,就可以減少開銷並提升性能。某些硬件可以繞過主存儲器將數據直接傳輸到另一個設備,但是如1.4中的類比描述一樣,並不是所有硬件都支持這項功能,而且這項功能的實現遠非這么簡單。
為了降低開銷,我們可以減少復制,而不是直接消除復制。
系統為了減少復制所采用的所有方式中,最多的就是讓這些傳輸盡可能不跨越用戶空間和內核空間的邊界,因為每次跨越邊界就意味着一次復制。

3. zero-copy mmap

圖3. mmap方式零拷貝數據傳輸示意圖

mmap系統調用的方式如下:

tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);

這種方式用到兩種系統調用,mmap+write。
這種傳輸方式使用mmap()代替了read(),磁盤上的數據會通過DMA被拷貝到內核緩沖區,然后操作系統會把這塊內核緩沖區與應用程序共享,這樣就避免了跨越邊界的一次復制。應用程序再調用write()直接將內核緩沖區的內容拷貝到socket緩沖區,最后系統把數據從socket緩沖區傳輸到網卡。
mmap減少了一次拷貝,提升了效率。但是mmap也可能遇到一些隱藏的問題。例如,當應用程序map了一個文件,但是這個文件被另一個進程截斷時,write()系統調用會因為訪問非法地址而被SIGBUS信號終止。SIGBUS信號默認會殺死你的進程並產生一個coredump

解決mmap上述問題的方式通常有兩種:

  1. 增加對SIGBUS信號的處理程序
    當遇到SIGBUS信號時,處理程序可以直接去調用return,這樣,write調用在被中斷之前返回已經寫入的字節數並且將errno設置為success。但是這么處理顯得較為粗糙。
  2. 使用文件租借鎖
    在文件描述符上使用租借鎖,這樣當有進程要截斷這個文件時,內核會立刻發送一個RT_SIGNAL_LEASE信號,這樣在程序訪問非法內存之前,中斷write調用,返回已經寫入的字節數,並將errno設置為success,而不必等到writeSIGBUS殺死再做處理。

圖4 mmap上下文切換與數據傳輸

如上圖所示,mmap+write的復制減少了文件的復制,但是上下文切換的次數和read+write的方式是一樣的。

4. sendfile

圖5 sendfile數據傳輸示意圖

Linux的內核版本2.1之后,系統引入了sendfile來簡化文件傳輸到網絡的工作,這種方式不僅減少了拷貝次數,也減少了上下文的切換。
使用sendfile代替了read+write操作。

圖6 sendfile上下文切換與數據傳輸

數據發生三次拷貝,首先sendfile系統調用,通過DMA引擎將文件復制到內核緩沖區。
在內核區,內核將數據復制到socket緩沖區。
最后,DMA引擎將數據從內核socket緩沖區傳遞到協議引擎中(網卡)。

sendfile是否會遇到和mmap同樣的隱藏問題?

  • 如果另一個進程截斷了使用sendfile傳輸的文件,sendfile在沒有任何信號處理程序的情況下,會返回被中斷前傳輸的字節數,並且errno被設置為success
  • 如果使用了文件租借鎖,sendfile可以獲得RT_SIGNAL_LEASE信號,並給出和沒有使用文件租借鎖同樣的返回。

5. 使用DMA gather copy的sendfile

在內核2.4版本之后,sendfile可以在硬件支持的情況下實現更高效的傳輸。

圖7 使用DMA gather copy的sendfile數據傳輸示意圖

在硬件的支持下,不再從內核緩沖區的數據拷貝到socket緩沖區,取而代之的僅僅是緩沖區文件描述符和數據長度的拷貝。
這樣DMA引擎直接將頁緩存中數據打包發送到網絡中即可。

圖8 DMA gather copy的sendfile上下文切換與數據傳輸

這種方式避免了最后一次拷貝,並且減輕了CPU的負擔,省去了頁緩存到socket緩沖區的CPU Copy。這種sendfile是Linux中真正的零拷貝,雖然依然需要磁盤到內存的復制,但是內核空間和用戶空間內已經不存在任何多余的復制。

這種方式的前提是硬件和相關驅動程序支持DMA Gather Copy

6.總結

通過以上描述,可以解答文章開始的幾個問題。

為什么mmap會多消耗CPU?
mmap沒有完全消除內存中的文件復制,從頁緩存到socket緩沖區需要進行CPU Copy,並且上下文的切換次數和傳統的read+write方式一樣。
因此,相對於sendfilemmap會占用更多的CPU資源。

為什么mmap比sendfile內存安全性控制復雜,為什么mmap會引起JVM Crash?
mmap沒有提供被其他進程截斷時的處理,需要添加對SIGBUS信號中斷的處理。由於截斷后,mmap訪問了非法內存,SIGBUS信號會導致JVM Crash的問題。

為什么sendfile只能是BIO的,不能使用NIO?(個人理解,未驗證)

sendfile在使用DMA gather copy的情況下,降低了CPU資源的占用,減少了文件復制和上下文切換次數,但是由於socket緩沖區中拿到的只是文件描述符和數據長度,並沒有拿到真正的文件,因此並不能執行write等相關操作異步寫或者延遲寫,只能進行同步寫。這也是減少上下文切換付出的代價,接收到sendfile后,都在內核態執行,缺少應用程序的干預因此可控性也較差。所以sendfile只能使用BIO這種同步阻塞的IO。
However, 在大文件的傳輸上,sendfile依然是最佳的方式。
Java中NIO的類庫通過java.nio.channels.FileChannel中的transferTo()依賴的零拷貝是sendfile,因此實質上transferTo並不支持真正意義上的NIO。
mmap+write的方式,使真正的文件被復制到socket緩沖區,從socket緩沖區到網卡的復制過程是可以異步的,但是這種操作意味着更多的CPU消耗。
RocketMQ中更多的需求是小文件的傳輸,而NIO的特性可以更快更高效的應對這種場景。在這種權衡考量下,犧牲部分CPU資源來換取更高的文件傳輸效率的選擇顯然是一種更優的方案。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM