https://m.toutiao.com/is/Jx4qMgr/
作者:allanpan,騰訊 IEG 后臺開(kāi)發(fā)工程師
三萬(wàn)字長(cháng)文從虛擬內存、I/O 緩沖區,用戶(hù)態(tài)&內核態(tài)以及 I/O 模式等等知識點(diǎn)全面而又詳盡地剖析 Linux 系統的 I/O 底層原理,分析了 Linux 傳統的 I/O 模式的弊端,進(jìn)而引入 Linux Zero-copy 零拷貝技術(shù)的介紹和原理解析,將零拷貝技術(shù)和傳統的 I/O 模式進(jìn)行區分和對比,幫助讀者理解 Linux 內核對 I/O 模塊的優(yōu)化改進(jìn)思路。全網(wǎng)最深度和詳盡的 Linux I/O 及零拷貝技術(shù)的解析文章
如今的網(wǎng)絡(luò )應用早已從 CPU 密集型轉向了 I/O 密集型,網(wǎng)絡(luò )服務(wù)器大多是基于 C-S 模型,也即 客戶(hù)端 - 服務(wù)端 模型,客戶(hù)端需要和服務(wù)端進(jìn)行大量的網(wǎng)絡(luò )通信,這也決定了現代網(wǎng)絡(luò )應用的性能瓶頸:I/O。
傳統的 Linux 操作系統的標準 I/O 接口是基于數據拷貝操作的,即 I/O 操作會(huì )導致數據在操作系統內核地址空間的緩沖區和用戶(hù)進(jìn)程地址空間定義的緩沖區之間進(jìn)行傳輸。設置緩沖區最大的好處是可以減少磁盤(pán) I/O 的操作,如果所請求的數據已經(jīng)存放在操作系統的高速緩沖存儲器中,那么就不需要再進(jìn)行實(shí)際的物理磁盤(pán) I/O 操作;然而傳統的 Linux I/O 在數據傳輸過(guò)程中的數據拷貝操作深度依賴(lài) CPU,也就是說(shuō) I/O 過(guò)程需要 CPU 去執行數據拷貝的操作,因此導致了極大的系統開(kāi)銷(xiāo),限制了操作系統有效進(jìn)行數據傳輸操作的能力。
I/O 是決定網(wǎng)絡(luò )服務(wù)器性能瓶頸的關(guān)鍵,而傳統的 Linux I/O 機制又會(huì )導致大量的數據拷貝操作,損耗性能,所以我們亟需一種新的技術(shù)來(lái)解決數據大量拷貝的問(wèn)題,這個(gè)答案就是零拷貝(Zero-copy)。
既然要分析 Linux I/O,就不能不了解計算機的各類(lèi)存儲器。
存儲器是計算機的核心部件之一,在完全理想的狀態(tài)下,存儲器應該要同時(shí)具備以下三種特性:
但是現實(shí)往往是殘酷的,我們目前的計算機技術(shù)無(wú)法同時(shí)滿(mǎn)足上述的三個(gè)條件,于是現代計算機的存儲器設計采用了一種分層次的結構:
從頂至底,現代計算機里的存儲器類(lèi)型分別有:寄存器、高速緩存、主存和磁盤(pán),這些存儲器的速度逐級遞減而容量逐級遞增。存取速度最快的是寄存器,因為寄存器的制作材料和 CPU 是相同的,所以速度和 CPU 一樣快,CPU 訪(fǎng)問(wèn)寄存器是沒(méi)有時(shí)延的,然而因為價(jià)格昂貴,因此容量也極小,一般 32 位的 CPU 配備的寄存器容量是 32??32 Bit,64 位的 CPU 則是 64??64 Bit,不管是 32 位還是 64 位,寄存器容量都小于 1 KB,且寄存器也必須通過(guò)軟件自行管理。
第二層是高速緩存,也即我們平時(shí)了解的 CPU 高速緩存 L1、L2、L3,一般 L1 是每個(gè) CPU 獨享,L3 是全部 CPU 共享,而 L2 則根據不同的架構設計會(huì )被設計成獨享或者共享兩種模式之一,比如 Intel 的多核芯片采用的是共享 L2 模式而 AMD 的多核芯片則采用的是獨享 L2 模式。
第三層則是主存,也即主內存,通常稱(chēng)作隨機訪(fǎng)問(wèn)存儲器(Random Access Memory, RAM)。是與 CPU 直接交換數據的內部存儲器。它可以隨時(shí)讀寫(xiě)(刷新時(shí)除外),而且速度很快,通常作為操作系統或其他正在運行中的程序的臨時(shí)資料存儲介質(zhì)。
最后則是磁盤(pán),磁盤(pán)和主存相比,每個(gè)二進(jìn)制位的成本低了兩個(gè)數量級,因此容量比之會(huì )大得多,動(dòng)輒上 GB、TB,而問(wèn)題是訪(fǎng)問(wèn)速度則比主存慢了大概三個(gè)數量級。機械硬盤(pán)速度慢主要是因為機械臂需要不斷在金屬盤(pán)片之間移動(dòng),等待磁盤(pán)扇區旋轉至磁頭之下,然后才能進(jìn)行讀寫(xiě)操作,因此效率很低。
主內存是操作系統進(jìn)行 I/O 操作的重中之重,絕大部分的工作都是在用戶(hù)進(jìn)程和內核的內存緩沖區里完成的,因此我們接下來(lái)需要提前學(xué)習一些主存的相關(guān)原理。
我們平時(shí)一直提及的物理內存就是上文中對應的第三種計算機存儲器,RAM 主存,它在計算機中以?xún)却鏃l的形式存在,嵌在主板的內存槽上,用來(lái)加載各式各樣的程序與數據以供 CPU 直接運行和使用。
在計算機領(lǐng)域有一句如同摩西十誡般神圣的哲言:'計算機科學(xué)領(lǐng)域的任何問(wèn)題都可以通過(guò)增加一個(gè)間接的中間層來(lái)解決',從內存管理、網(wǎng)絡(luò )模型、并發(fā)調度甚至是硬件架構,都能看到這句哲言在閃爍著(zhù)光芒,而虛擬內存則是這一哲言的完美實(shí)踐之一。
虛擬內存是現代計算機中的一個(gè)非常重要的存儲器抽象,主要是用來(lái)解決應用程序日益增長(cháng)的內存使用需求:現代物理內存的容量增長(cháng)已經(jīng)非??焖倭?,然而還是跟不上應用程序對主存需求的增長(cháng)速度,對于應用程序來(lái)說(shuō)內存還是不夠用,因此便需要一種方法來(lái)解決這兩者之間的容量差矛盾。
計算機對多程序內存訪(fǎng)問(wèn)的管理經(jīng)歷了 靜態(tài)重定位 --> 動(dòng)態(tài)重定位 --> 交換(swapping)技術(shù) --> 虛擬內存,最原始的多程序內存訪(fǎng)問(wèn)是直接訪(fǎng)問(wèn)絕對內存地址,這種方式幾乎是完全不可用的方案,因為如果每一個(gè)程序都直接訪(fǎng)問(wèn)物理內存地址的話(huà),比如兩個(gè)程序并發(fā)執行以下指令的時(shí)候:
mov cx, 2mov bx, 1000Hmov ds, bxmov [0], cx...mov ax, [0]add ax, ax這一段匯編表示在地址 1000:0 處存入數值 2,然后在后面的邏輯中把該地址的值取出來(lái)乘以 2,最終存入 ax 寄存器的值就是 4,如果第二個(gè)程序存入 cx 寄存器里的值是 3,那么并發(fā)執行的時(shí)候,第一個(gè)程序最終從 ax 寄存器里得到的值就可能是 6,這就完全錯誤了,得到臟數據還頂多算程序結果錯誤,要是其他程序往特定的地址里寫(xiě)入一些危險的指令而被另一個(gè)程序取出來(lái)執行,還可能會(huì )導致整個(gè)系統的崩潰。所以,為了確保進(jìn)程間互不干擾,每一個(gè)用戶(hù)進(jìn)程都需要實(shí)時(shí)知曉當前其他進(jìn)程在使用哪些內存地址,這對于寫(xiě)程序的人來(lái)說(shuō)無(wú)疑是一場(chǎng)噩夢(mèng)。
因此,操作絕對內存地址是完全不可行的方案,那就只能用操作相對內存地址,我們知道每個(gè)進(jìn)程都會(huì )有自己的進(jìn)程地址,從 0 開(kāi)始,可以通過(guò)相對地址來(lái)訪(fǎng)問(wèn)內存,但是這同樣有問(wèn)題,還是前面類(lèi)似的問(wèn)題,比如有兩個(gè)大小為 16KB 的程序 A 和 B,現在它們都被加載進(jìn)了內存,內存地址段分別是 0 ~ 16384,16384 ~ 32768。A 的第一條指令是 jmp 1024,而在地址 1024 處是一條 mov 指令,下一條指令是 add,基于前面的 mov 指令做加法運算,與此同時(shí),B 的第一條指令是 jmp 1028,本來(lái)在 B 的相對地址 1028 處應該也是一條 mov 去操作自己的內存地址上的值,但是由于這兩個(gè)程序共享了段寄存器,因此雖然他們使用了各自的相對地址,但是依然操作的還是絕對內存地址,于是 B 就會(huì )跳去執行 add 指令,這時(shí)候就會(huì )因為非法的內存操作而 crash。
有一種靜態(tài)重定位的技術(shù)可以解決這個(gè)問(wèn)題,它的工作原理非常簡(jiǎn)單粗暴:當 B 程序被加載到地址 16384 處之后,把 B 的所有相對內存地址都加上 16384,這樣的話(huà)當 B 執行 jmp 1028 之時(shí),其實(shí)執行的是 jmp 1028+16384,就可以跳轉到正確的內存地址處去執行正確的指令了,但是這種技術(shù)并不通用,而且還會(huì )對程序裝載進(jìn)內存的性能有影響。
再往后,就發(fā)展出來(lái)了存儲器抽象:地址空間,就好像進(jìn)程是 CPU 的抽象,地址空間則是存儲器的抽象,每個(gè)進(jìn)程都會(huì )分配獨享的地址空間,但是獨享的地址空間又帶來(lái)了新的問(wèn)題:如何實(shí)現不同進(jìn)程的相同相對地址指向不同的物理地址?最開(kāi)始是使用動(dòng)態(tài)重定位技術(shù)來(lái)實(shí)現,這是用一種相對簡(jiǎn)單的地址空間到物理內存的映射方法。
基本原理就是為每一個(gè) CPU 配備兩個(gè)特殊的硬件寄存器:基址寄存器和界限寄存器,用來(lái)動(dòng)態(tài)保存每一個(gè)程序的起始物理內存地址和長(cháng)度,比如前文中的 A,B 兩個(gè)程序,當 A 運行時(shí)基址寄存器和界限寄存器就會(huì )分別存入 0 和 16384,而當 B 運行時(shí)則兩個(gè)寄存器又會(huì )分別存入 16384 和 32768。然后每次訪(fǎng)問(wèn)指定的內存地址時(shí),CPU 會(huì )在把地址發(fā)往內存總線(xiàn)之前自動(dòng)把基址寄存器里的值加到該內存地址上,得到一個(gè)真正的物理內存地址,同時(shí)還會(huì )根據界限寄存器里的值檢查該地址是否溢出,若是,則產(chǎn)生錯誤中止程序,動(dòng)態(tài)重定位技術(shù)解決了靜態(tài)重定位技術(shù)造成的程序裝載速度慢的問(wèn)題,但是也有新問(wèn)題:每次訪(fǎng)問(wèn)內存都需要進(jìn)行加法和比較運算,比較運算本身可以很快,但是加法運算由于進(jìn)位傳遞時(shí)間的問(wèn)題,除非使用特殊的電路,否則會(huì )比較慢。
然后就是 交換(swapping)技術(shù),這種技術(shù)簡(jiǎn)單來(lái)說(shuō)就是動(dòng)態(tài)地把程序在內存和磁盤(pán)之間進(jìn)行交換保存,要運行一個(gè)進(jìn)程的時(shí)候就把程序的代碼段和數據段調入內存,然后再把程序封存,存入磁盤(pán),如此反復。為什么要這么麻煩?因為前面那兩種重定位技術(shù)的前提條件是計算機內存足夠大,能夠把所有要運行的進(jìn)程地址空間都加載進(jìn)主存,才能夠并發(fā)運行這些進(jìn)程,但是現實(shí)往往不是如此,內存的大小總是有限的,所有就需要另一類(lèi)方法來(lái)處理內存超載的情況,第一種便是簡(jiǎn)單的交換技術(shù):
先把進(jìn)程 A 換入內存,然后啟動(dòng)進(jìn)程 B 和 C,也換入內存,接著(zhù) A 被從內存交換到磁盤(pán),然后又有新的進(jìn)程 D 調入內存,用了 A 退出之后空出來(lái)的內存空間,最后 A 又被重新?lián)Q入內存,由于內存布局已經(jīng)發(fā)生了變化,所以 A 在換入內存之時(shí)會(huì )通過(guò)軟件或者在運行期間通過(guò)硬件(基址寄存器和界限寄存器)對其內存地址進(jìn)行重定位,多數情況下都是通過(guò)硬件。
另一種處理內存超載的技術(shù)就是虛擬內存技術(shù)了,它比交換(swapping)技術(shù)更復雜而又更高效,是目前最新應用最廣泛的存儲器抽象技術(shù):
虛擬內存的核心原理是:為每個(gè)程序設置一段'連續'的虛擬地址空間,把這個(gè)地址空間分割成多個(gè)具有連續地址范圍的頁(yè) (page),并把這些頁(yè)和物理內存做映射,在程序運行期間動(dòng)態(tài)映射到物理內存。當程序引用到一段在物理內存的地址空間時(shí),由硬件立刻執行必要的映射;而當程序引用到一段不在物理內存中的地址空間時(shí),由操作系統負責將缺失的部分裝入物理內存并重新執行失敗的指令:
虛擬地址空間按照固定大小劃分成被稱(chēng)為頁(yè)(page)的若干單元,物理內存中對應的則是頁(yè)框(page frame)。這兩者一般來(lái)說(shuō)是一樣的大小,如上圖中的是 4KB,不過(guò)實(shí)際上計算機系統中一般是 512 字節到 1 GB,這就是虛擬內存的分頁(yè)技術(shù)。因為是虛擬內存空間,每個(gè)進(jìn)程分配的大小是 4GB (32 位架構),而實(shí)際上當然不可能給所有在運行中的進(jìn)程都分配 4GB 的物理內存,所以虛擬內存技術(shù)還需要利用到前面介紹的交換(swapping)技術(shù),在進(jìn)程運行期間只分配映射當前使用到的內存,暫時(shí)不使用的數據則寫(xiě)回磁盤(pán)作為副本保存,需要用的時(shí)候再讀入內存,動(dòng)態(tài)地在磁盤(pán)和內存之間交換數據。
其實(shí)虛擬內存技術(shù)從某種角度來(lái)看的話(huà),很像是糅合了基址寄存器和界限寄存器之后的新技術(shù)。它使得整個(gè)進(jìn)程的地址空間可以通過(guò)較小的單元映射到物理內存,而不需要為程序的代碼和數據地址進(jìn)行重定位。
進(jìn)程在運行期間產(chǎn)生的內存地址都是虛擬地址,如果計算機沒(méi)有引入虛擬內存這種存儲器抽象技術(shù)的話(huà),則 CPU 會(huì )把這些地址直接發(fā)送到內存地址總線(xiàn)上,直接訪(fǎng)問(wèn)和虛擬地址相同值的物理地址;如果使用虛擬內存技術(shù)的話(huà),CPU 則是把這些虛擬地址通過(guò)地址總線(xiàn)送到內存管理單元(Memory Management Unit,MMU),MMU 將虛擬地址映射為物理地址之后再通過(guò)內存總線(xiàn)去訪(fǎng)問(wèn)物理內存:
虛擬地址(比如 16 位地址 8196=0010 000000000100)分為兩部分:虛擬頁(yè)號(高位部分)和偏移量(低位部分),虛擬地址轉換成物理地址是通過(guò)頁(yè)表(page table)來(lái)實(shí)現的,頁(yè)表由頁(yè)表項構成,頁(yè)表項中保存了頁(yè)框號、修改位、訪(fǎng)問(wèn)位、保護位和 '在/不在' 位等信息,從數學(xué)角度來(lái)說(shuō)頁(yè)表就是一個(gè)函數,入參是虛擬頁(yè)號,輸出是物理頁(yè)框號,得到物理頁(yè)框號之后復制到寄存器的高三位中,最后直接把 12 位的偏移量復制到寄存器的末 12 位構成 15 位的物理地址,即可以把該寄存器的存儲的物理內存地址發(fā)送到內存總線(xiàn):
在 MMU 進(jìn)行地址轉換時(shí),如果頁(yè)表項的 '在/不在' 位是 0,則表示該頁(yè)面并沒(méi)有映射到真實(shí)的物理頁(yè)框,則會(huì )引發(fā)一個(gè)缺頁(yè)中斷,CPU 陷入操作系統內核,接著(zhù)操作系統就會(huì )通過(guò)頁(yè)面置換算法選擇一個(gè)頁(yè)面將其換出 (swap),以便為即將調入的新頁(yè)面騰出位置,如果要換出的頁(yè)面的頁(yè)表項里的修改位已經(jīng)被設置過(guò),也就是被更新過(guò),則這是一個(gè)臟頁(yè) (dirty page),需要寫(xiě)回磁盤(pán)更新改頁(yè)面在磁盤(pán)上的副本,如果該頁(yè)面是'干凈'的,也就是沒(méi)有被修改過(guò),則直接用調入的新頁(yè)面覆蓋掉被換出的舊頁(yè)面即可。
最后,還需要了解的一個(gè)概念是轉換檢測緩沖器(Translation Lookaside Buffer,TLB),也叫快表,是用來(lái)加速虛擬地址映射的,因為虛擬內存的分頁(yè)機制,頁(yè)表一般是保存內存中的一塊固定的存儲區,導致進(jìn)程通過(guò) MMU 訪(fǎng)問(wèn)內存比直接訪(fǎng)問(wèn)內存多了一次內存訪(fǎng)問(wèn),性能至少下降一半,因此需要引入加速機制,即 TLB 快表,TLB 可以簡(jiǎn)單地理解成頁(yè)表的高速緩存,保存了最高頻被訪(fǎng)問(wèn)的頁(yè)表項,由于一般是硬件實(shí)現的,因此速度極快,MMU 收到虛擬地址時(shí)一般會(huì )先通過(guò)硬件 TLB 查詢(xún)對應的頁(yè)表號,若命中且該頁(yè)表項的訪(fǎng)問(wèn)操作合法,則直接從 TLB 取出對應的物理頁(yè)框號返回,若不命中則穿透到內存頁(yè)表里查詢(xún),并且會(huì )用這個(gè)從內存頁(yè)表里查詢(xún)到最新頁(yè)表項替換到現有 TLB 里的其中一個(gè),以備下次緩存命中。
至此,我們介紹完了包含虛擬內存在內的多項計算機存儲器抽象技術(shù),虛擬內存的其他內容比如針對大內存的多級頁(yè)表、倒排頁(yè)表,以及處理缺頁(yè)中斷的頁(yè)面置換算法等等,以后有機會(huì )再單獨寫(xiě)一篇文章介紹,或者各位讀者也可以先行去查閱相關(guān)資料了解,這里就不再深入了。
一般來(lái)說(shuō),我們在編寫(xiě)程序操作 Linux I/O 之時(shí)十有八九是在用戶(hù)空間和內核空間之間傳輸數據,因此有必要先了解一下 Linux 的用戶(hù)態(tài)和內核態(tài)的概念。
首先是用戶(hù)態(tài)和內核態(tài):

