偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

從頭捋了一遍Epoll原理,收獲頗豐!

系統(tǒng) Linux 開發(fā)工具
Epoll 是個(gè)很老的知識(shí)點(diǎn),是后端工程師的經(jīng)典必修課。這種知識(shí)具備的特點(diǎn)就是研究的人多,所以研究的趨勢(shì)就會(huì)越來(lái)越深。當(dāng)然分享的人也多,由于分享者水平參差不齊,也產(chǎn)生的大量錯(cuò)誤理解。

 Epoll 是個(gè)很老的知識(shí)點(diǎn),是后端工程師的經(jīng)典必修課。這種知識(shí)具備的特點(diǎn)就是研究的人多,所以研究的趨勢(shì)就會(huì)越來(lái)越深。當(dāng)然分享的人也多,由于分享者水平參差不齊,也產(chǎn)生的大量錯(cuò)誤理解。

[[386789]]

圖片來(lái)自 Pexels 

今天我再次分享 epoll,肯定不會(huì)列個(gè)表格,對(duì)比一下差異,那就太無(wú)聊了。

我將從線程阻塞的原理,中斷優(yōu)化,網(wǎng)卡處理數(shù)據(jù)過(guò)程出發(fā),深入的介紹 epoll 背后的原理,最后還會(huì) diss 一些流行的觀點(diǎn)。相信無(wú)論你是否已經(jīng)熟悉 epoll,本文都會(huì)對(duì)你有價(jià)值。

正文開始前,先問大家?guī)讉€(gè)問題:

①epoll 性能到底有多高。很多文章介紹 epoll 可以輕松處理幾十萬(wàn)個(gè)連接。而傳統(tǒng) IO 只能處理幾百個(gè)連接,是不是說(shuō) epoll 的性能就是傳統(tǒng) IO 的千倍呢?

②很多文章把網(wǎng)絡(luò) IO 劃分為阻塞,非阻塞,同步,異步。并表示:非阻塞的性能比阻塞性能好,異步的性能比同步性能好。

如果說(shuō)阻塞導(dǎo)致性能低,那傳統(tǒng) IO 為什么要阻塞呢?epoll 是否需要阻塞呢?Java 的 NIO 和 AIO 底層都是 epoll 實(shí)現(xiàn)的,這又怎么理解同步和異步的區(qū)別?

③都是 IO 多路復(fù)用。既生瑜何生亮,為什么會(huì)有 select,poll 和 epoll 呢?為什么 epoll 比 select 性能高?

PS:本文共包含三大部分:初識(shí) epoll、epoll 背后的原理 、Diss 環(huán)節(jié)。本文的重點(diǎn)是介紹原理,建議讀者的關(guān)注點(diǎn)盡量放在:“為什么”。

Linux 下進(jìn)程和線程的區(qū)別其實(shí)并不大,尤其是在討論原理和性能問題時(shí),因此本文中“進(jìn)程”和“線程”兩個(gè)詞是混用的。

初識(shí) epoll

epoll 是 Linux 內(nèi)核的可擴(kuò)展 I/O 事件通知機(jī)制,其最大的特點(diǎn)就是性能優(yōu)異。

下圖是 libevent(一個(gè)知名的異步事件處理軟件庫(kù))對(duì) select,poll,epoll ,kqueue 這幾個(gè) I/O 多路復(fù)用技術(shù)做的性能測(cè)試。

 

很多文章在描述 epoll 性能時(shí)都引用了這個(gè)基準(zhǔn)測(cè)試,但少有文章能夠清晰的解釋這個(gè)測(cè)試結(jié)果。

這是一個(gè)限制了 100 個(gè)活躍連接的基準(zhǔn)測(cè)試,每個(gè)連接發(fā)生 1000 次讀寫操作為止??v軸是請(qǐng)求的響應(yīng)時(shí)間,橫軸是持有的 socket 句柄數(shù)量。

隨著句柄數(shù)量的增加,epoll 和 kqueue 響應(yīng)時(shí)間幾乎無(wú)變化,而 poll 和 select 的響應(yīng)時(shí)間卻增長(zhǎng)了非常多。

