一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

虛擬內(nèi)存 & I/O & 零拷貝

 好漢勃士 2022-10-13 發(fā)布于廣東

作者:mosun,騰訊 PCG 后臺開發(fā)工程師

一、虛擬內(nèi)存

1.1 虛擬內(nèi)存引入

我們知道計(jì)算機(jī)由 CPU、存儲器、輸入/輸出設(shè)備三大核心部分組成,如下:

文章圖片1

CPU 運(yùn)行速度很快,在完全理想的狀態(tài)下,存儲器應(yīng)該要同時具備以下三種特性:

  • 速度足夠快:這樣 CPU 的效率才不會受限于存儲器;
  • 容量足夠大:容量能夠存儲計(jì)算機(jī)所需的全部數(shù)據(jù);
  • 價格足夠便宜:價格低廉,所有類型的計(jì)算機(jī)都能配備;

然而,出于成本考慮,當(dāng)前計(jì)算機(jī)體系中,存儲都是采用分層設(shè)計(jì)的,常見層次如下:

文章圖片2

上圖分別為寄存器、高速緩存、主存和磁盤,它們的速度逐級遞減、成本逐級遞減,在計(jì)算機(jī)中的容量逐級遞增。通常我們所說的物理內(nèi)存即上文中的主存,常作為操作系統(tǒng)或其他正在運(yùn)行中的程序的臨時資料存儲介質(zhì)。在嵌入式以及一些老的操作系統(tǒng)中,系統(tǒng)直接通過物理尋址方式和主存打交道。然而,隨著科技發(fā)展,遇到如下窘境:

  • 一臺機(jī)器可能同時運(yùn)行多臺大型應(yīng)用程序;
  • 每個應(yīng)用程序都需要在主存存儲大量臨時數(shù)據(jù);
  • 早期,單個 CPU 尋址能力 2^32,導(dǎo)致內(nèi)存最大 4G;

主存成了計(jì)算機(jī)系統(tǒng)的瓶頸。此時,科學(xué)家提出了一個概念:虛擬內(nèi)存。

文章圖片3

以 32 位操作系統(tǒng)為例,虛擬內(nèi)存的引入,使得操作系統(tǒng)可以為每個進(jìn)程分配大小為 4GB 的虛擬內(nèi)存空間,而實(shí)際上物理內(nèi)存在需要時才會被加載,有效解決了物理內(nèi)存有限空間帶來的瓶頸。在虛擬內(nèi)存到物理內(nèi)存轉(zhuǎn)換的過程中,有很重要的一步就是進(jìn)行地址翻譯,下面介紹。

1.2 地址翻譯

進(jìn)程在運(yùn)行期間產(chǎn)生的內(nèi)存地址都是虛擬地址,如果計(jì)算機(jī)沒有引入虛擬內(nèi)存這種存儲器抽象技術(shù)的話,則 CPU 會把這些地址直接發(fā)送到內(nèi)存地址總線上,然后訪問和虛擬地址相同值的物理地址;如果使用虛擬內(nèi)存技術(shù)的話,CPU 則是把這些虛擬地址通過地址總線送到內(nèi)存管理單元(Memory Management Unit,簡稱 MMU),MMU 將虛擬地址翻譯成物理地址之后再通過內(nèi)存總線去訪問物理內(nèi)存:

文章圖片4

虛擬地址(比如 16 位地址 8196=0010 000000000100)分為兩部分:虛擬頁號(Virtual Page Number,簡稱 VPN,這里是高 4 位部分)和偏移量(Virtual Page Offset,簡稱 VPO,這里是低 12 位部分),虛擬地址轉(zhuǎn)換成物理地址是通過頁表(page table)來實(shí)現(xiàn)的。頁表由多個頁表項(xiàng)(Page Table Entry, 簡稱 PTE)組成,一般頁表項(xiàng)中都會存儲物理頁框號、修改位、訪問位、保護(hù)位和 '在/不在' 位(有效位)等信息。

這里我們基于一個例子來分析當(dāng)分析當(dāng)頁面命中時,計(jì)算機(jī)各個硬件是如何交互的:

文章圖片5
  • 第 1 步:處理器生成一個虛擬地址 VA,通過總線發(fā)送到 MMU;
  • 第 2 步:MMU 通過虛擬頁號得到頁表項(xiàng)的地址 PTEA,通過內(nèi)存總線從 CPU 高速緩存/主存讀取這個頁表項(xiàng) PTE;
  • 第 3 步:CPU 高速緩存或者主存通過內(nèi)存總線向 MMU 返回頁表項(xiàng) PTE;
  • 第 4 步:MMU 先把頁表項(xiàng)中的物理頁框號 PPN 復(fù)制到寄存器的高三位中,接著把 12 位的偏移量 VPO 復(fù)制到寄存器的末 12 位構(gòu)成 15 位的物理地址,即可以把該寄存器存儲的物理內(nèi)存地址 PA 發(fā)送到內(nèi)存總線,訪問高速緩存/主存;
  • 第 5 步:CPU 高速緩存/主存返回該物理地址對應(yīng)的數(shù)據(jù)給處理器。