從宏觀(guān)上來(lái)看,Linux 操作系統的體系架構分為用戶(hù)態(tài)和內核態(tài)(或者用戶(hù)空間和內核)。內核從本質(zhì)上看是一種軟件 —— 控制計算機的硬件資源,并提供上層應用程序 (進(jìn)程) 運行的環(huán)境。用戶(hù)態(tài)即上層應用程序 (進(jìn)程) 的運行空間,應用程序 (進(jìn)程) 的執行必須依托于內核提供的資源,這其中包括但不限于 CPU 資源、存儲資源、I/O 資源等等。
現代操作系統都是采用虛擬存儲器,那么對 32 位操作系統而言,它的尋址空間(虛擬存儲空間)為 2^32 B = 4G。操作系統的核心是內核,獨立于普通的應用程序,可以訪(fǎng)問(wèn)受保護的內存空間,也有訪(fǎng)問(wèn)底層硬件設備的所有權限。為了保證用戶(hù)進(jìn)程不能直接操作內核(kernel),保證內核的安全,操心系統將虛擬空間劃分為兩部分,一部分為內核空間,一部分為用戶(hù)空間。針對 Linux 操作系統而言,將最高的 1G 字節(從虛擬地址 0xC0000000 到 0xFFFFFFFF),供內核使用,稱(chēng)為內核空間,而將較低的 3G 字節(從虛擬地址 0x00000000 到 0xBFFFFFFF),供各個(gè)進(jìn)程使用,稱(chēng)為用戶(hù)空間。
因為操作系統的資源是有限的,如果訪(fǎng)問(wèn)資源的操作過(guò)多,必然會(huì )消耗過(guò)多的系統資源,而且如果不對這些操作加以區分,很可能造成資源訪(fǎng)問(wèn)的沖突。所以,為了減少有限資源的訪(fǎng)問(wèn)和使用沖突,Unix/Linux 的設計哲學(xué)之一就是:對不同的操作賦予不同的執行等級,就是所謂特權的概念。簡(jiǎn)單說(shuō)就是有多大能力做多大的事,與系統相關(guān)的一些特別關(guān)鍵的操作必須由最高特權的程序來(lái)完成。Intel 的 x86 架構的 CPU 提供了 0 到 3 四個(gè)特權級,數字越小,特權越高,Linux 操作系統中主要采用了 0 和 3 兩個(gè)特權級,分別對應的就是內核態(tài)和用戶(hù)態(tài)。
運行于用戶(hù)態(tài)的進(jìn)程可以執行的操作和訪(fǎng)問(wèn)的資源都會(huì )受到極大的限制,而運行在內核態(tài)的進(jìn)程則可以執行任何操作并且在資源的使用上沒(méi)有限制。很多程序開(kāi)始時(shí)運行于用戶(hù)態(tài),但在執行的過(guò)程中,一些操作需要在內核權限下才能執行,這就涉及到一個(gè)從用戶(hù)態(tài)切換到內核態(tài)的過(guò)程。比如 C 函數庫中的內存分配函數 malloc(),它具體是使用 sbrk() 系統調用來(lái)分配內存,當 malloc 調用 sbrk() 的時(shí)候就涉及一次從用戶(hù)態(tài)到內核態(tài)的切換,類(lèi)似的函數還有 printf(),調用的是 wirte() 系統調用來(lái)輸出字符串,等等。
用戶(hù)進(jìn)程在系統中運行時(shí),大部分時(shí)間是處在用戶(hù)態(tài)空間里的,在其需要操作系統幫助完成一些用戶(hù)態(tài)沒(méi)有特權和能力完成的操作時(shí)就需要切換到內核態(tài)。那么用戶(hù)進(jìn)程如何切換到內核態(tài)去使用那些內核資源呢?答案是:1) 系統調用(trap),2) 異常(exception)和 3) 中斷(interrupt)。
通過(guò)上面的分析,我們可以得出 Linux 的內部層級可分為三大部分:


