(一)網(wǎng)絡(luò)基礎(chǔ)之 IO 模型

IO 讀寫的基礎(chǔ)原理

程序進(jìn)行 IO 的讀寫,依賴于系統(tǒng)底層的 IO 讀寫,基本上會(huì)用到底層的 read&write 兩大系統(tǒng)調(diào)用。在不同的操作系統(tǒng)中,IO 讀寫的系統(tǒng)調(diào)用的名稱可能不完全一樣,但是基本功能是一樣的。

read 系統(tǒng)調(diào)用,并不是直接從物理設(shè)備把數(shù)據(jù)讀取到內(nèi)存中; write 系統(tǒng)調(diào)用,也不是直接把數(shù)據(jù)寫入到物理設(shè)備。上層應(yīng)用無論是調(diào)用操作系統(tǒng)的 read,還是調(diào)用操作系統(tǒng)的 write,都會(huì)涉及緩沖區(qū)。具體來說,調(diào)用操作系統(tǒng)的 read,是把數(shù)據(jù)從系統(tǒng)內(nèi)核緩沖區(qū)復(fù)制到應(yīng)用進(jìn)程緩沖區(qū):而 write 系統(tǒng)調(diào)用,是把數(shù)據(jù)從進(jìn)程緩沖區(qū)復(fù)制到內(nèi)核緩沖區(qū)。

也就是說,應(yīng)用程序的 IO 操作,實(shí)際上不是物理設(shè)備級(jí)別的讀寫,而是緩存的復(fù)制。read&write 兩大系統(tǒng)調(diào)用,都不負(fù)責(zé)數(shù)據(jù)在內(nèi)核緩沖區(qū)和物理設(shè)備(如磁盤)之間的交換,這項(xiàng)底層的讀寫交換,是由操作系統(tǒng)內(nèi)核(Kernel)來完成的。

在程序中,無論是 Socket 的 IO、還是文件 IO 操作,都屬于上層應(yīng)用的開發(fā),它們的輸入( Input)和輸出( Output )的處理,在編程的流程上,都是一致的。

內(nèi)核緩沖區(qū)與進(jìn)程緩沖區(qū)

為什么設(shè)置那么多的緩沖區(qū),為什么要那么麻煩呢?緩沖區(qū)的目的,是為了減少頻繁地與設(shè)備之間的物理交換。大家都知道,外部設(shè)備的直接讀寫,涉及操作系統(tǒng)的中斷。發(fā)生系統(tǒng)中斷時(shí),需要保存之前的進(jìn)程數(shù)據(jù)和狀態(tài)等信息,而結(jié)束中斷之后,還需要恢復(fù)之前的進(jìn)程數(shù)據(jù)和狀態(tài)等信息。為了減少這種底層系統(tǒng)的時(shí)間損耗、性能損耗,于是出現(xiàn)了內(nèi)核緩沖區(qū) 。

有了內(nèi)核緩沖區(qū),應(yīng)用使用 read 系統(tǒng)調(diào)用時(shí),僅僅把數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到應(yīng)用的緩沖區(qū)(進(jìn)程緩沖區(qū));上層應(yīng)用使用 write 系統(tǒng)調(diào)用時(shí),僅僅把數(shù)據(jù)從進(jìn)程緩沖區(qū)復(fù)制到內(nèi)核緩沖區(qū)中。底層操作會(huì)對(duì)內(nèi)核緩沖區(qū)進(jìn)行監(jiān)控,等待緩沖區(qū)達(dá)到一定數(shù)量的時(shí)候,再進(jìn)行 IO 設(shè)備的中斷處理,集中執(zhí)行物理設(shè)備的實(shí)際 IO 操作,這種機(jī)制提升了系統(tǒng)的性能。至于什么時(shí)候中斷(讀中斷、寫中斷),由操作系統(tǒng)的內(nèi)核來決定,程序則不需要關(guān)心。

在 Linux 系統(tǒng)中,操作系統(tǒng)內(nèi)核只有一個(gè)內(nèi)核緩沖區(qū)。而每個(gè)用戶程序(進(jìn)程),有自己獨(dú)立的緩沖區(qū),叫作進(jìn)程緩沖區(qū)。所以,用戶程序的 IO 讀寫程序,在大多數(shù)情況下,并沒有進(jìn)行實(shí)際的 IO 操作,而是在進(jìn)程緩沖區(qū)和內(nèi)核緩沖區(qū)之間直接進(jìn)行數(shù)據(jù)的交換。

詳解系統(tǒng)調(diào)用流程

