Netty權(quán)威指南讀書筆記1:Java的I/O演進(jìn)之路

linux網(wǎng)絡(luò)I/O模型簡(jiǎn)介

1.1 用戶空間以及內(nèi)核空間概念
針對(duì)linux操作系統(tǒng)而言,將最高的1G字節(jié)(從虛擬地址0xC0000000到0xFFFFFFFF),供內(nèi)核使用,稱為內(nèi)核空間,而將較低的3G字節(jié)(從虛擬地址0x00000000到0xBFFFFFFF),供各個(gè)進(jìn)程使用,稱為用戶空間。

image.png

有了用戶空間和內(nèi)核空間,整個(gè)linux內(nèi)部結(jié)構(gòu)可以分為三部分,從最底層到最上層依次是:硬件-->內(nèi)核空間-->用戶空間。
我們都知道,為了OS的安全性等的考慮,進(jìn)程是無(wú)法直接操作I/O設(shè)備的,其必須通過(guò)系統(tǒng)調(diào)用請(qǐng)求內(nèi)核來(lái)協(xié)助完成I/O動(dòng)作,而內(nèi)核會(huì)為每個(gè)I/O設(shè)備維護(hù)一個(gè)buffer

image.png

整個(gè)請(qǐng)求過(guò)程為: 用戶進(jìn)程發(fā)起請(qǐng)求,內(nèi)核接受到請(qǐng)求后,從I/O設(shè)備中獲取數(shù)據(jù)到buffer中,再將buffer中的數(shù)據(jù)copy到用戶進(jìn)程的地址空間,該用戶進(jìn)程獲取到數(shù)據(jù)后再響應(yīng)客戶端。

  • 在整個(gè)請(qǐng)求過(guò)程中,數(shù)據(jù)輸入至buffer需要時(shí)間,而從buffer復(fù)制數(shù)據(jù)至進(jìn)程也需要時(shí)間。因此根據(jù)在這兩段時(shí)間內(nèi)等待方式的不同,I/O動(dòng)作可以分為以下五種模式:

    • 阻塞I/O (Blocking I/O)

    • 非阻塞I/O (Non-Blocking I/O)

    • I/O復(fù)用(I/O Multiplexing)

    • 信號(hào)驅(qū)動(dòng)的I/O (Signal Driven I/O)

    • 異步I/O (Asynchrnous I/O)

2.1 阻塞I/O (Blocking I/O)

??在linux中,默認(rèn)情況下所有的socket都是blocking,一個(gè)典型的讀操作流程大概是這樣:

image

??當(dāng)用戶進(jìn)程調(diào)用了recvfrom這個(gè)系統(tǒng)調(diào)用,內(nèi)核就開(kāi)始了IO的第一個(gè)階段:等待數(shù)據(jù)準(zhǔn)備。對(duì)于network io來(lái)說(shuō),很多時(shí)候數(shù)據(jù)在一開(kāi)始還沒(méi)有到達(dá)(比如,還沒(méi)有收到一個(gè)完整的UDP包),這個(gè)時(shí)候內(nèi)核就要等待足夠的數(shù)據(jù)到來(lái)。而在用戶進(jìn)程這邊,整個(gè)進(jìn)程會(huì)被阻塞。當(dāng)內(nèi)核一直等到數(shù)據(jù)準(zhǔn)備好了,它就會(huì)將數(shù)據(jù)從內(nèi)核中拷貝到用戶內(nèi)存,然后內(nèi)核返回結(jié)果,用戶進(jìn)程才解除block的狀態(tài),重新運(yùn)行起來(lái)。 所以,blocking IO的特點(diǎn)就是在IO執(zhí)行的兩個(gè)階段都被block了。(整個(gè)過(guò)程一直是阻塞的)

2.2 非阻塞I/O (Non-Blocking I/O)

??linux下,可以通過(guò)設(shè)置socket使其變?yōu)閚on-blocking。當(dāng)對(duì)一個(gè)non-blocking socket執(zhí)行讀操作時(shí),流程是這個(gè)樣子:

image

