一、同步阻塞 IO(BIO)

當用戶線程調用了 read 系統調用,內核(kernel)就開始了 IO 的第一個階段:准備數據。很多時候,數據在一開始還沒有到達(比如,還沒有收到一個完整的Socket數據包),這個時候 kernel 就要等待足夠的數據到來。
當 kernel 一直等到數據准備好了,它就會將數據從 kernel 內核緩沖區,拷貝到用戶緩沖區(用戶內存),然后 kernel 返回結果。
從用戶線程 read 系統調用開始,用戶線程就進入阻塞狀態,一直到 kernel 返回結果后,用戶線程才解除 block 的狀態,重新運行起來。
BIO 的特點:在內核進行 IO 執行的兩個階段,用戶線程都被 block 了。
BIO 的優點:程序簡單,在阻塞等待數據期間,用戶線程掛起,用戶線程基本不會占用 CPU 資源。
BIO 的缺點:一般情況下,會為每個連接配套一條獨立的線程,或者說一條線程維護一個連接成功的 IO 流的讀寫。在並發量小的情況下,這個沒有什么問題。但是,當在高並發的場景下,需要大量的線程來維護大量的網絡連接,內存、線程切換開銷會非常巨大。因此,基本上,BIO 模型在高並發場景下是不可用的。
二、同步非阻塞 IO(NIO)

當用戶線程調用了 read 系統調用,立即返回,不阻塞線程,用戶線程需要不斷地發起 IO 系統調用輪詢數據是否准備好;
kernel 數據准備好后,用戶線程發起系統調用,用戶線程阻塞。內核開始復制數據,它就會將數據從 kernel 內核緩沖區,拷貝到用戶緩沖區(用戶內存),然后 kernel 返回結果。
用戶線程解除 block 狀態,重新運作起來。
NIO 的特點:應用程序的線程需要不斷的進行 I/O 系統調用,輪詢數據是否已經准備好,如果沒有准備好,繼續輪詢,直到完成系統調用為止。
NIO 的優點:每次發起的 IO 系統調用,在內核的等待數據過程中可以立即返回,用戶線程不會阻塞,實時性較好。
NIO 的缺點:需要不斷的重復發起 IO 系統調用,這種不斷的輪詢,將會不斷地詢問內核,這將占用大量的 CPU 時間,系統資源利用率較低。
NIO 模型在高並發場景下,也是不可用的。一般 web 服務器不直接使用這種 IO 模型,而是在其他 IO 模型中使用非阻塞 IO 這一特性。java 的實際開發中,也不會涉及這種 IO 模型。
三、IO 多路復用