可以看出來(lái),epoll 性能是很高的,并且隨著監(jiān)聽的文件描述符的增加,epoll 的優(yōu)勢(shì)更加明顯。

不過(guò),這里限制的 100 個(gè)連接很重要。epoll 在應(yīng)對(duì)大量網(wǎng)絡(luò)連接時(shí),只有活躍連接很少的情況下才能表現(xiàn)的性能優(yōu)異。

換句話說(shuō),epoll 在處理大量非活躍的連接時(shí)性能才會(huì)表現(xiàn)的優(yōu)異。如果15000個(gè) socket 都是活躍的,epoll 和 select 其實(shí)差不了太多。

為什么 epoll 的高性能有這樣的局限性?問題好像越來(lái)越多了,看來(lái)我們需要更深入的研究了。

epoll 背后的原理

阻塞

①為什么阻塞

我們以網(wǎng)卡接收數(shù)據(jù)舉例,回顧一下之前我分享過(guò)的網(wǎng)卡接收數(shù)據(jù)的過(guò)程。

 

為了方便理解,我盡量簡(jiǎn)化技術(shù)細(xì)節(jié),可以把接收數(shù)據(jù)的過(guò)程分為四步:

  • NIC(網(wǎng)卡)接收到數(shù)據(jù),通過(guò) DMA 方式寫入內(nèi)存(Ring Buffer 和 sk_buff)。
  • NIC 發(fā)出中斷請(qǐng)求(IRQ),告訴內(nèi)核有新的數(shù)據(jù)過(guò)來(lái)了。
  • Linux 內(nèi)核響應(yīng)中斷,系統(tǒng)切換為內(nèi)核態(tài),處理 Interrupt Handler,從RingBuffer 拿出一個(gè) Packet, 并處理協(xié)議棧,填充 Socket 并交給用戶進(jìn)程。
  • 系統(tǒng)切換為用戶態(tài),用戶進(jìn)程處理數(shù)據(jù)內(nèi)容。

網(wǎng)卡何時(shí)接收到數(shù)據(jù)是依賴發(fā)送方和傳輸路徑的,這個(gè)延遲通常都很高,是毫秒(ms)級(jí)別的。而應(yīng)用程序處理數(shù)據(jù)是納秒(ns)級(jí)別的。

也就是說(shuō)整個(gè)過(guò)程中,內(nèi)核態(tài)等待數(shù)據(jù),處理協(xié)議棧是個(gè)相對(duì)很慢的過(guò)程。這么長(zhǎng)的時(shí)間里,用戶態(tài)的進(jìn)程是無(wú)事可做的,因此用到了“阻塞(掛起)”。

②阻塞不占用 CPU

阻塞是進(jìn)程調(diào)度的關(guān)鍵一環(huán),指的是進(jìn)程在等待某事件發(fā)生之前的等待狀態(tài)。

請(qǐng)看下表,在 Linux 中,進(jìn)程狀態(tài)大致有 7 種(在 include/linux/sched.h 中有更多狀態(tài)):

 

從說(shuō)明中其實(shí)就可以發(fā)現(xiàn),“可運(yùn)行狀態(tài)”會(huì)占用 CPU 資源,另外創(chuàng)建和銷毀進(jìn)程也需要占用 CPU 資源(內(nèi)核)。重點(diǎn)是,當(dāng)進(jìn)程被"阻塞/掛起"時(shí),是不會(huì)占用 CPU 資源的。

換個(gè)角度來(lái)講。為了支持多任務(wù),Linux 實(shí)現(xiàn)了進(jìn)程調(diào)度的功能(CPU 時(shí)間片的調(diào)度)。

而這個(gè)時(shí)間片的切換,只會(huì)在“可運(yùn)行狀態(tài)”的進(jìn)程間進(jìn)行。因此“阻塞/掛起”的進(jìn)程是不占用 CPU 資源的。

另外講個(gè)知識(shí)點(diǎn),為了方便時(shí)間片的調(diào)度,所有“可運(yùn)行狀態(tài)”狀態(tài)的進(jìn)程,會(huì)組成一個(gè)隊(duì)列,就叫“工作隊(duì)列”。