??當(dāng)用戶進(jìn)程調(diào)用recvfrom時(shí),系統(tǒng)不會(huì)阻塞用戶進(jìn)程,而是立刻返回一個(gè)ewouldblock錯(cuò)誤,從用戶進(jìn)程角度講 ,并不需要等待,而是馬上就得到了一個(gè)結(jié)果(這個(gè)結(jié)果就是ewouldblock )。用戶進(jìn)程判斷標(biāo)志是ewouldblock時(shí),就知道數(shù)據(jù)還沒(méi)準(zhǔn)備好,于是它就可以去做其他的事了,于是它可以再次發(fā)送recvfrom,一旦內(nèi)核中的數(shù)據(jù)準(zhǔn)備好了。并且又再次收到了用戶進(jìn)程的system call,那么它馬上就將數(shù)據(jù)拷貝到了用戶內(nèi)存,然后返回。 當(dāng)一個(gè)應(yīng)用程序在一個(gè)循環(huán)里對(duì)一個(gè)非阻塞調(diào)用recvfrom,我們稱為輪詢。應(yīng)用程序不斷輪詢內(nèi)核,看看是否已經(jīng)準(zhǔn)備好了某些操作。這通常是浪費(fèi)CPU時(shí)間,但這種模式偶爾會(huì)遇到。

2.3 I/O復(fù)用(I/O Multiplexing)

??IO multiplexing這個(gè)詞可能有點(diǎn)陌生,但是如果我說(shuō)select,epoll,大概就都能明白了。有些地方也稱這種IO方式為event driven IO。我們都知道,select/epoll的好處就在于單個(gè)process就可以同時(shí)處理多個(gè)網(wǎng)絡(luò)連接的IO。它的基本原理就是select/epoll這個(gè)function會(huì)不斷的輪詢所負(fù)責(zé)的所有socket,當(dāng)某個(gè)socket有數(shù)據(jù)到達(dá)了,就通知用戶進(jìn)程。它的流程如圖:

image

??當(dāng)用戶進(jìn)程調(diào)用了select,那么整個(gè)進(jìn)程會(huì)被block,而同時(shí),內(nèi)核會(huì)“監(jiān)視”所有select負(fù)責(zé)的socket,當(dāng)任何一個(gè)socket中的數(shù)據(jù)準(zhǔn)備好了,select就會(huì)返回。這個(gè)時(shí)候用戶進(jìn)程再調(diào)用read操作,將數(shù)據(jù)從內(nèi)核拷貝到用戶進(jìn)程。 這個(gè)圖和blocking IO的圖其實(shí)并沒(méi)有太大的不同,事實(shí)上,還更差一些。因?yàn)檫@里需要使用兩個(gè)system call (select 和 recvfrom),而blocking IO只調(diào)用了一個(gè)system call (recvfrom)。但是,用select的優(yōu)勢(shì)在于它可以同時(shí)處理多個(gè)connection。(多說(shuō)一句。所以,如果處理的連接數(shù)不是很高的話,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延遲還更大。select/epoll的優(yōu)勢(shì)并不是對(duì)于單個(gè)連接能處理得更快,而是在于能處理更多的連接。) 在IO multiplexing Model中,實(shí)際中,對(duì)于每一個(gè)socket,一般都設(shè)置成為non-blocking,但是,如上圖所示,整個(gè)用戶的process其實(shí)是一直被block的。只不過(guò)process是被select這個(gè)函數(shù)block,而不是被socket IO給block。

文件描述符fd

Linux的內(nèi)核將所有外部設(shè)備都可以看做一個(gè)文件來(lái)操作。那么我們對(duì)與外部設(shè)備的操作都可以看做對(duì)文件進(jìn)行操作。我們對(duì)一個(gè)文件的讀寫,都通過(guò)調(diào)用內(nèi)核提供的系統(tǒng)調(diào)用;內(nèi)核給我們返回一個(gè)filede scriptor(fd,文件描述符)。而對(duì)一個(gè)socket的讀寫也會(huì)有相應(yīng)的描述符,稱為socketfd(socket描述符)。描述符就是一個(gè)數(shù)字,指向內(nèi)核中一個(gè)結(jié)構(gòu)體(文件路徑,數(shù)據(jù)區(qū),等一些屬性)。那么我們的應(yīng)用程序?qū)ξ募淖x寫就通過(guò)對(duì)描述符的讀寫完成。

select