當用戶線程調用了 read 系統調用,用戶線程不直接訪問 kernel ,而是進行 select/poll/epoll(多路復用器)系統調用。當然,這里有一個前提,需要將目標網絡連接,提前注冊到 select/poll/epoll 的可查詢 socket 列表中(這部分由 kernel 完成)。
用戶線程進行 select/poll/epoll 系統調用,線程阻塞,kernel 會查詢所有 select/poll/epoll 的可查詢 socket 列表,當任何一個 socket 中的數據准備好了,select/poll/epoll 就會返回。
用戶線程獲得了目標連接后,發起 read 系統調用,線程阻塞,內核開始復制數據,它就會將數據從 kernel 內核緩沖區,拷貝到用戶緩沖區(用戶內存),然后 kernel 返回結果。
用戶線程才解除 block 的狀態,用戶線程終於真正讀取到數據,繼續執行。
多路復用 IO 的特點:
- 建立在操作系統 kernel 內核能夠提供的多路復用系統調用 select/poll/epoll 基礎之上的,多路復用 IO 需要用到兩個系統調用(system call), 一個 select/poll/epoll 查詢調用,一個是 IO 的讀取調用。
- 和 NIO 模型類似,多路復用 IO 需要輪詢,需要有單獨的線程不斷的進行 select/poll/epoll 輪詢,查找出可以進行 IO 操作的連接。
- 多路復用 IO 模型與前面的 NIO 模型是有關系的,對於每一個可以查詢的 socket,一般都設置成為 non-blocking 模型。
多路復用 IO 的優點:用 select/poll/epoll 的優勢在於,它可以同時處理成千上萬個連接(connection)。與一條線程維護一個連接相比,I/O 多路復用不必創建線程,也不必維護這些線程,從而大大減小了系統的開銷。
多路復用 IO 的缺點:本質上,select/poll/epoll 系統調用,屬於同步 IO,也是阻塞 IO,需要在讀寫事件就緒后,自己負責進行讀寫,也就是說這個讀寫過程是阻塞的。
tips:
- "多路"指的是多個連接;"復用"指的是復用一個進程/線程進行監控。
- Java 的 NIO(New IO)技術,使用的就是 IO 多路復用模型。在 linux 系統上,使用的是 epoll 系統調用。
多路復用器
select 是一個主動模型,需要線程自己通過一個集合存放所有的 Socket,然后發生 I/O 變化的時候遍歷。在 select 模型下,操作系統不知道哪個線程應該響應哪個事件,而是由線程自己去操作系統看有沒有發生網絡 I/O 事件,然后再遍歷自己管理的所有 Socket,看看這些 Socket 有沒有發生變化。
poll 提供了更優質的編程接口,但是本質和 select 模型相同。因此千級並發以下的 I/O,你可以考慮 select 和 poll,但是如果出現更大的並發量,就需要用 epoll 模型。
select 支持的文件描述符數量默認是1024;poll 沒有最大連接數限制,因其基於鏈表存儲。
select 和 poll 的主動式的 I/O 多路復用,對負責 I/O 的線程壓力過大,因此通常會設計一個高效的中間數據結構作為 I/O 事件的觀察者,線程通過訂閱 I/O 事件被動響應,這就是響應式模型。在 Socket 編程中,最適合提供這種中間數據結構的就是操作系統的內核,事實上 epoll 模型也是在操作系統的內核中提供了紅黑樹結構。
epoll 模型在操作系統內核中提供了一個中間數據結構,這個中間數據結構會提供事件監聽注冊,以及快速判斷消息關聯到哪個線程的能力(紅黑樹實現,文件描述符構成了一棵紅黑樹,而紅黑樹的節點上掛着文件描述符對應的線程、線程監聽事件類型以及相應程序)。因此在高並發 I/O 下,可以考慮 epoll 模型,它的速度更快,開銷更小。
中間觀察者需要一個快速能插入(注冊過程)、查詢(通知過程)一個整數(Socket 的文件描述符)的數據結構。綜合來看,能夠解決這個問題的數據結構中,跳表和二叉搜索樹都是不錯的選擇。
tips: 一文搞懂select、poll和epoll區別
四、異步非阻塞IO(AIO)

當用戶線程調用了 read 系統調用,用戶線程立刻就能去做其它的事,用戶線程不阻塞。
內核(kernel)就開始了 IO 的第一個階段:准備數據,當 kernel 一直等到數據准備好了,它就會將數據從 kernel 內核緩沖區,拷貝到用戶緩沖區(用戶內存)。
然后,kernel 會給用戶線程發送一個信號(signal),或者回調用戶線程注冊的回調接口,告訴用戶線程 read 操作完成了。
用戶線程讀取用戶緩沖區的數據,完成后續的業務操作。
AIO 的特點:
- 在內核 kernel 的等待數據和復制數據的兩個階段,用戶線程都不是 block 的。
- 用戶線程需要接受 kernel 的 IO 操作完成的事件,或者說注冊 IO 操作完成的回調函數到操作系統的內核,因此,異步 IO 有的時候也叫做信號驅動 IO。
AIO 的缺點:需要完成事件的注冊與傳遞,這里邊需要底層操作系統提供大量的支持,去做大量的工作。
目前來說, Windows 系統下通過 IOCP 實現了真正的異步 I/O,但是,就目前的業界形式而言,Windows 系統,很少作為百萬級以上或者說高並發應用的服務器操作系統來使用。
而在 Linux 系統下,異步 IO 模型在2.6版本才引入,目前並不完善。所以,這也是在 Linux 下,實現高並發網絡編程時都是以 IO 復用模型模式為主。(https://github.com/netty/netty/issues/2515)
