一文讀懂計算機核心態、使用者態和零複製技術

儲存介質的效能

話不多說,先看一張圖,下圖左邊是磁碟到記憶體的不同介質,右邊形象地描述了每種介質的讀寫速率。

一句話總結就是越靠近cpu,讀寫效能越快

。瞭解了不同硬體介質的讀寫速率後,你會發現零複製技術是多麼的香,對於追求極致效能的讀寫系統而言,掌握這個技術是多麼的優秀~

一文讀懂計算機核心態、使用者態和零複製技術

上圖是當前主流儲存介質的讀寫效能,從磁碟到記憶體、記憶體到快取、快取到暫存器,每上一個臺階,效能就提升10倍。如果我們開啟一個檔案去讀裡面的內容,你會發現時間讀取的時間是遠大於磁碟提供的這個時延的,這是為什麼呢?問題就在核心態和使用者態這2個概念後面深藏的I/O邏輯作怪。

核心態和使用者態

核心態:也稱為核心空間。cpu可以訪問記憶體的所有資料,還控制著外圍裝置的訪問,例如硬碟、網絡卡、滑鼠、鍵盤等。cpu也可以將自己從一個程式切換到另一個程式。

使用者態:也稱為使用者空間。只能受限的訪問記憶體地址,cpu資源可以被其他程式獲取。

一文讀懂計算機核心態、使用者態和零複製技術

計算機資源的管控範圍

坦白地說核心態就是一個高階管理員,它可以控制整個資源的許可權,使用者態就是一個業務,每個人都可以使用它。那計算機為啥要這麼分呢?且看下文……

由於需要限制不同的程式之間的訪問能力, 防止他們獲取別的程式的記憶體資料, 或者獲取外圍裝置的資料, 併發送到網路。CPU劃分出兩個許可權等級:使用者態和核心態。

32 位作業系統和 64 位作業系統的虛擬地址空間大小是不同的,在 Linux 作業系統中,虛擬地址空間的內部又被分為

核心空間和使用者空間

兩部分,如下所示:

一文讀懂計算機核心態、使用者態和零複製技術

透過這裡可以看出:

32 位系統的核心空間佔用 1G,位於最高處,剩下的 3G 是使用者空間;

64 位系統的核心空間和使用者空間都是 128T,分別佔據整個記憶體空間的最高和最低處,剩下的中間部分是未定義的。

核心態控制的是核心空間的資源管理,使用者態訪問的是使用者空間內的資源。

從使用者態到核心態切換可以透過三種方式:

系統呼叫,其實系統呼叫本身就是中斷,但是軟體中斷,跟硬中斷不同。

異常:如果當前程序執行在使用者態,如果這個時候發生了異常事件,就會觸發切換。例如:缺頁異常。

外設中斷:當外設完成使用者的請求時,會向CPU傳送中斷訊號。

核心態和使用者態是怎麼控制資料傳輸的?

舉個例子:當計算機A上a程序要把一個檔案傳送到計算機B上的b程序空間裡面去,它是怎麼做的呢?在當前的計算機系統架構下,它的I/O路徑如下圖所示:

一文讀懂計算機核心態、使用者態和零複製技術

計算機A的程序a先要透過系統呼叫Read(核心態)開啟一個磁碟上的檔案,這個時候就要把資料copy一次到核心態的PageCache中,進入了核心態;

程序a負責將資料從核心空間的 Page Cache 搬運到使用者空間的緩衝區,進入使用者態;

程序a負責將資料從使用者空間的緩衝區搬運到核心空間的

Socket(資源由核心管控)

緩衝區中,進入核心態。

程序a負責將資料從核心空間的 Socket 緩衝區搬運到的網路中,進入使用者態;

從以上4個步驟我們可以發現,正是因為使用者態沒法控制磁碟和網路資源,所以需要來回的在核心態切換。這樣一個傳送檔案的過程就產生了4 次上下文切換:

read 系統呼叫讀磁碟上的檔案時:使用者態切換到核心態;

read 系統呼叫完畢:核心態切換回使用者態;

write 系統呼叫寫到socket時:使用者態切換到核心態;