用戶程序所使用的系統(tǒng)調(diào)用 read&write ,它們不等價(jià)于數(shù)據(jù)在內(nèi)核緩沖區(qū)和磁盤之間的交換。read 把數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到進(jìn)程緩沖區(qū),write 把數(shù)據(jù)從進(jìn)程緩沖區(qū)復(fù)制到內(nèi)核緩沖區(qū),具體的流程如下圖:

image.png

以 read 系統(tǒng)調(diào)用為例,完整輸入流程的兩個(gè)階段:

  • 等待數(shù)據(jù)準(zhǔn)備好
  • 從內(nèi)核向進(jìn)程復(fù)制數(shù)據(jù)

如果是 read 一個(gè) socket(套接字),那么以上兩個(gè)階段的具體處理流程如下:

  • 第一個(gè)階段,等待數(shù)據(jù)從網(wǎng)絡(luò)中到達(dá)網(wǎng)卡。當(dāng)所等待的分組到達(dá)時(shí),它被復(fù)制到內(nèi)核中的某個(gè)緩沖區(qū)。找個(gè)工作有操作系統(tǒng)自動(dòng)完成,用戶程序無感知。
  • 第二個(gè)階段,就是把數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到應(yīng)用進(jìn)程緩沖區(qū)。

如果是在 Java 服務(wù)器端,完成一次 socket 請(qǐng)求和響應(yīng),完整的流程如下:

  • 客戶端請(qǐng)求:通過 write 系統(tǒng)調(diào)用將請(qǐng)求數(shù)據(jù)寫入進(jìn)程緩沖區(qū),然后由系統(tǒng)將數(shù)據(jù)復(fù)制到內(nèi)核緩沖區(qū),將內(nèi)核緩沖區(qū)中的數(shù)據(jù)寫入網(wǎng)卡,網(wǎng)卡通過底層通信協(xié)議將數(shù)據(jù)發(fā)給服務(wù)端。
  • 服務(wù)端獲取請(qǐng)求數(shù)據(jù):服務(wù)端通過 read 系統(tǒng)調(diào)用,將內(nèi)核緩沖區(qū)的數(shù)據(jù)復(fù)制到進(jìn)程緩沖區(qū)。
  • 服務(wù)端返回?cái)?shù)據(jù):完成業(yè)務(wù)處理后,構(gòu)建好響應(yīng)數(shù)據(jù),將這些數(shù)據(jù)通過 write 系統(tǒng)調(diào)用,從用戶緩沖區(qū)寫入內(nèi)核緩沖區(qū)中,然后由系統(tǒng)將內(nèi)核緩沖區(qū)中的數(shù)據(jù)寫入網(wǎng)卡,網(wǎng)卡通過底層通信協(xié)議將數(shù)據(jù)發(fā)給客戶端。

四種主要的 IO 模型

同步阻塞 IO(Blocking IO)

最常用的 IO 模型就是阻塞 IO 模型,在默認(rèn)情況下,所有 IO 操作都是阻塞的。

以套接字接口為例,在阻塞式 IO 模型中,在進(jìn)程空間中調(diào)用 read,其系統(tǒng)調(diào)用直到數(shù)據(jù)包達(dá)到切被復(fù)制到應(yīng)用進(jìn)程的緩沖區(qū)中或者發(fā)生錯(cuò)誤才返回,在此期間線程會(huì)一直等待,再從調(diào)用 read 開始到它返回的整段時(shí)間內(nèi)都是被阻塞的,因此被稱之為阻塞 IO 模型。

image.png

同步非阻塞 IO (None Blocking IO)

socket 默認(rèn)是阻塞模式,在 Linux 系統(tǒng)下,可以設(shè)置成非阻塞模式。使用非阻塞模式的 IO 讀寫。一旦使用非阻塞模式開始 IO 系統(tǒng)調(diào)用,會(huì)出現(xiàn)以下兩種情況:

  • 在內(nèi)核緩沖區(qū)中沒有數(shù)據(jù)的情況下,系統(tǒng)調(diào)用會(huì)立即返回,返回一個(gè)調(diào)用失敗信息
  • 在內(nèi)核緩沖區(qū)中有數(shù)據(jù)的情況下,是阻塞的,直到數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到應(yīng)用進(jìn)程緩沖區(qū)。復(fù)制完成后,系統(tǒng)調(diào)用返回成功,此時(shí)應(yīng)用就可以開始處理獲取到的數(shù)據(jù)了。
image.png