在 MMU 進(jìn)行地址轉(zhuǎn)換時,如果頁表項(xiàng)的有效位是 0,則表示該頁面并沒有映射到真實(shí)的物理頁框號 PPN,則會引發(fā)一個缺頁中斷,CPU 陷入操作系統(tǒng)內(nèi)核,接著操作系統(tǒng)就會通過頁面置換算法選擇一個頁面將其換出 (swap),以便為即將調(diào)入的新頁面騰出位置,如果要換出的頁面的頁表項(xiàng)里的修改位已經(jīng)被設(shè)置過,也就是被更新過,則這是一個臟頁 (Dirty Page),需要寫回磁盤更新該頁面在磁盤上的副本,如果該頁面是'干凈'的,也就是沒有被修改過,則直接用調(diào)入的新頁面覆蓋掉被換出的舊頁面即可。缺頁中斷的具體流程如下:

文章圖片6
  • 第 1 步到第 3 步:和前面的頁面命中的前 3 步是一致的;
  • 第 4 步:檢查返回的頁表項(xiàng) PTE 發(fā)現(xiàn)其有效位是 0,則 MMU 觸發(fā)一次缺頁中斷異常,然后 CPU 轉(zhuǎn)入到操作系統(tǒng)內(nèi)核中的缺頁中斷處理器;
  • 第 5 步:缺頁中斷處理程序檢查所需的虛擬地址是否合法,確認(rèn)合法后系統(tǒng)則檢查是否有空閑物理頁框號 PPN 可以映射給該缺失的虛擬頁面,如果沒有空閑頁框,則執(zhí)行頁面置換算法尋找一個現(xiàn)有的虛擬頁面淘汰,如果該頁面已經(jīng)被修改過,則寫回磁盤,更新該頁面在磁盤上的副本;
  • 第 6 步:缺頁中斷處理程序從磁盤調(diào)入新的頁面到內(nèi)存,更新頁表項(xiàng) PTE;
  • 第 7 步:缺頁中斷程序返回到原先的進(jìn)程,重新執(zhí)行引起缺頁中斷的指令,CPU 將引起缺頁中斷的虛擬地址重新發(fā)送給 MMU,此時該虛擬地址已經(jīng)有了映射的物理頁框號 PPN,因此會按照前面『Page Hit』的流程走一遍,最后主存把請求的數(shù)據(jù)返回給處理器。

1.2.1 高速緩存

前面在分析虛擬內(nèi)存的工作原理之時,談到頁表的存儲位置,為了簡化處理,都是默認(rèn)把主存和高速緩存放在一起,而實(shí)際上更詳細(xì)的流程應(yīng)該是如下的原理圖:

文章圖片7

如果一臺計(jì)算機(jī)同時配備了虛擬內(nèi)存技術(shù)和 CPU 高速緩存,那么 MMU 每次都會優(yōu)先嘗試到高速緩存中進(jìn)行尋址,如果緩存命中則會直接返回,只有當(dāng)緩存不命中之后才去主存尋址。

通常來說,大多數(shù)系統(tǒng)都會選擇利用物理內(nèi)存地址去訪問高速緩存,因?yàn)楦咚倬彺嫦啾扔谥鞔嬉〉枚?,所以使用物理尋址也不會太?fù)雜;另外也因?yàn)楦咚倬彺嫒萘亢苄?,所以系統(tǒng)需要盡量在多個進(jìn)程之間共享數(shù)據(jù)塊,而使用物理地址能夠使得多進(jìn)程同時在高速緩存中存儲數(shù)據(jù)塊以及共享來自相同虛擬內(nèi)存頁的數(shù)據(jù)塊變得更加直觀。

1.2.2 加速翻譯&優(yōu)化頁表

虛擬內(nèi)存這項(xiàng)技術(shù)能不能真正地廣泛應(yīng)用到計(jì)算機(jī)中,還需要解決如下兩個問題:

  • 虛擬地址到物理地址的映射過程必須要非??欤刂贩g如何加速。
  • 虛擬地址范圍的增大必然會導(dǎo)致頁表的膨脹,形成大頁表。

'計(jì)算機(jī)科學(xué)領(lǐng)域的任何問題都可以通過增加一個間接的中間層來解決'。雖然虛擬內(nèi)存本身就已經(jīng)是一個中間層了,但是中間層里的問題同樣可以通過再引入一個中間層來解決。加速地址翻譯過程的方案目前是通過引入頁表緩存模塊 -- TLB,而大頁表則是通過實(shí)現(xiàn)多級頁表或倒排頁表來解決。

1.2.2.1 TLB 加速

翻譯后備緩沖器(Translation Lookaside Buffer,TLB),也叫快表,是用來加速虛擬地址翻譯的,因?yàn)樘摂M內(nèi)存的分頁機(jī)制,頁表一般是保存在內(nèi)存中的一塊固定的存儲區(qū),而 MMU 每次翻譯虛擬地址的時候都需要從頁表中匹配一個對應(yīng)的 PTE,導(dǎo)致進(jìn)程通過 MMU 訪問指定內(nèi)存數(shù)據(jù)的時候比沒有分頁機(jī)制的系統(tǒng)多了一次內(nèi)存訪問,一般會多耗費(fèi)幾十到幾百個 CPU 時鐘周期,性能至少下降一半,如果 PTE 碰巧緩存在 CPU L1 高速緩存中,則開銷可以降低到一兩個周期,但是我們不能寄希望于每次要匹配的 PTE 都剛好在 L1 中,因此需要引入加速機(jī)制,即 TLB 快表。