write 系統呼叫完畢:核心態切換回使用者態。

如此笨拙的設計,我們覺得計算機是不是太幼稚了,為啥要來回切換不能直接在使用者態做資料傳輸嗎?

CPU 全程負責記憶體內的資料複製,參考磁碟介質的讀寫效能,這個操作是可以接受的,但是如果要讓記憶體的資料和磁碟來回複製,這個時間消耗就非常的難看,因為磁碟、網絡卡的速度遠小於記憶體,記憶體又遠遠小於 CPU;

4 次 copy + 4 次上下文切換,代價太高。

所以計算機體系結構的大佬們就想到了能不能單獨地做一個模組來專職負責這個資料的傳輸,不因為佔用cpu而降低系統的吞吐呢?方案就是引入了DMA(Direct memory access)

一文讀懂計算機核心態、使用者態和零複製技術

什麼是 DMA ?

沒有 DMA ,計算機程式訪問磁碟上的資料I/O 的過程是這樣的:

CPU 先發出讀指令給磁碟控制器(發出一個系統呼叫),然後返回;

磁碟控制器接受到指令,開始準備資料,把資料複製到磁碟控制器的內部緩衝區中,然後產生一個

中斷

CPU 收到中斷訊號後,讓出CPU資源,把磁碟控制器的緩衝區的資料一次一個位元組地複製進自己的暫存器,然後再把暫存器裡的資料複製到記憶體,而

在資料傳輸的期間 CPU 是無法執行其他任務

的。

一文讀懂計算機核心態、使用者態和零複製技術

可以看到,整個資料的傳輸有幾個問題:一是資料在不同的介質之間被複製了多次;二是每個過程都要需要 CPU 親自參與(搬運資料的過程),在這個過程,在資料複製沒有完成前,CPU 是不能做額外事情的,被IO獨佔。

如果I/O操作能比較快的完成,比如簡單的字元資料,那沒問題。如果我們用萬兆網絡卡或者硬碟傳輸大量資料,CPU就會一直被佔用,其他服務無法使用,對單核系統是致命的。

為了解決上面的CPU被持續佔用的問題,大佬們就提出了 DMA 技術,即

直接記憶體訪問(Direct Memory Access)

技術。

那到底什麼是 DMA 技術

所謂的 DMA(Direct Memory Access,即直接儲存器訪問)其實是一個硬體技術,其主要目的是減少大資料量傳輸時的 CPU 消耗,從而提高 CPU 利用效率。其本質上是一個主機板和 IO 裝置上的 DMAC 晶片。CPU 透過排程 DMAC 可以不參與磁碟緩衝區到核心緩衝區的資料傳輸消耗,從而提高效率。

那有了DMA,資料讀取過程是怎麼樣的呢?下面我們來具體看看。

一文讀懂計算機核心態、使用者態和零複製技術

詳細過程:

使用者程序a呼叫系統呼叫read 方法,向OS核心(資源總管)發出 I/O 請求,請求讀取資料到自己的記憶體緩衝區中,程序進入阻塞狀態;

OS核心收到請求後,進一步將 I/O 請求傳送 DMA,然後讓 CPU 執行其他任務;

DMA 再將 I/O 請求傳送給磁碟控制器;

磁碟控制器收到 DMA 的 I/O 請求,把資料從磁碟複製到磁碟控制器的緩衝區中,當磁碟控制器的緩衝區被寫滿後,它向 DMA 發起中斷訊號,告知自己緩衝區已滿;

DMA 收到磁碟的中斷訊號後,將磁碟控制器緩衝區中的資料複製到核心緩衝區中,此時不佔用 CPU,CPU 可以執行其他任務

當 DMA 讀取了一個固定buffer的資料,就會發送中斷訊號給 CPU;

CPU 收到 DMA 的訊號,知道資料已經Ready,於是將資料從核心複製到使用者空間,結束系統呼叫;

DMA技術就是釋放了CPU的佔用時間,它只做事件通知,資料複製完全由DMA完成。雖然DMA優化了CPU的利用率,但是並沒有提高資料讀取的效能。為了減少資料在2種狀態之間的切換次數,因為狀態切換是一個