③阻塞的恢復(fù)

內(nèi)核當(dāng)然可以很容易的修改一個(gè)進(jìn)程的狀態(tài),問題是網(wǎng)絡(luò) IO 中,內(nèi)核該修改那個(gè)進(jìn)程的狀態(tài)。

 

socket 結(jié)構(gòu)體,包含了兩個(gè)重要數(shù)據(jù):進(jìn)程 ID 和端口號(hào)。進(jìn)程 ID 存放的就是執(zhí)行 connect,send,read 函數(shù),被掛起的進(jìn)程。

在 socket 創(chuàng)建之初,端口號(hào)就被確定了下來(lái),操作系統(tǒng)會(huì)維護(hù)一個(gè)端口號(hào)到 socket 的數(shù)據(jù)結(jié)構(gòu)。

當(dāng)網(wǎng)卡接收到數(shù)據(jù)時(shí),數(shù)據(jù)中一定會(huì)帶著端口號(hào),內(nèi)核就可以找到對(duì)應(yīng)的 socket,并從中取得“掛起”進(jìn)程的 ID。

將進(jìn)程的狀態(tài)修改為“可運(yùn)行狀態(tài)”(加入到工作隊(duì)列)。此時(shí)內(nèi)核代碼執(zhí)行完畢,將控制權(quán)交還給用戶態(tài)。通過(guò)正常的“CPU 時(shí)間片的調(diào)度”,用戶進(jìn)程得以處理數(shù)據(jù)。

④進(jìn)程模型

上面介紹的整個(gè)過(guò)程,基本就是 BIO(阻塞 IO)的基本原理了。用戶進(jìn)程都是獨(dú)立的處理自己的業(yè)務(wù),這其實(shí)是一種符合進(jìn)程模型的處理方式。

上下文切換的優(yōu)化

上面介紹的過(guò)程中,有兩個(gè)地方會(huì)造成頻繁的上下文切換,效率可能會(huì)很低:

  • 如果頻繁的收到數(shù)據(jù)包,NIC 可能頻繁發(fā)出中斷請(qǐng)求(IRQ)。CPU 也許在用戶態(tài),也許在內(nèi)核態(tài),也許還在處理上一條數(shù)據(jù)的協(xié)議棧。但無(wú)論如何,CPU 都要盡快的響應(yīng)中斷。這么做實(shí)際上非常低效,造成了大量的上下文切換,也可能導(dǎo)致用戶進(jìn)程長(zhǎng)時(shí)間無(wú)法獲得數(shù)據(jù)。(即使是多核,每次協(xié)議棧都沒有處理完,自然無(wú)法交給用戶進(jìn)程)
  • 每個(gè) Packet 對(duì)應(yīng)一個(gè) socket,每個(gè) socket 對(duì)應(yīng)一個(gè)用戶態(tài)的進(jìn)程。這些用戶態(tài)進(jìn)程轉(zhuǎn)為“可運(yùn)行狀態(tài)”,必然要引起進(jìn)程間的上下文切換。

①網(wǎng)卡驅(qū)動(dòng)的 NAPI 機(jī)制

在 NIC 上,解決頻繁 IRQ 的技術(shù)叫做 New API(NAPI)。

原理其實(shí)特別簡(jiǎn)單,把 Interrupt Handler 分為兩部分:

  • 函數(shù)名為 napi_schedule,專門快速響應(yīng) IRQ,只記錄必要信息,并在合適的時(shí)機(jī)發(fā)出軟中斷 softirq。
  • 函數(shù)名為 netrxaction,在另一個(gè)進(jìn)程中執(zhí)行,專門響應(yīng) napi_schedule 發(fā)出的軟中斷,批量的處理 RingBuffer 中的數(shù)據(jù)。

 