TLB 可以簡單地理解成頁表的高速緩存,保存了最高頻被訪問的頁表項(xiàng) PTE。由于 TLB 一般是硬件實(shí)現(xiàn)的,因此速度極快,MMU 收到虛擬地址時一般會先通過硬件 TLB 并行地在頁表中匹配對應(yīng)的 PTE,若命中且該 PTE 的訪問操作不違反保護(hù)位(比如嘗試寫一個只讀的內(nèi)存地址),則直接從 TLB 取出對應(yīng)的物理頁框號 PPN 返回,若不命中則會穿透到主存頁表里查詢,并且會在查詢到最新頁表項(xiàng)之后存入 TLB,以備下次緩存命中,如果 TLB 當(dāng)前的存儲空間不足則會替換掉現(xiàn)有的其中一個 PTE。

下面來具體分析一下 TLB 命中和不命中。

TLB 命中

文章圖片8
  • 第 1 步:CPU 產(chǎn)生一個虛擬地址 VA;
  • 第 2 步和第 3 步:MMU 從 TLB 中取出對應(yīng)的 PTE;
  • 第 4 步:MMU 將這個虛擬地址 VA 翻譯成一個真實(shí)的物理地址 PA,通過地址總線發(fā)送到高速緩存/主存中去;
  • 第 5 步:高速緩存/主存將物理地址 PA 上的數(shù)據(jù)返回給 CPU。

TLB 不命中

文章圖片9
  • 第 1 步:CPU 產(chǎn)生一個虛擬地址 VA;
  • 第 2 步至第 4 步:查詢 TLB 失敗,走正常的主存頁表查詢流程拿到 PTE,然后把它放入 TLB 緩存,以備下次查詢,如果 TLB 此時的存儲空間不足,則這個操作會汰換掉 TLB 中另一個已存在的 PTE;
  • 第 5 步:MMU 將這個虛擬地址 VA 翻譯成一個真實(shí)的物理地址 PA,通過地址總線發(fā)送到高速緩存/主存中去;
  • 第 6 步:高速緩存/主存將物理地址 PA 上的數(shù)據(jù)返回給 CPU。

1.2.2.2 多級頁表

TLB 的引入可以一定程度上解決虛擬地址到物理地址翻譯的開銷問題,接下來還需要解決另一個問題:大頁表。理論上一臺 32 位的計(jì)算機(jī)的尋址空間是 4GB,也就是說每一個運(yùn)行在該計(jì)算機(jī)上的進(jìn)程理論上的虛擬尋址范圍是 4GB。到目前為止,我們一直在討論的都是單頁表的情形,如果每一個進(jìn)程都把理論上可用的內(nèi)存頁都裝載進(jìn)一個頁表里,但是實(shí)際上進(jìn)程會真正使用到的內(nèi)存其實(shí)可能只有很小的一部分,而我們也知道頁表也是保存在計(jì)算機(jī)主存中的,那么勢必會造成大量的內(nèi)存浪費(fèi),甚至有可能導(dǎo)致計(jì)算機(jī)物理內(nèi)存不足從而無法并行地運(yùn)行更多進(jìn)程。

這個問題一般通過多級頁表(Multi-Level Page Tables)來解決,通過把一個大頁表進(jìn)行拆分,形成多級的頁表,我們具體來看一個二級頁表應(yīng)該如何設(shè)計(jì):假定一個虛擬地址是 32 位,由 10 位的一級頁表索引、10 位的二級頁表索引以及 12 位的地址偏移量,則 PTE 是 4 字節(jié),頁面 page 大小是 2^12 = 4KB,總共需要 2^20 個 PTE,一級頁表中的每個 PTE 負(fù)責(zé)映射虛擬地址空間中的一個 4MB 的 chunk,每一個 chunk 都由 1024 個連續(xù)的頁面 Page 組成,如果尋址空間是 4GB,那么一共只需要 1024 個 PTE 就足夠覆蓋整個進(jìn)程地址空間。二級頁表中的每一個 PTE 都負(fù)責(zé)映射到一個 4KB 的虛擬內(nèi)存頁面,和單頁表的原理是一樣的。

多級頁表的關(guān)鍵在于,我們并不需要為一級頁表中的每一個 PTE 都分配一個二級頁表,而只需要為進(jìn)程當(dāng)前使用到的地址做相應(yīng)的分配和映射。因此,對于大部分進(jìn)程來說,它們的一級頁表中有大量空置的 PTE,那么這部分 PTE 對應(yīng)的二級頁表也將無需存在,這是一個相當(dāng)可觀的內(nèi)存節(jié)約,事實(shí)上對于一個典型的程序來說,理論上的 4GB 可用虛擬內(nèi)存地址空間絕大部分都會處于這樣一種未分配的狀態(tài);更進(jìn)一步,在程序運(yùn)行過程中,只需要把一級頁表放在主存中,虛擬內(nèi)存系統(tǒng)可以在實(shí)際需要的時候才去創(chuàng)建、調(diào)入和調(diào)出二級頁表,這樣就可以確保只有那些最頻繁被使用的二級頁表才會常駐在主存中,此舉亦極大地緩解了主存的壓力。

文章圖片10

二、 內(nèi)核空間 & 用戶空間