非常、非常、非常繁重

的工作。為此,大佬們就提了零複製技術。

零複製技術實現的方式

常見的有2種,而今引入持久化記憶體後,還有APP直接訪問記憶體資料的方式,這裡先不展開。下面介紹常用的2種方案,它們的目的減少“上下文切換”和“資料複製”的次數。

mmap + write(系統呼叫)

sendfile

mmap + write

主要目的,減少資料的複製

read()

系統呼叫:把核心緩衝區的資料複製到使用者的緩衝區裡,用

mmap()

替換

read()

mmap()

直接把核心緩衝區裡的資料

對映

到使用者空間,減少這一次複製。

buf = mmap(file, len);write(sockfd, buf, len);

一文讀懂計算機核心態、使用者態和零複製技術

具體過程如下:

應用程序呼叫了

mmap()

後,DMA 會把磁碟的資料複製到核心的緩衝區裡。因為建立了這個記憶體的mapping,所以使用者態的資料可以直接訪問了;

應用程序再呼叫

write()

,CPU將核心緩衝區的資料複製到 socket 緩衝區中,這一切都發生在核心態

DMA把核心的 socket 緩衝區裡的資料,複製到網絡卡的緩衝區裡

由上可知,系統呼叫

mmap()

來代替

read()

, 可以減少一次資料複製。那我們是否還有最佳化的空間呢?畢竟使用者態和核心態仍然需要 4 次上下文切換,系統呼叫還是 2 次。那繼續研究下是否還能繼續減少切換和資料複製呢?答案是確定的:可以

sendfile

Linux 核心版本 2。1 提供了一個專門傳送檔案的系統呼叫函式

sendfile()

,函式形式如下:

#include ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

引數說明:

前2個引數分別是目的端和源端的檔案描述符,

後2個引數是源端的偏移量和複製資料的長度,返回值是實際複製資料的長度。

首先,使用sendfile()可以替代前面的

read()

write()

這兩個系統呼叫,減少一次系統呼叫和 2 次上下文切換。

其次,sendfile可以直接把核心緩衝區裡的資料複製到 socket 緩衝區裡,不再複製到使用者態,最佳化後只有 2 次上下文切換,和 3 次資料複製。如下圖:

一文讀懂計算機核心態、使用者態和零複製技術

儘管如此,我們還是又資料複製,這不符合我們的標題目標。如果網絡卡支援 SG-DMA(

The Scatter-Gather Direct Memory Access

)技術,我們就可以進一步減少透過 CPU 把核心緩衝區裡的資料複製到 socket 緩衝區的過程。

我們可以在 Linux 系統下透過下面的命令,檢視網絡卡是否支援 scatter-gather 特性:

$ ethtool -k eth0 | grep scatter-gatherscatter-gather: on

於是,從 Linux 核心

2。4

版本開始起,對於支援網絡卡支援 SG-DMA 技術的情況下,

sendfile()

系統呼叫的過程發生了點變化,具體過程如下:

透過 DMA 將磁碟上的資料複製到核心緩衝區裡;

緩衝區描述符和資料長度傳到 socket 緩衝區,這樣網絡卡的 SG-DMA 控制器就可以直接將核心快取中的資料複製到網絡卡的緩衝區裡;

在這個過程之中,實際上只進行了 2 次資料複製,如下圖:

一文讀懂計算機核心態、使用者態和零複製技術

這就是

零複製(Zero-copy)技術,因為我們沒有在記憶體層面去複製資料,也就是說全程沒有透過 CPU 來搬運資料,所有的資料都是透過 DMA 來進行傳輸的。

零複製技術的檔案傳輸方式相比傳統檔案傳輸的方式,

只需要 2 次上下文切換和資料複製次數,就可以完成檔案的傳輸,而且 2 次的資料複製過程,都不需要透過 CPU,2 次都是由 DMA 來搬運。

所以,

零複製技術可以把檔案傳輸的效能提高至少一倍。

為啥要聊PageCache?