所以使用了 NAPI 的驅(qū)動(dòng),接收數(shù)據(jù)過(guò)程可以簡(jiǎn)化描述為:

  • NIC 接收到數(shù)據(jù),通過(guò) DMA 方式寫入內(nèi)存(Ring Buffer 和 sk_buff)。
  • NIC 發(fā)出中斷請(qǐng)求(IRQ),告訴內(nèi)核有新的數(shù)據(jù)過(guò)來(lái)了。
  • driver 的 napi_schedule 函數(shù)響應(yīng) IRQ,并在合適的時(shí)機(jī)發(fā)出軟中斷(NET_RX_SOFTIRQ)。
  • driver 的 net_rx_action 函數(shù)響應(yīng)軟中斷,從 Ring Buffer 中批量拉取收到的數(shù)據(jù)。并處理協(xié)議棧,填充 Socket 并交給用戶進(jìn)程。
  • 系統(tǒng)切換為用戶態(tài),多個(gè)用戶進(jìn)程切換為“可運(yùn)行狀態(tài)”,按 CPU 時(shí)間片調(diào)度,處理數(shù)據(jù)內(nèi)容。

一句話概括就是:等著收到一批數(shù)據(jù),再一次批量的處理數(shù)據(jù)。

②單線程的 IO 多路復(fù)用

內(nèi)核優(yōu)化“進(jìn)程間上下文切換”的技術(shù)叫的“IO 多路復(fù)用”,思路和 NAPI 是很接近的。

每個(gè) socket 不再阻塞讀寫它的進(jìn)程,而是用一個(gè)專門的線程,批量的處理用戶態(tài)數(shù)據(jù),這樣就減少了線程間的上下文切換。

 

作為 IO 多路復(fù)用的一個(gè)實(shí)現(xiàn),select 的原理也很簡(jiǎn)單。所有的 socket 統(tǒng)一保存執(zhí)行 select 函數(shù)的(監(jiān)視進(jìn)程)進(jìn)程 ID。

任何一個(gè) socket 接收了數(shù)據(jù),都會(huì)喚醒“監(jiān)視進(jìn)程”。內(nèi)核只要告訴“監(jiān)視進(jìn)程”,那些 socket 已經(jīng)就緒,監(jiān)視進(jìn)程就可以批量處理了。

IO 多路復(fù)用的進(jìn)化

①對(duì)比 epoll 與 select

select,poll 和 epoll 都是“IO 多路復(fù)用”,那為什么還會(huì)有性能差距呢?篇幅限制,這里我們只簡(jiǎn)單對(duì)比 select 和 epoll 的基本原理差異。

對(duì)于內(nèi)核,同時(shí)處理的 socket 可能有很多,監(jiān)視進(jìn)程也可能有多個(gè)。所以監(jiān)視進(jìn)程每次“批量處理數(shù)據(jù)”,都需要告訴內(nèi)核它“關(guān)心的 socket”。

內(nèi)核在喚醒監(jiān)視進(jìn)程時(shí),就可以把“關(guān)心的 socket”中,就緒的 socket 傳給監(jiān)視進(jìn)程。

換句話說(shuō),在執(zhí)行系統(tǒng)調(diào)用 select 或 epoll_create 時(shí),入?yún)⑹?ldquo;關(guān)心的 socket”,出參是“就緒的 socket”。

而 select 與 epoll 的區(qū)別在于:

select (一次O(n)查找):

  • 每次傳給內(nèi)核一個(gè)用戶空間分配的 fd_set 用于表示“關(guān)心的 socket”。其結(jié)構(gòu)(相當(dāng)于 bitset)限制了只能保存 1024 個(gè) socket。
  • 每次 socket 狀態(tài)變化,內(nèi)核利用 fd_set 查詢 O(1),就能知道監(jiān)視進(jìn)程是否關(guān)心這個(gè) socket。
  • 內(nèi)核是復(fù)用了 fd_set 作為出參,返還給監(jiān)視進(jìn)程(所以每次 select 入?yún)⑿枰刂?。

然而監(jiān)視進(jìn)程必須遍歷一遍 socket 數(shù)組 O(n),才知道哪些 socket 就緒了。