在 Linux 中,當程序調用各類(lèi)文件操作函數后,用戶(hù)數據(User Data)到達磁盤(pán)(Disk)的流程如上圖所示。
圖中描述了 Linux 中文件操作函數的層級關(guān)系和內存緩存層的存在位置,中間的黑色實(shí)線(xiàn)是用戶(hù)態(tài)和內核態(tài)的分界線(xiàn)。
read(2)/write(2) 是 Linux 系統中最基本的 I/O 讀寫(xiě)系統調用,我們開(kāi)發(fā)操作 I/O 的程序時(shí)必定會(huì )接觸到它們,而在這兩個(gè)系統調用和真實(shí)的磁盤(pán)讀寫(xiě)之間存在一層稱(chēng)為 Kernel buffer cache 的緩沖區緩存。在 Linux 中 I/O 緩存其實(shí)可以細分為兩個(gè):Page Cache 和 Buffer Cache,這兩個(gè)其實(shí)是一體兩面,共同組成了 Linux 的內核緩沖區(Kernel Buffer Cache):
Page Cache 會(huì )通過(guò)頁(yè)面置換算法如 LRU 定期淘汰舊的頁(yè)面,加載新的頁(yè)面??梢钥闯?,所謂 I/O 緩沖區緩存就是在內核和磁盤(pán)、網(wǎng)卡等外設之間的一層緩沖區,用來(lái)提升讀寫(xiě)性能的。
在 Linux 還不支持虛擬內存技術(shù)之前,還沒(méi)有頁(yè)的概念,因此 Buffer Cache 是基于操作系統讀寫(xiě)磁盤(pán)的最小單位 -- 塊(block)來(lái)進(jìn)行的,所有的磁盤(pán)塊操作都是通過(guò) Buffer Cache 來(lái)加速,Linux 引入虛擬內存的機制來(lái)管理內存后,頁(yè)成為虛擬內存管理的最小單位,因此也引入了 Page Cache 來(lái)緩存 Linux 文件內容,主要用來(lái)作為文件系統上的文件數據的緩存,提升讀寫(xiě)性能,常見(jiàn)的是針對文件的 read()/write() 操作,另外也包括了通過(guò) mmap() 映射之后的塊設備,也就是說(shuō),事實(shí)上 Page Cache 負責了大部分的塊設備文件的緩存工作。而 Buffer Cache 用來(lái)在系統對塊設備進(jìn)行讀寫(xiě)的時(shí)候,對塊進(jìn)行數據緩存的系統來(lái)使用,實(shí)際上負責所有對磁盤(pán)的 I/O 訪(fǎng)問(wèn):