對 32 位操作系統(tǒng)而言,它的尋址空間(虛擬地址空間,或叫線性地址空間)為 4G(2 的 32 次方)。也就是說一個進(jìn)程的最大地址空間為 4G。操作系統(tǒng)的核心是內(nèi)核(kernel),它獨(dú)立于普通的應(yīng)用程序,可以訪問受保護(hù)的內(nèi)存空間,也有訪問底層硬件設(shè)備的所有權(quán)限。為了保證內(nèi)核的安全,現(xiàn)在的操作系統(tǒng)一般都強(qiáng)制用戶進(jìn)程不能直接操作內(nèi)核。具體的實(shí)現(xiàn)方式基本都是由操作系統(tǒng)將虛擬地址空間劃分為兩部分,一部分為內(nèi)核空間,另一部分為用戶空間。針對 Linux 操作系統(tǒng)而言,最高的 1G 字節(jié)(從虛擬地址 0xC0000000 到 0xFFFFFFFF)由內(nèi)核使用,稱為內(nèi)核空間。而較低的 3G 字節(jié)(從虛擬地址 0x00000000 到 0xBFFFFFFF)由各個進(jìn)程使用,稱為用戶空間。

文章圖片11

為什么需要區(qū)分內(nèi)核空間與用戶空間? 在 CPU 的所有指令中,有些指令是非常危險的,如果錯用,將導(dǎo)致系統(tǒng)崩潰,比如清內(nèi)存、設(shè)置時鐘等。如果允許所有的程序都可以使用這些指令,那么系統(tǒng)崩潰的概率將大大增加。所以,CPU 將指令分為特權(quán)指令和非特權(quán)指令,對于那些危險的指令,只允許操作系統(tǒng)及其相關(guān)模塊使用,普通應(yīng)用程序只能使用那些不會造成災(zāi)難的指令。區(qū)分內(nèi)核空間和用戶空間本質(zhì)上是要提高操作系統(tǒng)的穩(wěn)定性及可用性。

2.1 內(nèi)核態(tài)與用戶態(tài)

當(dāng)進(jìn)程運(yùn)行在內(nèi)核空間時就處于內(nèi)核態(tài),而進(jìn)程運(yùn)行在用戶空間時則處于用戶態(tài)。

在內(nèi)核態(tài)下,進(jìn)程運(yùn)行在內(nèi)核地址空間中,此時 CPU 可以執(zhí)行任何指令。運(yùn)行的代碼也不受任何的限制,可以自由地訪問任何有效地址,也可以直接進(jìn)行端口的訪問。

在用戶態(tài)下,進(jìn)程運(yùn)行在用戶地址空間中,被執(zhí)行的代碼要受到 CPU 的諸多檢查,它們只能訪問映射其地址空間的頁表項(xiàng)中規(guī)定的在用戶態(tài)下可訪問頁面的虛擬地址,且只能對任務(wù)狀態(tài)段(TSS)中 I/O 許可位圖(I/O Permission Bitmap)中規(guī)定的可訪問端口進(jìn)行直接訪問。

對于以前的 DOS 操作系統(tǒng)來說,是沒有內(nèi)核空間、用戶空間以及內(nèi)核態(tài)、用戶態(tài)這些概念的??梢哉J(rèn)為所有的代碼都是運(yùn)行在內(nèi)核態(tài)的,因而用戶編寫的應(yīng)用程序代碼可以很容易的讓操作系統(tǒng)崩潰掉。

對于 Linux 來說,通過區(qū)分內(nèi)核空間和用戶空間的設(shè)計(jì),隔離了操作系統(tǒng)代碼(操作系統(tǒng)的代碼要比應(yīng)用程序的代碼健壯很多)與應(yīng)用程序代碼。即便是單個應(yīng)用程序出現(xiàn)錯誤也不會影響到操作系統(tǒng)的穩(wěn)定性,這樣其它的程序還可以正常的運(yùn)行。

如何從用戶空間進(jìn)入內(nèi)核空間?

其實(shí)所有的系統(tǒng)資源管理都是在內(nèi)核空間中完成的。比如讀寫磁盤文件,分配回收內(nèi)存,從網(wǎng)絡(luò)接口讀寫數(shù)據(jù)等等。我們的應(yīng)用程序是無法直接進(jìn)行這樣的操作的。但是我們可以通過內(nèi)核提供的接口來完成這樣的任務(wù)。比如應(yīng)用程序要讀取磁盤上的一個文件,它可以向內(nèi)核發(fā)起一個 “系統(tǒng)調(diào)用” 告訴內(nèi)核:“我要讀取磁盤上的某某文件”。

其實(shí)就是通過一個特殊的指令讓進(jìn)程從用戶態(tài)進(jìn)入到內(nèi)核態(tài)(到了內(nèi)核空間),在內(nèi)核空間中,CPU 可以執(zhí)行任何的指令,當(dāng)然也包括從磁盤上讀取數(shù)據(jù)。具體過程是先把數(shù)據(jù)讀取到內(nèi)核空間中,然后再把數(shù)據(jù)拷貝到用戶空間并從內(nèi)核態(tài)切換到用戶態(tài)。此時應(yīng)用程序已經(jīng)從系統(tǒng)調(diào)用中返回并且拿到了想要的數(shù)據(jù),可以開開心心的往下執(zhí)行了。簡單說就是應(yīng)用程序把高科技的事情(從磁盤讀取文件)外包給了系統(tǒng)內(nèi)核,系統(tǒng)內(nèi)核做這些事情既專業(yè)又高效。

三、 IO