非阻塞 IO 的特點(diǎn):應(yīng)用進(jìn)程的線程需要不斷的進(jìn)行 IO 系統(tǒng)調(diào)用,輪詢數(shù)據(jù)是否已經(jīng)準(zhǔn)備好,如果沒有準(zhǔn)備好,就繼續(xù)輪詢,知道完成 IO 系統(tǒng)調(diào)用為止。

非阻塞 IO 的優(yōu)點(diǎn):每次發(fā)起的 IO 系統(tǒng)調(diào)用,在內(nèi)核等待數(shù)據(jù)過程中可以立即返回。用戶線程不會(huì)阻塞,實(shí)時(shí)性較好。

同步非阻塞 IO 的缺點(diǎn):不斷地輪詢內(nèi)核,這將占用大量的 CPU 時(shí)間,效率低下。

總體來說,在高并發(fā)應(yīng)用場景下,同步非阻塞 IO 也是不可用的。一般 Web 服務(wù)器不使用這種 IO 模型。這種 IO 模型一般很少直接使用,而是在其他 IO 模型中使用非阻塞 IO 這-特性。在 Java 的實(shí)際開發(fā)中,也不會(huì)涉及這種 IO 模型。

這里說明一下,非阻塞 IO,可以簡稱為 NIO,但是,它不是 Java 中的 NIO,雖然它們的英文縮寫一樣,希望大家不要混淆。Java 的 NIO ( New IO ),對(duì)應(yīng)的是另外的一種模型,叫作 IO 多路復(fù)用模型( IO Multiplexing )。

IO 多路復(fù)用模型

在 IO 多路復(fù)用模型中,引入了一種新的系統(tǒng)調(diào)用,查詢 IO 的就緒狀態(tài)。 在 Linux 系統(tǒng)中,對(duì)應(yīng)的系統(tǒng)調(diào)用為 select,epoll 系統(tǒng)調(diào)用。通過該系統(tǒng)調(diào)用,一個(gè)進(jìn)程可以監(jiān)視多個(gè)文件描述符,一旦某個(gè)描述符就緒( 一般是內(nèi)核緩沖區(qū)可讀/可寫〉,內(nèi)核能夠?qū)⒕途w的狀態(tài)返回給應(yīng)用程序。隨后,應(yīng)用程序根據(jù)就緒的狀態(tài),進(jìn)行相應(yīng)的 IO 系統(tǒng)調(diào)用。

目前支持 IO 多路復(fù)用的系統(tǒng)調(diào)用,有 select、epoll 等等。select 系統(tǒng)調(diào)用,幾乎在所有的操作系統(tǒng)上都有支持,具有良好的跨平臺(tái)特性。epoll 是在 Linux 2.6 內(nèi)核中提出的,是 select 系統(tǒng)調(diào)用的 Linux 增強(qiáng)版本。

應(yīng)用進(jìn)程通過將一個(gè)或多個(gè) fd 傳遞給 select 系統(tǒng)調(diào)用,阻塞在 select 操作上,這樣 select 可以幫我們偵測多個(gè) fd 是否處于就緒狀態(tài)。select 是順序掃描 fd 是否就緒,而且支持的 fd 數(shù)量有限,因此它的使用受到了一些限制。epoll 系統(tǒng)調(diào)用不同于 select,它是基于事件驅(qū)動(dòng)方式代替順序掃描,因此性能更高,當(dāng)有 fd 就緒時(shí),立即回調(diào)函數(shù) rollback。

image.png

假如在 java 中發(fā)起一個(gè)多路復(fù)用 IO 的 read 讀操作的系統(tǒng)調(diào)用,流程如下:

  • 選擇器注冊(cè):首先,將需要 read 操作的目標(biāo) socket 網(wǎng)絡(luò)連接,提前注冊(cè)到 select或 epoll 選擇器中,Java 中對(duì)應(yīng)的選擇器類是 Selector 類。然后,才可以開啟整個(gè) IO 多路復(fù)用模型的輪詢流程。
  • 就緒狀態(tài)的輪詢:通過選擇器的查詢方法,查詢注冊(cè)過餓所有 socket 連接的就緒狀態(tài)。通過查詢的系統(tǒng)調(diào)用,內(nèi)核會(huì)返回一個(gè)就緒的 socket 列表。當(dāng)任何一個(gè)注冊(cè)過的 socket 中的數(shù)據(jù)準(zhǔn)備好了,內(nèi)核緩沖區(qū)有數(shù)據(jù)了,內(nèi)核就將該 socket 加入到就緒的列表中,整個(gè)查詢系統(tǒng)調(diào)用過程是阻塞的。
  • 獲取到了就緒狀態(tài)的 socket 列表后,發(fā)起 read 系統(tǒng)調(diào)用,線程阻塞,內(nèi)核開始復(fù)制數(shù)據(jù),將數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到用戶緩沖區(qū)。
  • 復(fù)制完成后,內(nèi)核放回結(jié)果,用戶線程才會(huì)解除阻塞狀態(tài)。