因為 Buffer Cache 是對粒度更細的設備塊的緩存,而 Page Cache 是基于虛擬內存的頁(yè)單元緩存,因此還是會(huì )基于 Buffer Cache,也就是說(shuō)如果是緩存文件內容數據就會(huì )在內存里緩存兩份相同的數據,這就會(huì )導致同一份文件保存了兩份,冗余且低效。另外一個(gè)問(wèn)題是,調用 write 后,有效數據是在 Buffer Cache 中,而非 Page Cache 中。這就導致 mmap訪(fǎng)問(wèn)的文件數據可能存在不一致問(wèn)題。為了規避這個(gè)問(wèn)題,所有基于磁盤(pán)文件系統的 write,都需要調用 update_vm_cache() 函數,該操作會(huì )把調用 write 之后的 Buffer Cache 更新到 Page Cache 去。由于有這些設計上的弊端,因此在 Linux 2.4 版本之后,kernel 就將兩者進(jìn)行了統一,Buffer Cache 不再以獨立的形式存在,而是以融合的方式存在于 Page Cache 中:

融合之后就可以統一操作 Page Cache 和 Buffer Cache:處理文件 I/O 緩存交給 Page Cache,而當底層 RAW device 刷新數據時(shí)以 Buffer Cache 的塊單位來(lái)實(shí)際處理。
在 Linux 或者其他 Unix-like 操作系統里,I/O 模式一般有三種:
下面我分別詳細地講解一下這三種 I/O 模式。
這是最簡(jiǎn)單的一種 I/O 模式,也叫忙等待或者輪詢(xún):用戶(hù)通過(guò)發(fā)起一個(gè)系統調用,陷入內核態(tài),內核將系統調用翻譯成一個(gè)對應設備驅動(dòng)程序的過(guò)程調用,接著(zhù)設備驅動(dòng)程序會(huì )啟動(dòng) I/O 不斷循環(huán)去檢查該設備,看看是否已經(jīng)就緒,一般通過(guò)返回碼來(lái)表示,I/O 結束之后,設備驅動(dòng)程序會(huì )把數據送到指定的地方并返回,切回用戶(hù)態(tài)。
比如發(fā)起系統調用 read():

