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