回顧第一節的儲存介質的效能,如果我們總是在磁碟和記憶體間傳輸資料,一個大檔案的跨機器傳輸肯定會讓你抓狂。那有什麼方法加速呢?直觀的想法就是建立一個離CPU近的一個臨時通道,這樣就可以加速檔案的傳輸。 這個通道就是我們前文提到的「核心緩衝區」,這個「核心緩衝區」實際上是

磁碟快取記憶體(PageCache)

零複製就是使用了DMA + PageCache 技術提升了效能,我們來看看 PageCache 是如何做到的。

從開篇的介質效能看,磁碟相比記憶體讀寫的速度要慢很多,所以最佳化的思路就是儘量的把「讀寫磁碟」替換成「讀寫記憶體」。因此透過 DMA 把磁盤裡的資料搬運到記憶體裡,轉為直接讀記憶體,這樣就快多了。但是記憶體的空間是有限的,成本也比磁碟貴,它只能複製磁盤裡的一小部分資料。

那就不可避免的產生一個問題,到底選擇哪些磁碟資料複製到記憶體呢?

從業務的視角來看,業務的資料有冷熱之分,我們透過一些的淘汰演算法可以知道哪些是熱資料,因為資料訪問的時序性,被訪問過的資料可能被再次訪問的機率很高,於是我們可以用

PageCache 來快取最近被訪問的資料

,當空間不足時淘汰最久未被訪問的資料。

讀Cache

當核心發起一個讀請求時(例如程序發起read()請求),首先會檢查請求的資料是否快取到了Page Cache中。如果有,那麼直接從記憶體中讀取,不需要訪問磁碟,這被稱為cache命中(cache hit);如果cache中沒有請求的資料,即cache未命中(cache miss),就必須從磁碟中讀取資料。然後核心將讀取的資料快取到cache中,這樣後續的讀請求就可以命中cache了。

page可以只快取一個檔案部分的內容,不需要把整個檔案都快取進來。

寫Cache

當核心發起一個寫請求時(例如程序發起write()請求),同樣是直接往cache中寫入,後備儲存中的內容不會直接更新(當伺服器出現斷電關機時,存在資料丟失風險)。

核心會將被寫入的page標記為dirty,並將其加入dirty list中。核心會週期性地將dirty list中的page寫回到磁碟上,從而使磁碟上的資料和記憶體中快取的資料一致。

當滿足以下兩個條件之一將觸發髒資料重新整理到磁碟操作:

資料存在的時間超過了dirty_expire_centisecs(預設300釐秒,即30秒)時間;

髒資料所佔記憶體 > dirty_background_ratio,也就是說當髒資料所佔用的記憶體佔總記憶體的比例超過dirty_background_ratio(預設10,即系統記憶體的10%)的時候會觸發pdflush重新整理髒資料。

還有一點,現在的磁碟是擦除式讀寫,每次需要讀一個固定的大小,隨機讀取帶來的磁頭定址會增加時延,為了降低它的影響,

PageCache 使用了「預讀功能」

在某些應用場景下,比如我們每次開啟檔案只需要讀取或者寫入幾個位元組的情況,會比Direct I/O多一些磁碟的讀取於寫入。

舉個例子,假設每次我們要讀

32 KB

的位元組,read填充到使用者buffer的大小是0~32KB,但核心會把其後面的 32~64 KB 也讀取到 PageCache,這樣後面讀取 32~64 KB 的成本就很低,如果在 32~64 KB 淘汰出 PageCache 前,程序需要讀這些資料,對比分塊讀取的方式,這個策略收益就非常大。

Page Cache的優勢與劣勢

優勢

加快對資料的訪問

減少磁碟I/O的訪問次數,提高系統磁碟壽命

減少對磁碟I/O的訪問,提高系統磁碟I/O吞吐量(Page Cache的預讀機制)

劣勢

使用額外的物理記憶體空間,當物理記憶體比較緊俏的時候,可能會導致頻繁的swap操作,最終會導致系統的磁碟I/O負載上升。

Page Cache沒有給應用層提供一個很好的API。導致應用層想要最佳化Page Cache的使用策略很難。因此一些應用實現了自己的Page管理,比如MySQL的InnoDB儲存引擎以16KB的頁進行管理。