第二種 I/O 模式是利用中斷來(lái)實(shí)現的:

流程如下:
并發(fā)系統的性能高低究其根本,是取決于如何對 CPU 資源的高效調度和使用,而回頭看前面的中斷驅動(dòng) I/O 模式的流程,可以發(fā)現第 6、7 步的數據拷貝工作都是由 CPU 親自完成的,也就是在這兩次數據拷貝階段中 CPU 是完全被占用而不能處理其他工作的,那么這里明顯是有優(yōu)化空間的;第 7 步的數據拷貝是從內核緩沖區到用戶(hù)緩沖區,都是在主存里,所以這一步只能由 CPU 親自完成,但是第 6 步的數據拷貝,是從磁盤(pán)控制器的緩沖區到主存,是兩個(gè)設備之間的數據傳輸,這一步并非一定要 CPU 來(lái)完成,可以借助 DMA 來(lái)完成,減輕 CPU 的負擔。
DMA 全稱(chēng)是 Direct Memory Access,也即直接存儲器存取,是一種用來(lái)提供在外設和存儲器之間或者存儲器和存儲器之間的高速數據傳輸。整個(gè)過(guò)程無(wú)須 CPU 參與,數據直接通過(guò) DMA 控制器進(jìn)行快速地移動(dòng)拷貝,節省 CPU 的資源去做其他工作。
目前,大部分的計算機都配備了 DMA 控制器,而 DMA 技術(shù)也支持大部分的外設和存儲器。借助于 DMA 機制,計算機的 I/O 過(guò)程就能更加高效:

DMA 控制器內部包含若干個(gè)可以被 CPU 讀寫(xiě)的寄存器:一個(gè)主存地址寄存器 MAR(存放要交換數據的主存地址)、一個(gè)外設地址寄存器 ADR(存放 I/O 設備的設備碼,或者是設備信息存儲區的尋址信息)、一個(gè)字節數寄存器 WC(對傳送數據的總字數進(jìn)行統計)、和一個(gè)或多個(gè)控制寄存器。
Linux 中傳統的 I/O 讀寫(xiě)是通過(guò) read()/write() 系統調用完成的,read() 把數據從存儲器 (磁盤(pán)、網(wǎng)卡等) 讀取到用戶(hù)緩沖區,write() 則是把數據從用戶(hù)緩沖區寫(xiě)出到存儲器:
#include <unistd.h>ssize_t read(int fd, void *buf, size_t count);ssize_t write(int fd, const void *buf, size_t count);一次完整的讀磁盤(pán)文件然后寫(xiě)出到網(wǎng)卡的底層傳輸過(guò)程如下:

可以清楚看到這里一共觸發(fā)了 4 次用戶(hù)態(tài)和內核態(tài)的上下文切換,分別是 read()/write() 調用和返回時(shí)的切換,2 次 DMA 拷貝,2 次 CPU 拷貝,加起來(lái)一共 4 次拷貝操作。
通過(guò)引入 DMA,我們已經(jīng)把 Linux 的 I/O 過(guò)程中的 CPU 拷貝次數從 4 次減少到了 2 次,但是 CPU 拷貝依然是代價(jià)很大的操作,對系統性能的影響還是很大,特別是那些頻繁 I/O 的場(chǎng)景,更是會(huì )因為 CPU 拷貝而損失掉很多性能,我們需要進(jìn)一步優(yōu)化,降低、甚至是完全避免 CPU 拷貝。
本文中我主要講解了 Linux I/O 底層原理,下篇將介紹并解析 Linux 中的 Zero-copy 技術(shù),并給出了 Linux 對 I/O 模塊的優(yōu)化和改進(jìn)思路。
聯(lián)系客服