在進(jìn)行 IO 操作時,通常需要經(jīng)過如下兩個階段:

  • 數(shù)據(jù)準(zhǔn)備階段:數(shù)據(jù)從硬件到內(nèi)核空間
  • 數(shù)據(jù)拷貝階段:數(shù)據(jù)從內(nèi)核空間到用戶空間
文章圖片12

通常我們所說的 IO 的阻塞/非阻塞以及同步/異步,和這兩個階段關(guān)系密切:

  • 阻塞 IO 和非阻塞 IO 判定標(biāo)準(zhǔn):數(shù)據(jù)準(zhǔn)備階段,應(yīng)用程序是否阻塞等待操作系統(tǒng)將數(shù)據(jù)從硬件加載到內(nèi)核空間;
  • 同步 IO 和異步 IO 判定標(biāo)準(zhǔn):數(shù)據(jù)拷貝階段,數(shù)據(jù)是否備好后直接通知應(yīng)用程序使用,無需等待拷貝;

3.1 (同步)阻塞 IO

阻塞 IO :當(dāng)用戶發(fā)生了系統(tǒng)調(diào)用后,如果數(shù)據(jù)未從網(wǎng)卡到達(dá)內(nèi)核態(tài),內(nèi)核態(tài)數(shù)據(jù)未準(zhǔn)備好,此時會一直阻塞。直到數(shù)據(jù)就緒,然后從內(nèi)核態(tài)拷貝到用戶態(tài)再返回。

文章圖片13

阻塞 IO 每個連接一個單獨(dú)的線程進(jìn)行處理,通常搭配多線程來應(yīng)對大流量,但是,開辟線程的開銷比較大,一個程序可以開辟的線程是有限的,面對百萬連接的情況,是無法處理。非阻塞 IO 可以一定程度上解決上述問題。

3.2 (同步)非阻塞 IO

非阻塞 IO :在第一階段(網(wǎng)卡-內(nèi)核態(tài))數(shù)據(jù)未到達(dá)時不等待,然后直接返回。數(shù)據(jù)就緒后,從內(nèi)核態(tài)拷貝到用戶態(tài)再返回。

文章圖片14

非阻塞 IO 解決了阻塞 IO每個連接一個線程處理的問題,所以其最大的優(yōu)點(diǎn)就是 一個線程可以處理多個連接。然而,非阻塞 IO 需要用戶多次發(fā)起系統(tǒng)調(diào)用。頻繁的系統(tǒng)調(diào)用是比較消耗系統(tǒng)資源的。

3.3 IO 多路復(fù)用

為了解決非阻塞 IO 存在的頻繁的系統(tǒng)調(diào)用這個問題,隨著內(nèi)核的發(fā)展,出現(xiàn)了 IO 多路復(fù)用模型。

IO 多路復(fù)用:通過一種機(jī)制一個進(jìn)程能同時等待多個文件描述符,而這些文件描述符(套接字描述符)其中的任意一個進(jìn)入讀就緒狀態(tài),就可以返回。

文章圖片15

IO 多路復(fù)用本質(zhì)上復(fù)用了系統(tǒng)調(diào)用,使多個文件的狀態(tài)可以復(fù)用一個系統(tǒng)調(diào)用獲取,有效減少了系統(tǒng)調(diào)用。select、poll、epoll均是基于 IO 多路復(fù)用思想實(shí)現(xiàn)的。

文章圖片16
文章圖片17

select 和 poll 的工作原理比較相似,通過 select()或者 poll()將多個 socket fds 批量通過系統(tǒng)調(diào)用傳遞給內(nèi)核,由內(nèi)核進(jìn)行循環(huán)遍歷判斷哪些 fd 上數(shù)據(jù)就緒了,然后將就緒的 readyfds 返回給用戶。再由用戶進(jìn)行挨個遍歷就緒好的 fd,讀取或者寫入數(shù)據(jù)。所以通過 IO 多路復(fù)用+非阻塞 IO,一方面降低了系統(tǒng)調(diào)用次數(shù),另一方面可以用極少的線程來處理多個網(wǎng)絡(luò)連接。select 和 poll 的最大區(qū)別是:select 默認(rèn)能處理的最大連接是 1024 個,可以通過修改配置來改變,但終究是有限個;而 poll 理論上可以支持無限個。而 select 和 poll 則面臨相似的問題在管理海量的連接時,會頻繁的從用戶態(tài)拷貝到內(nèi)核態(tài),比較消耗資源。

epoll 有效規(guī)避了將 fd 頻繁的從用戶態(tài)拷貝到內(nèi)核態(tài),通過使用紅黑樹(RB-tree)搜索被監(jiān)視的文件描述符(file descriptor)。在 epoll 實(shí)例上注冊事件時,epoll 會將該事件添加到 epoll 實(shí)例的紅黑樹上并注冊一個回調(diào)函數(shù),當(dāng)事件發(fā)生時會將事件添加到就緒鏈表中。

3.3.1 epoll 數(shù)據(jù)結(jié)構(gòu) + 算法

epoll 的核心數(shù)據(jù)結(jié)構(gòu)是:1 個 紅黑樹 和 1 個 雙向鏈表,還有 3個核心API 。

文章圖片18

3.3.2 監(jiān)視 socket 索引-紅黑樹

