包教包會的零拷貝,你會了嗎?
這一篇的主題是零拷貝這個技術點!
我們接下來從下面這幾個問題的角度來給全方面分析零拷貝這個技術點,一邊讀不懂的同學,趕緊收藏,讀多幾遍就懂了
還有還有,收藏起來,等以后忘記了或者快要面試的時候,可以逃出來熟悉熟悉
畢竟,好記性不如爛筆頭的嘞
為什么要有 DMA 技術?
我們先來看一下在沒有DMA技術之前的IO過程:
1、CPU發出對應的指令到磁盤系統,然后返回
2、磁盤系統收到指令,把數據放入到磁盤系統的內部緩沖區中,然后產生一個中斷指令
3、CPU收到中斷信號,停止當前工作,緊接著把磁盤系統緩沖區中的數據讀到自己的寄存器內,然后把寄存器的數據寫入到內存,在此數據傳輸期間CPU無法執行其它工作
畫了一個圖幫助大家理解
聰明的小伙伴已經發現其中的弊端了,就是數據傳輸期間,CPU無法執行其它命令
我們知道CPU是中央處理器,這個東西的性能能省就省,能扣著點用就扣著點用,畢竟整個機器都要用這家伙
簡單的搬運幾個字符數據肯定沒啥問題,但是如果傳輸大量數據的時候都需要CPU來搬運,那就很糟糕了
于是,DMA技術就誕生了,就是直接內存訪問技術Direct Memory Access
DMA技術,就是在進行IO設備和內存之間數據傳輸的時候,數據搬運的工作全部交給DMA控制器,而CPU不再參與任何和數據搬運相關的事情了,這樣就把CPU空出來了
具體來看一下使用DMA控制器的流程
1、用戶調用read,先操作系統發起IO請求,請求讀取數據到自己的內存緩沖區,然后進入阻塞
2、操作系統收到請求,把IO請求發給了DMA,然后CPU執行其它任務,DMA發送給磁盤
3、磁盤收到IO請求,把數據放入到自己的緩沖區,磁盤系統緩沖區滿的時候,向DMA發起中斷指令
4、DMA收到中斷指令,將磁盤緩沖區數據拷貝到內核緩沖區,不占用CPU
5、DMA讀取了足夠多數據,發送中斷信號給CPU
6、CPU收到DMA信號,知道數據準備好了,將數據從內核拷貝到用戶空間
看整個過程,發現CPU不再參與數據搬運的工作,而是由DMA完成的,但是呢,CPU在這個過程也是必不可少,因為傳輸什么,從哪里傳輸到哪里需要CPU來告訴DMA控制器
這就像創業公司,老板自己干活忙不過來了,就招了一個秘書,但是,這個秘書操作什么,如何操作,還是得聽老板的指揮
早期 DMA 只存在在主板上,如今由于 I/O 設備越來越多,數據傳輸的需求也不盡相同,所以每個 I/O 設備里面都有自己的 DMA 控制器。
傳統的傳輸文件
先來給大家簡單說一下用戶空間和內核空間,比如我們部署一個Java程序到一臺Linux服務器上,我們可以認為JVM的區域就是用戶空間,其余的空間就是內核空間,用戶空間和內核空間對于系統文件的操作權限是不一樣的
傳統的文件傳輸的工作方式:數據讀取和寫入是從用戶空間和內核空間來回復制,而內核空間的數據是通過操作系統層面的IO接口從磁盤讀取或者寫入
代碼通常如下,一般會需要兩個系統調用:
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
看這兩行代碼做了啥
兩次系統調用,發生了4次用戶態和內核態的上下文切換,每次系統調用都得先從用戶態切換到內核態,然后等內核態完成任務,再切換回到用戶態
一次上下文的切換耗時幾十納秒到幾微秒,時間看上去很短,但是在高并發的場景下,這類時間就會變得不可忽視,從而影響系統的性能
中間還發生了4次數據拷貝,其中兩次是DMA的拷貝,DMA技術是優化IO設備到內核區的,另外兩次是通過CPU拷貝用戶緩沖區的
1、第一次拷貝,磁盤上的數據通過DMA技術拷貝到操作系統的內核區中
2、第二次拷貝,CPU把內核緩沖區數據拷貝到用戶緩沖區中
3、第三次拷貝,CPU將用戶緩沖區的數據搬運到內核緩沖區中
4、第四次拷貝,通過DMA技術把內核數據搬運到網卡的緩沖區中
問題:我們搬運一次數據,中間卻復制了4次,過多的上下文切換和過多的數據拷貝都會降低系統性能,所以,如果想提高文件傳輸的性能,就需要減少用戶態和內核態的上下文切換和內容拷貝的次數
優化思路
減少用戶態和內核態之間的上下文切換
之所以發生上下文的切換,是因為用戶空間沒有權限操作磁盤或者網卡,內核的權限最高,這些操作設備的過程都需要交給操作系統的內核來完成,一次系統調用也就意味著必然發生2次上下文的切換,首先從用戶態切換到內核態,內核態執行完任務之后再切換到用戶態執行相應進程的代碼指令
所以,要減少上下文切換的次數,就需要減少系統調用的次數
減少數據拷貝的次數
數據傳輸的4次拷貝,其中內核拷貝到用戶緩沖區,再從用戶緩沖區拷貝到內核緩沖區,這兩個過程是沒必要的,因為在文件傳輸的應用場景中,在用戶空間我們并不會對數據再加工,所以這個數據沒必要搬運到用戶空間
如何實現零拷貝?
零拷貝技術實現的方式通常有 2 種:
mmap + write(三次拷貝+兩次系統調用)
Sendfile(三次拷貝+一次系統調用)
下面就談一談,它們是如何減少「上下文切換」和「數據拷貝」的次數。
mmap + write
在前面我們知道,read() 系統調用的過程中會把內核緩沖區的數據拷貝到用戶的緩沖區里,于是為了減少這一步開銷,我們可以用 mmap() 替換 read() 系統調用函數。
buf = mmap(file, len);
write(sockfd, buf, len);
mmap() 系統調用函數會直接把內核緩沖區里的數據「映射」到用戶空間,這樣,操作系統內核與用戶空間就不需要再進行任何的數據拷貝操作。
具體過程如下:
1、應用調用了mmap(),DMA把磁盤數據拷貝到內核緩沖區,此時,應用進程和內核會共享這個內核緩沖區
2、應用系統調用write(),操作系統直接把內核緩沖區數據拷貝到網絡緩沖區中,這個也是屬于內核態,內核中的拷貝,由CPU來操作
3、第三次拷貝,通過DMA技術把網絡緩沖區數據拷貝到網卡的緩沖區中
我們可以得知,通過使用mmap()來代替 read(),可以減少一次數據拷貝的過程。
但這還不是最理想的零拷貝,因為仍然需要通過 CPU 把內核緩沖區的數據拷貝到 socket 緩沖區里,而且仍然需要 4 次上下文切換,因為系統調用還是 2 次。
Sendfile
在 Linux 內核版本 2.1 中,提供了一個專門發送文件的系統調用函數 sendfile(),函數形式如下:
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
它的前兩個參數分別是目的端和源端的文件描述符,后面兩個參數是源端的偏移量和復制數據的長度,返回值是實際復制數據的長度。
首先,它可以替代前面的 read() 和 write() 這兩個系統調用,這樣就可以減少一次系統調用,也就減少了 2次上下文切換的開銷。
其次,該系統調用,可以直接把內核緩沖區里的數據拷貝到 socket 緩沖區里,不再拷貝到用戶態,這樣就只有 2 次上下文切換,和 3 次數據拷貝。
如下圖
但是這還不是真正的零拷貝技術,如果網卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技術(和普通的 DMA 有所不同),我們可以進一步減少通過 CPU 把內核緩沖區里的數據拷貝到 socket 緩沖區的過程。
你可以在你的 Linux 系統通過下面這個命令,查看網卡是否支持 scatter-gather 特性:
$ ethtool -k eth0 | grep scatter-gather
scatter-gather: on
于是,從 Linux 內核 2.4 版本開始起,對于支持網卡支持 SG-DMA 技術的情況下, sendfile() 系統調用的過程發生了點變化,具體過程如下:
1、DMA直接將磁盤上的數據拷貝到內核緩沖區中
2、緩沖區描述符和數據長度傳到 socket 緩沖區,這樣網卡的 SG-DMA 控制器就可以直接將內核緩存中的數據拷貝到網卡的緩沖區里,此過程不需要將數據從操作系統內核緩沖區拷貝到 socket 緩沖區中,這樣就減少了一次數據拷貝
所以,這個過程之中,只進行了 2 次數據拷貝,如下圖:
這就是所謂的零拷貝(Zero-copy)技術,因為我們沒有在內存層面去拷貝數據,也就是說全程沒有通過 CPU來搬運數據,所有的數據都是通過 DMA 來進行傳輸的。
CPU屬于參與了,但沒完全參與,DMA操作需要CPU指揮,描述符和數據長度需要CPU發送
零拷貝技術的文件傳輸方式相比傳統文件傳輸的方式,減少了 2 次上下文切換和數據拷貝次數,只需要 2 次上下文切換和數據拷貝次數,就可以完成文件的傳輸,而且 2 次的數據拷貝過程,都不需要通過 CPU,2 次都是由 DMA 來搬運。
我們通常說的這個零拷貝技術中的這個零,指的是內核態和用戶態之間的拷貝次數,變成了0
所以,總體來看,零拷貝技術可以把文件傳輸的性能提高至少一倍以上。
PageCache
上面說的第一步是先把磁盤文件數據拷貝到內核緩沖區中,這個內核緩沖區就是磁盤高速緩沖區PageCache,內存速度比磁盤速度快,但是內存空間比磁盤要小
我們需要把此時的熱點數據放入到緩存中,因為這是最近需要頻繁訪問的,空間不足的時候淘汰掉那些訪問頻率低的數據
緩存這些道理大家應該都懂,零拷貝也使用了緩存技術,讀取數據的時候,優先在PageCache中找,找到直接返回,找不到去磁盤中讀取,然后緩存到PageCache中
還有一點,讀取磁盤數據的時候,需要找到數據所在的位置,但是對于機械磁盤來說,就是通過磁頭旋轉到數據所在的扇區,再開始「順序」讀取數據,但是旋轉磁頭這個物理動作是非常耗時的,為了降低它的影響,PageCache 使用了「預讀功能」。
比如,假設 read 方法每次只會讀 32 KB 的字節,雖然read 剛開始只會讀 0 ~ 32 KB 的字節,但內核會把其后面的 32~64 KB 也讀取到 PageCache,這樣后面讀取32~64 KB 的成本就很低,如果在 32~64 KB 淘汰出PageCache 前,進程讀取到它了,收益就非常大。
所以,PageCache 的優點主要是兩個:
緩存最近被訪問的數據;預讀功能;
這兩個做法,將大大提高讀寫磁盤的性能。
但是,在傳輸大文件(GB 級別的文件)的時候,PageCache 會不起作用,那就白白浪費 DMA 多做的一次數據拷貝,造成性能的降低,即使使用了PageCache 的零拷貝也會損失性能,一個大文件直接占滿,導致某些熱點小文件無法使用,性能就降低了
所以,針對大文件的傳輸,不應該使用 PageCache,也就是說不應該使用零拷貝技術,因為可能由于PageCache 被大文件占據,而導致「熱點」小文件無法利用到 PageCache,這樣在高并發的環境下,會帶來嚴重的性能問題。
對于大文件傳輸,可以通過異步IO和繞開PageCache的IO來代替零拷貝技術
在 nginx 中,我們可以用如下配置,來根據文件的大小來使用不同的方式:
location /video/ { sendfile on; aio on; directio 1024m;}
當文件大小大于 directio 值后,使用「異步 I/O + 直接 I/O」,否則使用「零拷貝技術」。
總結
1、早期IO,內核數據需要IO進行復制,2次系統調用,4次上下文切換,4次數據的拷貝,CPU拷貝數據期間不能執行其它命令
2、引入DMA技術,DMA可以代替CPU進行磁盤到內核區域數據的復制,這個期間CPU可執行其它命令,改善了性能
3、零拷貝技術:mmap+write,2次系統調用,4次上下文切換,3次數據的拷貝,減少了讀取期間內核區域到用戶區域的數據復制,原因是兩者共享了內核區域的緩沖區
4、零拷貝技術:Sendfile,1次系統調用,2次上下文切換,3次數據的拷貝,直接指定了原文件和目標文件,替代了原來的兩次系統調用,直接一次完成
5、真正的零拷貝技術:網卡支持 SG-DMA技術,數據從磁盤系統讀取到內核緩沖區之后,不需要復制到相應的socket緩沖區即可,只需要發送描述符和數據長度即可,這個期間經歷了1次系統調用,2次上下文切換,2次數據拷貝,沒有在內核層面去進行數據的拷貝
6、零拷貝技術引用PageCache緩存技術,緩存技術用于加速熱點文件的查詢速度,但是不適用于大文件,大文件可以通過異步IO和繞開PageCache的IO來代替零拷貝技術
參考文獻:https://zhuanlan.zhihu.com/p/258513662
本文轉載自微信公眾號「左耳君」,可以通過以下二維碼關注。轉載本文請聯系左耳君公眾號。