epoll (全是O(1)查找):

  • 每次傳給內(nèi)核一個(gè)實(shí)例句柄。這個(gè)句柄是在內(nèi)核分配的紅黑樹 rbr+雙向鏈表 rdllist。只要句柄不變,內(nèi)核就能復(fù)用上次計(jì)算的結(jié)果。
  • 每次 socket 狀態(tài)變化,內(nèi)核就可以快速?gòu)?rbr 查詢O(1),監(jiān)視進(jìn)程是否關(guān)心這個(gè) socket。同時(shí)修改 rdllist,所以 rdllist 實(shí)際上是“就緒的 socket”的一個(gè)緩存。
  • 內(nèi)核復(fù)制 rdllist 的一部分或者全部(LT 和 ET),到專門的 epoll_event 作為出參。

所以監(jiān)視進(jìn)程,可以直接一個(gè)個(gè)處理數(shù)據(jù),無(wú)需再遍歷確認(rèn)。

Select 示例代碼:

 

Epoll 示例代碼:

 

另外,epoll_create 底層實(shí)現(xiàn),到底是不是紅黑樹,其實(shí)也不太重要(完全可以換成 hashtable)。

重要的是 efd 是個(gè)指針,其數(shù)據(jù)結(jié)構(gòu)完全可以對(duì)外透明的修改成任意其他數(shù)據(jù)結(jié)構(gòu)。

②API 發(fā)布的時(shí)間線

另外,我們?cè)賮?lái)看看網(wǎng)絡(luò) IO 中,各個(gè) api 的發(fā)布時(shí)間線:

  • 1983,socket 發(fā)布在 Unix(4.2 BSD)
  • 1983,select 發(fā)布在 Unix(4.2 BSD)
  • 1994,Linux 的 1.0,已經(jīng)支持 socket 和 select
  • 1997,poll 發(fā)布在 Linux 2.1.23
  • 2002,epoll 發(fā)布在 Linux 2.5.44

就可以得到兩個(gè)有意思的結(jié)論:

  • socket 和 select 是同時(shí)發(fā)布的。這說(shuō)明了,select 不是用來(lái)代替?zhèn)鹘y(tǒng) IO 的。這是兩種不同的用法(或模型),適用于不同的場(chǎng)景。
  • select、poll 和 epoll,這三個(gè)“IO 多路復(fù)用 API”是相繼發(fā)布的。這說(shuō)明了,它們是 IO 多路復(fù)用的 3 個(gè)進(jìn)化版本。

因?yàn)?API 設(shè)計(jì)缺陷,無(wú)法在不改變 API 的前提下優(yōu)化內(nèi)部邏輯。所以用 poll 替代 select,再用 epoll 替代 poll。

總結(jié)

我們花了三個(gè)章節(jié),闡述 epoll 背后的原理,現(xiàn)在用三句話總結(jié)一下:

  • 基于數(shù)據(jù)收發(fā)的基本原理,系統(tǒng)利用阻塞提高了 CPU 利用率。
  • 為了優(yōu)化上線文切換,設(shè)計(jì)了“IO 多路復(fù)用”(和 NAPI)。
  • 為了優(yōu)化“內(nèi)核與監(jiān)視進(jìn)程的交互”,設(shè)計(jì)了三個(gè)版本的 API(select,poll,epoll)。

Diss 環(huán)節(jié)

講完“epoll 背后的原理”,已經(jīng)可以回答最初的幾個(gè)問題。這已經(jīng)是一個(gè)完整的文章,很多人勸我刪掉下面的 diss 環(huán)節(jié)。

我的觀點(diǎn)是:學(xué)習(xí)就是個(gè)研究+理解的過(guò)程。上面是研究,下面再講一下我的個(gè)人“理解”,歡迎指正。

關(guān)于 IO 模型的分類

關(guān)于阻塞,非阻塞,同步,異步的分類,這么分自然有其道理。但是在操作系統(tǒng)的角度來(lái)看“這樣分類,容易產(chǎn)生誤解,并不好”。

 

①阻塞和非阻塞

Linux 下所有的 IO 模型都是阻塞的,這是收發(fā)數(shù)據(jù)的基本原理導(dǎo)致的。阻塞用戶線程是一種高效的方式。

你當(dāng)然可以寫一個(gè)程序,socket 設(shè)置成非阻塞模式,在不使用監(jiān)視器的情況下,依靠死循環(huán)完成一次 IO 操作。但是這樣做的效率實(shí)在是太低了,完全沒有實(shí)際意義。