為什么采用紅黑樹呢?因?yàn)楹?epoll 的工作機(jī)制有關(guān)。epoll 在添加一個 socket 或者刪除一個 socket 或者修改一個 socket 的時候,它需要查詢速度更快,操作效率最高,因此需要一個更加優(yōu)秀的數(shù)據(jù)結(jié)構(gòu)能夠管理這些 socket。我們想到的比如鏈表,數(shù)組,二叉搜索樹,B+樹等都無法滿足要求,

  • 因?yàn)殒湵碓诓樵儯瑒h除的時候毫無疑問時間復(fù)雜度是 O(n);
  • 數(shù)組查詢很快,但是刪除和新增時間復(fù)雜度是 O(n);
  • 二叉搜索樹雖然查詢效率是 lgn,但是如果不是平衡的,那么就會退化為線性查找,復(fù)雜度直接來到 O(n);
  • B+樹是平衡多路查找樹,主要是通過降低樹的高度來存儲上億級別的數(shù)據(jù),但是它的應(yīng)用場景是內(nèi)存放不下的時候能夠用最少的 IO 訪問次數(shù)從磁盤獲取數(shù)據(jù)。比如數(shù)據(jù)庫聚簇索引,成百上千萬的數(shù)據(jù)內(nèi)存無法滿足查找就需要到內(nèi)存查找,而因?yàn)?B+樹層高很低,只需要幾次磁盤 IO 就能獲取數(shù)據(jù)到內(nèi)存,所以在這種磁盤到內(nèi)存訪問上 B+樹更適合。

因?yàn)槲覀兲幚砩先f級的 fd,它們本身的存儲空間并不會很大,所以傾向于在內(nèi)存中去實(shí)現(xiàn)管理,而紅黑樹是一種非常優(yōu)秀的平衡樹,它完全是在內(nèi)存中操作,而且查找,刪除和新增時間復(fù)雜度都是 lgn,效率非常高,因此選擇用紅黑樹實(shí)現(xiàn) epoll 是最佳的選擇。當(dāng)然不選擇用 AVL 樹是因?yàn)榧t黑樹是不符合 AVL 樹的平衡條件的,紅黑是用非嚴(yán)格的平衡來換取增刪節(jié)點(diǎn)時候旋轉(zhuǎn)次數(shù)的降低,任何不平衡都會在三次旋轉(zhuǎn)之內(nèi)解決;而 AVL 樹是嚴(yán)格平衡樹,在增加或者刪除節(jié)點(diǎn)的時候,根據(jù)不同情況,旋轉(zhuǎn)的次數(shù)比紅黑樹要多。所以紅黑樹的插入效率更高。

3.3.2 就緒 socket 列表-雙向鏈表

就緒列表存儲的是就緒的 socket,所以它應(yīng)能夠快速的插入數(shù)據(jù)。程序可能隨時調(diào)用 epoll_ctl 添加監(jiān)視 socket,也可能隨時刪除。當(dāng)刪除時,若該 socket 已經(jīng)存放在就緒列表中,它也應(yīng)該被移除。(事實(shí)上,每個 epoll_item 既是紅黑樹節(jié)點(diǎn),也是鏈表節(jié)點(diǎn),刪除紅黑樹節(jié)點(diǎn),自然刪除了鏈表節(jié)點(diǎn)) 所以就緒列表應(yīng)是一種能夠快速插入和刪除的數(shù)據(jù)結(jié)構(gòu)。雙向鏈表就是這樣一種數(shù)據(jù)結(jié)構(gòu),epoll 使用 雙向鏈表來實(shí)現(xiàn)就緒隊(duì)列 (rdllist)

3.3.3 三個 API

3.3.3.1 int epoll_create(int size)

功能:內(nèi)核會產(chǎn)生一個 epoll 實(shí)例數(shù)據(jù)結(jié)構(gòu)并返回一個文件描述符 epfd,這個特殊的描述符就是 epoll 實(shí)例的句柄,后面的兩個接口都以它為中心。同時也會創(chuàng)建紅黑樹和就緒列表,紅黑樹來管理注冊 fd,就緒列表來收集所有就緒 fd。size 參數(shù)表示所要監(jiān)視文件描述符的最大值,不過在后來的 Linux 版本中已經(jīng)被棄用(同時,size 不要傳 0,會報 invalid argument 錯誤)

3.3.3.2 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

功能:將被監(jiān)聽的 socket 文件描述符添加到紅黑樹或從紅黑樹中刪除或者對監(jiān)聽事件進(jìn)行修改;同時向內(nèi)核中斷處理程序注冊一個回調(diào)函數(shù),內(nèi)核在檢測到某文件描述符可讀/可寫時會調(diào)用回調(diào)函數(shù),該回調(diào)函數(shù)將文件描述符放在就緒鏈表中。

3.3.3.3 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

功能:阻塞等待注冊的事件發(fā)生,返回事件的數(shù)目,并將觸發(fā)的事件寫入 events 數(shù)組中。events: 用來記錄被觸發(fā)的 events,其大小應(yīng)該和 maxevents 一致 maxevents: 返回的 events 的最大個數(shù)處于 ready 狀態(tài)的那些文件描述符會被復(fù)制進(jìn) ready list 中,epoll_wait 用于向用戶進(jìn)程返回 ready list(就緒列表)。events 和 maxevents 兩個參數(shù)描述一個由用戶分配的 struct epoll event 數(shù)組,調(diào)用返回時,內(nèi)核將就緒列表(雙向鏈表)復(fù)制到這個數(shù)組中,并將實(shí)際復(fù)制的個數(shù)作為返回值。注意,如果就緒列表比 maxevents 長,則只能復(fù)制前 maxevents 個成員;反之,則能夠完全復(fù)制就緒列表。另外,struct epoll event 結(jié)構(gòu)中的 events 域在這里的解釋是:在被監(jiān)測的文件描述符上實(shí)際發(fā)生的事件