另外,由於檔案太大,可能某些部分的檔案資料已經被淘汰出去了,這樣就會帶來 2 個問題:

PageCache 由於長時間被大檔案的部分塊佔據,而導致一些「熱點」的小檔案可能就無法常駐 PageCache,導致頻繁讀寫磁碟而引起效能下降;

PageCache 中的大檔案資料,由於沒有全部常駐記憶體,只有部分無法享受到快取帶來的好處,同時過多的DMA 複製動作,增加了時延;

因此針對大檔案的傳輸,不應該使用 PageCache。

Page Cache快取檢視工具:

cachestat

PageCache的引數調優

備註:不同硬體配置的伺服器可能效果不同,所以,具體的引數值設定需要考慮自己叢集硬體配置。

考慮的因素主要包括:CPU核數、記憶體大小、硬碟型別、網路頻寬等。

檢視Page Cache引數: sysctl -a|grep dirty

調整核心引數來最佳化IO效能?

vm。dirty_background_ratio引數最佳化:當cached中快取當資料佔總記憶體的比例達到這個引數設定的值時將觸發刷磁碟操作。把這個引數適當調小,這樣可以把原來一個大的IO刷盤操作變為多個小的IO刷盤操作,從而把IO寫峰值削平。對於記憶體很大和磁碟效能比較差的伺服器,應該把這個值設定的小一點。

vm。dirty_ratio引數最佳化:對於寫壓力特別大的,建議把這個引數適當調大;對於寫壓力小的可以適當調小;如果cached的資料所佔比例(這裡是佔總記憶體的比例)超過這個設定,系統會停止所有的應用層的IO寫操作,等待刷完資料後恢復IO。所以萬一觸發了系統的這個操作,對於使用者來說影響非常大的。

vm。dirty_expire_centisecs引數最佳化:這個引數會和引數vm。dirty_background_ratio一起來作用,一個表示大小比例,一個表示時間;即滿足其中任何一個的條件都達到刷盤的條件。

vm。dirty_writeback_centisecs引數最佳化:理論上調小這個引數,可以提高刷磁碟的頻率,從而儘快把髒資料重新整理到磁碟上。但一定要保證間隔時間內一定可以讓資料刷盤完成。

vm。swappiness引數最佳化:禁用swap空間,設定vm。swappiness=0

大檔案傳輸怎麼做?

我們先來回顧下前文的讀流程,當呼叫 read 方法讀取檔案時,如果資料沒有準備好,程序會阻塞在 read 方法呼叫,要等待磁碟資料的返回,如下圖:

一文讀懂計算機核心態、使用者態和零複製技術

具體過程:

當呼叫 read 方法時,切到核心態訪問磁碟資源。此時核心會向磁碟發起 I/O 請求,磁碟收到請求後,準備資料。資料讀取到控制器緩衝區完成後,就會向核心發起 I/O 中斷,通知核心磁碟資料已經準備好;

核心收到 I/O 中斷後,將資料從磁碟控制器緩衝區複製到 PageCache 裡;

核心把 PageCache 中的資料複製到使用者緩衝區,read 呼叫返回成功。

對於大塊數傳輸導致的阻塞,可以用非同步 I/O 來解決,如下圖:

一文讀懂計算機核心態、使用者態和零複製技術

分為兩步執行:

核心向磁碟發起讀請求,因為是非同步請求可以

不等待資料就位就可以返回

,於是CPU釋放出來可以處理其他任務;

當核心將磁碟中的資料複製到程序緩衝區後,程序將接收到核心的

通知

,再去處理資料;

從上面流程來看,非同步 I/O 並沒有讀寫 PageCache,繞開 PageCache 的 I/O 叫直接 I/O,使用 PageCache 的 I/O 則叫快取 I/O。通常,對於磁碟非同步 I/O 只支援直接 I/O。

因此,在高併發的場景下,針對大檔案的傳輸的方式,應該使用「非同步 I/O + 直接 I/O」來替代零複製技術

直接 I/O 的兩種場景:

應用程式已經實現了磁碟資料的快取

大檔案傳輸