換句話說(shuō),阻塞不是問題,運(yùn)行才是問題,運(yùn)行才會(huì)消耗 CPU。IO 多路復(fù)用不是減少了阻塞,是減少了運(yùn)行。

上下文切換才是問題,IO 多路復(fù)用,通過(guò)減少運(yùn)行的進(jìn)程,有效的減少了上下文切換。

②同步和異步

Linux 下所有的 IO 模型都是同步的。BIO 是同步的,select 同步的,poll 同步的,epoll 還是同步的。

Java 提供的 AIO,也許可以稱作“異步”的。但是 JVM 是運(yùn)行在用戶態(tài)的,Linux 沒有提供任何的異步支持。

因此 JVM 提供的異步支持,和你自己封裝成“異步”的框架是沒有本質(zhì)區(qū)別的(你完全可以使用 BIO 封裝成異步框架)。

所謂的“同步“和”異步”只是兩種事件分發(fā)器(event dispatcher)或者說(shuō)是兩個(gè)設(shè)計(jì)模式(Reactor 和 Proactor)。

都是運(yùn)行在用戶態(tài)的,兩個(gè)設(shè)計(jì)模式能有多少性能差異呢?

  • Reactor 對(duì)應(yīng) Java 的 NIO,也就是 Channel,Buffer 和 Selector 構(gòu)成的核心的 API。
  • Proactor對(duì)應(yīng) java 的 AIO,也就是 Async 組件和 Future 或 Callback 構(gòu)成的核心的 API。

③我的分類

我認(rèn)為 IO 模型只分兩類:

  • 更加符合程序員理解和使用的,進(jìn)程模型。
  • 更加符合操作系統(tǒng)處理邏輯的,IO 多路復(fù)用模型。

對(duì)于“IO 多路復(fù)用”的事件分發(fā),又分為兩類:Reactor 和 Proactor。

關(guān)于 mmap

epoll 到底用沒用到 mmap?答案:沒有!

這是個(gè)以訛傳訛的謠言。其實(shí)很容易證明的,用 epoll 寫個(gè) demo。strace 一下就清楚了。

作者:馮志明

簡(jiǎn)介:2019 年至今負(fù)責(zé)搜索算法的相關(guān)工作,擅長(zhǎng)處理復(fù)雜的業(yè)務(wù)系統(tǒng),對(duì)底層技術(shù)有濃厚興趣。

編輯:陶家龍

出處:轉(zhuǎn)載自公眾號(hào)Qunar技術(shù)沙龍(ID:QunarTL)

 

責(zé)任編輯:武曉燕 來(lái)源: Qunar技術(shù)沙龍
相關(guān)推薦

2021-03-04 08:06:13

Java代理機(jī)制

2021-10-07 20:12:03

MVCC事務(wù)原理

2021-08-12 10:36:18

order byMySQL數(shù)據(jù)庫(kù)

2023-01-10 19:47:47

Redis原理多線程

2017-12-26 14:17:24

潤(rùn)乾報(bào)表

2021-06-15 07:15:15

Oracle底層explain

2022-01-17 20:59:37

開發(fā)group by思路

2020-12-18 06:09:07

Java淺拷貝深拷貝

2025-02-13 09:06:27

2021-12-01 07:26:13

IO模型異步

2024-05-21 08:40:21

分庫(kù)分表源碼

2015-10-10 11:10:24

重敲代碼拷貝粘貼

2024-03-26 07:59:32

IO模型多路復(fù)用

2023-09-12 07:31:45

HashMap線程

2024-03-12 08:20:57

零拷貝存儲(chǔ)開發(fā)

2019-09-19 08:04:40

網(wǎng)絡(luò)七層模型TCPUDP

2020-02-09 17:30:54

反轉(zhuǎn)鏈表程序員節(jié)點(diǎn)

2022-02-22 09:16:41

AndroidWindows狀態(tài)欄

2010-01-02 10:38:51

360商業(yè)拓展dell

2022-08-26 10:41:03

指針C語(yǔ)言
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)