3.3.4 工作模式

epoll 對文件描述符的操作有兩種模式:LT(level trigger)和 ET(edge trigger)。

  1. LT 模式 LT(level triggered)是缺省的工作方式,并且同時支持 block 和 no-block socket.在這種做法中,內(nèi)核告訴你一個文件描述符是否就緒了,然后你可以對這個就緒的 fd 進(jìn)行 IO 操作。如果你不作任何操作,內(nèi)核還是會繼續(xù)通知你
  2. ET 模式 ET(edge-triggered)是高速工作方式,只支持 no-block socket。在這種模式下,當(dāng)描述符從未就緒變?yōu)榫途w時,內(nèi)核通過 epoll 告訴你。然后它會假設(shè)你知道文件描述符已經(jīng)就緒,并且不會再為那個文件描述符發(fā)送更多的就緒通知,直到你做了某些操作導(dǎo)致那個文件描述符不再為就緒狀態(tài)了(比如,你在發(fā)送,接收或者接收請求,或者發(fā)送接收的數(shù)據(jù)少于一定量時導(dǎo)致了一個 EWOULDBLOCK 錯誤)。注意,如果一直不對這個 fd 作 IO 操作(從而導(dǎo)致它再次變成未就緒),內(nèi)核不會發(fā)送更多的通知(only once) ET 模式在很大程度上減少了 epoll 事件被重復(fù)觸發(fā)的次數(shù),因此效率要比 LT 模式高。epoll 工作在 ET 模式的時候,必須使用非阻塞套接口,以避免由于一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務(wù)餓死。

3.4 網(wǎng)絡(luò) IO 模型

實(shí)際的網(wǎng)絡(luò)模型常結(jié)合 I/O 復(fù)用和線程池實(shí)現(xiàn),如 Reactor 模式:

文章圖片19

3.4.1 單 reactor 單線程模型

此種模型通常只有一個 epoll 對象,所有的接收客戶端連接、客戶端讀取客戶端寫入操作都包含在一個線程內(nèi)。

文章圖片20

優(yōu)點(diǎn):模型簡單,沒有多線程、進(jìn)程通信、競爭的問題,全部都在一個線程中完成 缺點(diǎn):單線程無法完全發(fā)揮多核 CPU 的性能;I/O 操作和非 I/O 的業(yè)務(wù)操作在一個 Reactor 線程完成,這可能會大大延遲 I/O 請求的響應(yīng);線程意外終止,或者進(jìn)入死循環(huán),會導(dǎo)致整個系統(tǒng)通信模塊不可用,不能接收和處理外部消息,造成節(jié)點(diǎn)故障;使用場景:客戶端的數(shù)量有限,業(yè)務(wù)處理非常快速,比如 Redis 在業(yè)務(wù)處理的時間復(fù)雜度 O(1) 的情況

3.4.2 單 reactor 多線程模型

該模型將讀寫的業(yè)務(wù)邏輯交給具體的線程池來處理

文章圖片21

優(yōu)點(diǎn):充分利用多核 cpu 的處理能力,提升 I/O 響應(yīng)速度;缺點(diǎn):在該模式中,雖然非 I/O 操作交給了線程池來處理,但是所有的 I/O 操作依然由 Reactor 單線程執(zhí)行,在高負(fù)載、高并發(fā)或大數(shù)據(jù)量的應(yīng)用場景,依然容易成為瓶頸。

3.4.3 multi-reactor 多線程模型

在這種模型中,主要分為兩個部分:mainReactor、subReactors。mainReactor 主要負(fù)責(zé)接收客戶端的連接,然后將建立的客戶端連接通過負(fù)載均衡的方式分發(fā)給 subReactors,subReactors 來負(fù)責(zé)具體的每個連接的讀寫 對于非 IO 的操作,依然交給工作線程池去做。

文章圖片22

優(yōu)點(diǎn):父線程與子線程的數(shù)據(jù)交互簡單職責(zé)明確,父線程只需要接收新連接,子線程完成后續(xù)的業(yè)務(wù)處理。Reactor 主線程只需要把新連接傳給子線程,子線程無需返回?cái)?shù)據(jù)。缺點(diǎn):編程復(fù)雜度較高。

3.4.4 主流的中間件所采用的網(wǎng)絡(luò)模型

文章圖片23

3.5 異步 IO

文章圖片24

前面介紹的所有網(wǎng)絡(luò) IO 都是同步 IO,因?yàn)楫?dāng)數(shù)據(jù)在內(nèi)核態(tài)就緒時,在內(nèi)核態(tài)拷貝用用戶態(tài)的過程中,仍然會有短暫時間的阻塞等待。而異步 IO 指:內(nèi)核態(tài)拷貝數(shù)據(jù)到用戶態(tài)這種方式也是交給系統(tǒng)線程來實(shí)現(xiàn),不由用戶線程完成,如 windows 的 IOCP ,Linux 的 AIO。

四、 零拷貝

4.1 傳統(tǒng) IO 流程

傳統(tǒng) IO 流程會經(jīng)過如下兩個過程:

  • 數(shù)據(jù)準(zhǔn)備階段:數(shù)據(jù)從硬件到內(nèi)核空間
  • 數(shù)據(jù)拷貝階段:數(shù)據(jù)從內(nèi)核空間到用戶空間