基本原理:select 函數(shù)監(jiān)視的文件描述符分3類,分別是writefds、readfds、和exceptfds。調(diào)用后select函數(shù)會(huì)阻塞,直到有描述符就緒(有數(shù)據(jù) 可讀、可寫、或者有except),或者超時(shí)(timeout指定等待時(shí)間,如果立即返回設(shè)為null即可),函數(shù)返回。當(dāng)select函數(shù)返回后,可以通過(guò)遍歷fdset,來(lái)找到就緒的描述符。

缺點(diǎn): 1、select最大的缺陷就是單個(gè)進(jìn)程所打開(kāi)的FD是有一定限制的,它由FDSETSIZE設(shè)置,32位機(jī)默認(rèn)是1024個(gè),64位機(jī)默認(rèn)是2048。 一般來(lái)說(shuō)這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大,”具體數(shù)目可以cat /proc/sys/fs/file-max察看”。32位機(jī)默認(rèn)是1024個(gè)。64位機(jī)默認(rèn)是2048. 2、對(duì)socket進(jìn)行掃描時(shí)是線性掃描,即采用輪詢的方法,效率較低。 當(dāng)套接字比較多的時(shí)候,每次select()都要通過(guò)遍歷FDSETSIZE個(gè)Socket來(lái)完成調(diào)度,不管哪個(gè)Socket是活躍的,都遍歷一遍。這會(huì)浪費(fèi)很多CPU時(shí)間?!比绻芙o套接字注冊(cè)某個(gè)回調(diào)函數(shù),當(dāng)他們活躍時(shí),自動(dòng)完成相關(guān)操作,那就避免了輪詢”,這正是epoll與kqueue做的。 3、需要維護(hù)一個(gè)用來(lái)存放大量fd的數(shù)據(jù)結(jié)構(gòu),這樣會(huì)使得用戶空間和內(nèi)核空間在傳遞該結(jié)構(gòu)時(shí)復(fù)制開(kāi)銷大。

poll

基本原理:poll本質(zhì)上和select沒(méi)有區(qū)別,它將用戶傳入的數(shù)組拷貝到內(nèi)核空間,然后查詢每個(gè)fd對(duì)應(yīng)的設(shè)備狀態(tài),如果設(shè)備就緒則在設(shè)備等待隊(duì)列中加入一項(xiàng)并繼續(xù)遍歷,如果遍歷完所有fd后沒(méi)有發(fā)現(xiàn)就緒設(shè)備,則掛起當(dāng)前進(jìn)程,直到設(shè)備就緒或者主動(dòng)超時(shí),被喚醒后它又要再次遍歷fd。這個(gè)過(guò)程經(jīng)歷了多次無(wú)謂的遍歷。

它沒(méi)有最大連接數(shù)的限制,原因是它是基于鏈表來(lái)存儲(chǔ)的,但是同樣有一個(gè)缺點(diǎn):1、大量的fd的數(shù)組被整體復(fù)制于用戶態(tài)和內(nèi)核地址空間之間,而不管這樣的復(fù)制是不是有意義。 2 、poll還有一個(gè)特點(diǎn)是“水平觸發(fā)”,如果報(bào)告了fd后,沒(méi)有被處理,那么下次poll時(shí)會(huì)再次報(bào)告該fd。

注意:從上面看,select和poll都需要在返回后,通過(guò)遍歷文件描述符來(lái)獲取已經(jīng)就緒的socket。事實(shí)上,同時(shí)連接的大量客戶端在一時(shí)刻可能只有很少的處于就緒狀態(tài),因此隨著監(jiān)視的描述符數(shù)量的增長(zhǎng),其效率也會(huì)線性下降。

epoll(事件驅(qū)動(dòng))

epoll是在2.6內(nèi)核中提出的,是之前的select和poll的增強(qiáng)版本。相對(duì)于select和poll來(lái)說(shuō),epoll更加靈活,沒(méi)有描述符限制。epoll使用一個(gè)文件描述符管理多個(gè)描述符,將用戶關(guān)系的文件描述符的事件存放到內(nèi)核的一個(gè)事件表中,這樣在用戶空間和內(nèi)核空間的copy只需一次。

基本原理:epoll支持水平觸發(fā)和邊緣觸發(fā),最大的特點(diǎn)在于邊緣觸發(fā),它只告訴進(jìn)程哪些fd剛剛變?yōu)榫途w態(tài),并且只會(huì)通知一次。還有一個(gè)特點(diǎn)是,epoll使用“事件”的就緒通知方式,通過(guò)epollctl注冊(cè)fd,一旦該fd就緒,內(nèi)核就會(huì)采用類似callback的回調(diào)機(jī)制來(lái)激活該fd,epollwait便可以收到通知。