IO 多路復(fù)用模型的特點(diǎn) :IO 多路復(fù)用模型的 IO 涉及兩種系統(tǒng)調(diào)用,一種是 select/epoll(就緒查詢),一種是 IO 操作。IO 多路復(fù)用模型建立在操作系統(tǒng)的基礎(chǔ)設(shè)施之上,即操作系統(tǒng)的內(nèi)核必須能夠提供多路分離的系統(tǒng)調(diào)用 select/epoll。

和 NIO 模型相似,多路復(fù)用 IO 也需要輪詢。負(fù)責(zé) select/epoll 狀態(tài)查詢調(diào)用的線程,需要不斷地進(jìn)行 select/epoll 輪詢,查找出達(dá)到 IO 操作就緒的 socket 連接。

IO 多路復(fù)用模型與非阻塞 IO 模型是有密切關(guān)系的。對(duì)于注冊(cè)在選擇器上的每一個(gè)可以查詢的 socket 連接,一般都設(shè)置成為非阻塞模型。這一點(diǎn),對(duì)于用戶程序而言是無感知的。

IO 多路復(fù)用模型的優(yōu)點(diǎn):與一個(gè)線程維護(hù)一個(gè)連接的阻塞 IO 模式相比,使用 select/epoll 的最大優(yōu)勢在于,一個(gè)選擇器查詢線程可以同時(shí)處理成千上萬個(gè)連接( Connection )。系統(tǒng)不必創(chuàng)建大量的線程,也不必維護(hù)這些線程,從而大大減小了系統(tǒng)的開銷。

Java 語言的 NIO (NewIO)技術(shù),使用的就是 IO 多路復(fù)用模型。 在 Linux 系統(tǒng)上,使用的是 epoll 系統(tǒng)調(diào)用。

IO 多路復(fù)用模型的缺點(diǎn):本質(zhì)上, select/epoll 系統(tǒng)調(diào)用是阻塞式的,屬于同步 IO。都需要在讀寫事件就緒后,由系統(tǒng)調(diào)用本身負(fù)責(zé)進(jìn)行讀寫,也就是說這個(gè)讀寫過程是阻塞的。

異步 IO 模型

異步 IO 模型( Asynchronous IO, 簡稱為 AIO )。AIO 的基本流程是:用戶線程通過系統(tǒng)調(diào)用,向內(nèi)核注冊(cè)某個(gè) IO 操作。內(nèi)核在整個(gè) IO 操作(包括數(shù)據(jù)準(zhǔn)備、數(shù)據(jù)復(fù)制)完成后,通知用戶程序, 用戶執(zhí)行后續(xù)的業(yè)務(wù)操作。

在異步 IO 模型中,在整個(gè)內(nèi)核的數(shù)據(jù)處理過程中,包括內(nèi)核將數(shù)據(jù)從網(wǎng)絡(luò)物理設(shè)備(網(wǎng)卡)讀取到內(nèi)核緩沖區(qū)、將內(nèi)核緩沖區(qū)的數(shù)據(jù)復(fù)制到用戶緩沖區(qū),用戶程序都不需要阻塞。

image.png

發(fā)起一個(gè)異步 IO 的 read 讀操作系統(tǒng)調(diào)用,流程如下:

  • 當(dāng)用戶線程發(fā)起了 read 系統(tǒng)調(diào)用會(huì)立即返回,用戶線程不阻塞
  • 內(nèi)核就開始了 IO 的第一個(gè)階段:準(zhǔn)備數(shù)據(jù)。等到數(shù)據(jù)準(zhǔn)備好了,內(nèi)核就會(huì)將數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到用戶進(jìn)程緩沖區(qū)。
  • 內(nèi)核會(huì)給用戶發(fā)送一個(gè)信號(hào)(Signal),或者回調(diào)用戶線程注冊(cè)的回調(diào)接口,告訴用戶線程 read 操作完成了。

異步 IO 模型的特點(diǎn):在內(nèi)核等待數(shù)據(jù)和復(fù)制數(shù)據(jù)的兩個(gè)階段,用戶線程都不是阻塞的。用戶線程需要接收內(nèi)核的 IO 操作完成的事件,或者用戶線程需要注冊(cè)一個(gè) IO 操作完成的回調(diào)函數(shù)。正因?yàn)槿绱?,異?IO 有的時(shí)候也被稱為信號(hào)驅(qū)動(dòng) IO。