文章圖片25

零拷貝指數(shù)據(jù)無需從硬件到內(nèi)核空間或從內(nèi)核空間到用戶空間。下面介紹常見的零拷貝實(shí)現(xiàn)

4.2 mmap + write

mmap 將內(nèi)核中讀緩沖區(qū)(read buffer)的地址與用戶空間的緩沖區(qū)(user buffer)進(jìn)行映射,從而實(shí)現(xiàn)內(nèi)核緩沖區(qū)與應(yīng)用程序內(nèi)存的共享,省去了將數(shù)據(jù)從內(nèi)核讀緩沖區(qū)(read buffer)拷貝到用戶緩沖區(qū)(user buffer)的過程,整個拷貝過程會發(fā)生 4 次上下文切換,1 次 CPU 拷貝和 2 次 DMA 拷貝。

文章圖片26

4.3 sendfile

通過 sendfile 系統(tǒng)調(diào)用,數(shù)據(jù)可以直接在內(nèi)核空間內(nèi)部進(jìn)行 I/O 傳輸,從而省去了數(shù)據(jù)在用戶空間和內(nèi)核空間之間的來回拷貝,sendfile 調(diào)用中 I/O 數(shù)據(jù)對用戶空間是完全不可見的,整個拷貝過程會發(fā)生 2 次上下文切換,1 次 CPU 拷貝和 2 次 DMA 拷貝。

文章圖片27

4.4 Sendfile + DMA gather copy

Linux2.4 引入 ,將內(nèi)核空間的讀緩沖區(qū)(read buffer)中對應(yīng)的數(shù)據(jù)描述信息(內(nèi)存地址、地址偏移量)記錄到相應(yīng)的網(wǎng)絡(luò)緩沖區(qū)(socketbuffer)中,由 DMA 根據(jù)內(nèi)存地址、地址偏移量將數(shù)據(jù)批量地從讀緩沖區(qū)(read buffer)拷貝到網(wǎng)卡設(shè)備中,這樣就省去了內(nèi)核空間中僅剩的 1 次 CPU 拷貝操作,發(fā)生 2 次上下文切換、0 次 CPU 拷貝以及 2 次 DMA 拷貝;

文章圖片28

4.5 splice

Linux2.6.17 版本引入,在內(nèi)核空間的讀緩沖區(qū)(read buffer)和網(wǎng)絡(luò)緩沖區(qū)(socket buffer)之間建立管道(pipeline),從而避免了兩者之間的 CPU 拷貝操作,2 次上下文切換,0 次 CPU 拷貝以及 2 次 DMA 拷貝。

文章圖片29

4.6 寫時復(fù)制

文章圖片30

通過盡量延遲產(chǎn)生私有對象中的副本,寫時復(fù)制最充分地利用了稀有的物理資源。

4.7 Java 中零拷貝

MappedByteBuffer:基于內(nèi)存映射(mmap)這種零拷貝方式的提供的一種實(shí)現(xiàn)。

文章圖片31

FileChannel 基于 sendfile 定義了 transferFrom() 和 transferTo() 兩個抽象方法,它通過在通道和通道之間建立連接實(shí)現(xiàn)數(shù)據(jù)傳輸?shù)摹?/span>

文章圖片32

五、參考

https://mp.weixin.qq.com/s/c81Fvws0J2tHjcdTgxvv6g

https://blog.csdn.net/Chasing__Dreams/article/details/106297351

https://mp.weixin.qq.com/s/EDzFOo3gcivOe_RgipkTkQ

https://mp.weixin.qq.com/s/G6TfGbc4U8Zhv30wnN0HIg

https://www./db/189656

https://mp.weixin.qq.com/s/r9RU4RoE-qrzXPAwiej5sw

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多

    国产亚洲欧美一区二区| 亚洲国产精品av在线观看| 一区二区三区四区亚洲专区| 国产又大又硬又粗又湿| 国产一区二区三区口爆在线| 福利视频一区二区在线| 国产一区二区久久综合| 欧美一区二区三区视频区| 在线观看视频日韩精品| 久久综合狠狠综合久久综合 | 色婷婷视频在线精品免费观看| 欧美一级特黄大片做受大屁股| 超薄肉色丝袜脚一区二区| 精品欧美日韩一区二区三区| 黑丝国产精品一区二区| 丰满少妇高潮一区二区| 日韩精品小视频在线观看| 日韩蜜桃一区二区三区| 国产一区二区久久综合| 微拍一区二区三区福利| 年轻女房东2中文字幕| 国产精品视频久久一区| 日韩欧美国产精品自拍| 爱草草在线观看免费视频| 偷自拍亚洲欧美一区二页| 亚洲中文字幕在线观看四区| 黄色国产自拍在线观看| 激情国产白嫩美女在线观看| 91精品国产品国语在线不卡| 国产日韩欧美在线播放| 在线视频三区日本精品| 欧美日韩一区二区综合| 亚洲天堂国产精品久久精品| 国产人妻熟女高跟丝袜| 日本二区三区在线播放| 欧美av人人妻av人人爽蜜桃| 伊人欧美一区二区三区| 最近中文字幕高清中文字幕无| 久久一区内射污污内射亚洲| 欧美成人免费视频午夜色| 国产又色又粗又黄又爽|