epoll的優(yōu)點(diǎn):1、沒(méi)有最大并發(fā)連接的限制,能打開(kāi)的FD的上限遠(yuǎn)大于1024(1G的內(nèi)存上能監(jiān)聽(tīng)約10萬(wàn)個(gè)端口)。 2、效率提升,不是輪詢的方式,不會(huì)隨著FD數(shù)目的增加效率下降。 只有活躍可用的FD才會(huì)調(diào)用callback函數(shù);即Epoll最大的優(yōu)點(diǎn)就在于它只管你“活躍”的連接,而跟連接總數(shù)無(wú)關(guān),因此在實(shí)際的網(wǎng)絡(luò)環(huán)境中,Epoll的效率就會(huì)遠(yuǎn)遠(yuǎn)高于select和poll。 3、內(nèi)存拷貝,利用mmap()文件映射內(nèi)存加速與內(nèi)核空間的消息傳遞;即epoll使用mmap減少?gòu)?fù)制開(kāi)銷。

JDK1.5_update10版本使用epoll替代了傳統(tǒng)的select/poll,極大的提升了NIO通信的性能。

備注:JDK NIO的BUG,例如臭名昭著的epoll bug,它會(huì)導(dǎo)致Selector空輪詢,最終導(dǎo)致CPU 100%。官方聲稱在JDK1.6版本的update18修復(fù)了該問(wèn)題,但是直到JDK1.7版本該問(wèn)題仍舊存在,只不過(guò)該BUG發(fā)生概率降低了一些而已,它并沒(méi)有被根本解決。這個(gè)可以在后續(xù)netty系列里面進(jìn)行說(shuō)明下。

信號(hào)驅(qū)動(dòng)的I/O (Signal Driven I/O)

image

很明顯可以看出用戶進(jìn)程不是阻塞的。首先用戶進(jìn)程建立SIGIO信號(hào)處理程序,并通過(guò)系統(tǒng)調(diào)用sigaction執(zhí)行一個(gè)信號(hào)處理函數(shù),這時(shí)用戶進(jìn)程便可以做其他的事了,一旦數(shù)據(jù)準(zhǔn)備好,系統(tǒng)便為該進(jìn)程生成一個(gè)SIGIO信號(hào),去通知它數(shù)據(jù)已經(jīng)準(zhǔn)備好了,于是用戶進(jìn)程便調(diào)用recvfrom把數(shù)據(jù)從內(nèi)核拷貝出來(lái),并返回結(jié)果。

3.5 異步I/O

一般來(lái)說(shuō),這些函數(shù)通過(guò)告訴內(nèi)核啟動(dòng)操作并在整個(gè)操作(包括內(nèi)核的數(shù)據(jù)到緩沖區(qū)的副本)完成時(shí)通知我們。這個(gè)模型和前面的信號(hào)驅(qū)動(dòng)I/O模型的主要區(qū)別是,在信號(hào)驅(qū)動(dòng)的I/O中,內(nèi)核告訴我們何時(shí)可以啟動(dòng)I/O操作,但是異步I/O時(shí),內(nèi)核告訴我們何時(shí)I/O操作完成。

image

當(dāng)用戶進(jìn)程向內(nèi)核發(fā)起某個(gè)操作后,會(huì)立刻得到返回,并把所有的任務(wù)都交給內(nèi)核去完成(包括將數(shù)據(jù)從內(nèi)核拷貝到用戶自己的緩沖區(qū)),內(nèi)核完成之后,只需返回一個(gè)信號(hào)告訴用戶進(jìn)程已經(jīng)完成就可以了。

5中I/O模型的對(duì)比

image

結(jié)果表明:前四個(gè)模型之間的主要區(qū)別是第一階段,四個(gè)模型的第二階段是一樣的:過(guò)程受阻在調(diào)用recvfrom當(dāng)數(shù)據(jù)從內(nèi)核拷貝到用戶緩沖區(qū)。然而,異步I/O處理兩個(gè)階段,與前四個(gè)不同。

從同步、異步,以及阻塞、非阻塞兩個(gè)維度來(lái)劃分來(lái)看:

image

參考文檔
匠心零度的公眾號(hào)
Netty權(quán)威指南第一章

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容