異步 IO 異步模型的缺點(diǎn): 應(yīng)用程序僅需要進(jìn)行事件的注冊(cè)與接收,其余的工作都留給了操作系統(tǒng),也就是說,需要底層內(nèi)核提供支持。理論上來說,異步 IO 是真正的異步輸入輸出,它的吞吐量高于 IO 多路復(fù)用模型的吞吐量。

就目前而言,Windows 系統(tǒng)下通過 IOCP 實(shí)現(xiàn)了真正的異步 IO。而在 Linux 系統(tǒng)下,異步 IO 模型在 2.6 版本才引入,目前并不完善,其底層實(shí)現(xiàn)仍使用 epoll ,與 IO 多路復(fù)用相同, 因此在性能上沒有明顯的優(yōu)勢。

大多數(shù)的高并發(fā)服務(wù)器端的程序,一般都是基于Linux 系統(tǒng)的。因而,目前這類高并發(fā)網(wǎng)絡(luò)應(yīng)用程序的開發(fā),大多采用 IO 多路復(fù)用模型。

Java 的 I/O 演進(jìn)

在 JDK 1.4 推出 Java NIO 之前,基于 Java 的所有 Socket 通信都采用了同步阻塞模式,這種一請(qǐng)求一應(yīng)答的通信模型簡化了的應(yīng)用開發(fā),但是在性能和可靠性方面卻存在著巨大的瓶頸。

正式由于 Java 傳統(tǒng) BIO 的拙劣表現(xiàn),才使得 Java 支持非阻塞 IO 的呼聲日漸高漲,終于在 JDK1.4 版本提供了新的 NIO 類庫,Java 終于支持非阻塞 IO 了。

從 JDK1.0 到 JDK1.3,Java 的 IO 類庫都非常原始,很多 UNIX 網(wǎng)絡(luò)編程中的概念或者接口都沒有體現(xiàn),例如 Pipe,Channel,Buffer 和 Selector 等。2002 年發(fā)布 JDK1.4時(shí), NIO 以 JSR-51 的身份正式隨 JDK 發(fā)布。新增了個(gè) java.nio 包,提供了很多進(jìn)行異步 IO 開發(fā)的 API 和類庫,主要類和接口如下:

  • 進(jìn)行異步 IO 操作的緩沖區(qū) ByteBuffer 等。
  • 進(jìn)行異步 IO 操作的管道 Pipe。
  • 進(jìn)行各種 IO 操作(異步或者同步)的 Channel,包括 ServerSocketChannel 和 SocketChannel
  • 各種字符集的編碼能力和解碼能力
  • 實(shí)現(xiàn)非阻塞 IO 操作的多路復(fù)用器 selector
  • 基于流行的 Perl 實(shí)現(xiàn)的正則表達(dá)式類庫
  • 文件通道 FileChannel

新的 NIO 類庫的提供,極大的促進(jìn)了基于 Java 的異步非阻塞編程的發(fā)展和應(yīng)用,但是依然有不完善的地方,特別是對(duì)文件系統(tǒng)的處理能力任顯不足,主要問題如下:

  • 沒有統(tǒng)一的文件屬性(例如讀寫權(quán)限)
  • API 能力比較弱,例如目錄的級(jí)聯(lián)創(chuàng)建和遞歸遍歷,往往需要自己實(shí)現(xiàn)
  • 底層存儲(chǔ)系統(tǒng)的一些高級(jí) API 無法使用
  • 所有文件操作都是同步阻塞調(diào)用,不支持異步文件讀寫操作

直到 2011 年 7 月 28 日,JDK1.7 正式發(fā)布。將原來的 NIO 類庫進(jìn)行了升級(jí),稱之為 NIO2.0。提供了如下方面的改進(jìn):

  • 提供能夠批量獲取文件屬性的 API,這些 API 具有平臺(tái)無關(guān)性,不與特性的文件系統(tǒng)相耦合。另外還提供了標(biāo)準(zhǔn)文件系統(tǒng)的 SPI,讓各個(gè)服務(wù)提供商擴(kuò)展實(shí)現(xiàn)。
  • 提供 AIO 功能,支持基于文件的異步 IO 操作和針對(duì)網(wǎng)絡(luò)套接字的異步操作。
  • 完成 JSR-51 定義的通道功能,包括對(duì)配置和多播數(shù)據(jù)報(bào)的支持等。
?著作權(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),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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