目錄html
瘋狂創客圈 Java 分佈式聊天室【 億級流量】實戰系列之 -21【 博客園 總入口 】java
你們好,我是 高併發的實戰社羣【瘋狂創客圈】尼恩。Java NIO 、 Reactor模式等基礎原理性的知識,很是重要,不管開發、仍是面試。
不少的小夥伴,被java IO 模型中,搞得有點兒暈,一下子是4種模型,一下子又變成了5種模型。
不少的小夥伴,也被nio這個名詞搞暈了,一下子java 的nio 不叫 非阻塞io,一下子java nio 又是非阻塞io,究竟是啥呢?
不少的小夥伴,被異步和非阻塞搞暈了。都非阻塞了,難道不是異步的嗎?
這這,好難呀。
此文,從底層入手,給各位小夥伴,起底一下,java的四大io模型。須要面試的,或者沒有弄清楚的小夥伴,完全的有福了。linux
順便說明下:
本文的內容只是一個初稿、初稿,本文的知識,在《Netty Zookeeper Redis 高併發實戰》一書時,進行大篇幅的完善和更新,而且進行的源碼的升級。 博客和書不同,書的內容更加系統化、全面化,更加層層升入、井井有條、更屢次的錯誤排查,請你們以書的內容爲準。
本文的最終內容, 具體請參考瘋狂創客圈 傾力編著,機械工業出版社出版的 《Netty Zookeeper Redis 高併發實戰》一書 。
面試
不管是Socket的讀寫仍是文件的讀寫,在Java層面的應用開發或者是linux系統底層開發,都屬於輸入input和輸出output的處理,簡稱爲IO讀寫。在原理上和處理流程上,都是一致的。區別在於參數的不一樣。編程
用戶程序進行IO的讀寫,基本上會用到read&write兩大系統調用。可能不一樣操做系統,名稱不徹底同樣,可是功能是同樣的。設計模式
先強調一個基礎知識:read系統調用,並非把數據直接從物理設備,讀數據到內存。write系統調用,也不是直接把數據,寫入到物理設備。緩存
read系統調用,是把數據從內核緩衝區複製到進程緩衝區;而write系統調用,是把數據從進程緩衝區複製到內核緩衝區。這個兩個系統調用,都不負責數據在內核緩衝區和磁盤之間的交換。底層的讀寫交換,是由操做系統kernel內核完成的。服務器
緩衝區的目的,是爲了減小頻繁的系統IO調用。你們都知道,系統調用須要保存以前的進程數據和狀態等信息,而結束調用以後回來還須要恢復以前的信息,爲了減小這種損耗時間、也損耗性能的系統調用,因而出現了緩衝區。網絡
有了緩衝區,操做系統使用read函數把數據從內核緩衝區複製到進程緩衝區,write把數據從進程緩衝區複製到內核緩衝區中。等待緩衝區達到必定數量的時候,再進行IO的調用,提高性能。至於何時讀取和存儲則由內核來決定,用戶程序不須要關心。併發
在linux系統中,系統內核也有個緩衝區叫作內核緩衝區。每一個進程有本身獨立的緩衝區,叫作進程緩衝區。
因此,用戶程序的IO讀寫程序,大多數狀況下,並無進行實際的IO操做,而是在讀寫本身的進程緩衝區。
用戶程序進行IO的讀寫,基本上會用到系統調用read&write,read把數據從內核緩衝區複製到進程緩衝區,write把數據從進程緩衝區複製到內核緩衝區,它們不等價於數據在內核緩衝區和磁盤之間的交換。
首先看看一個典型Java 服務端處理網絡請求的典型過程:
(1)客戶端請求
Linux經過網卡,讀取客戶斷的請求數據,將數據讀取到內核緩衝區。
(2)獲取請求數據
服務器從內核緩衝區讀取數據到Java進程緩衝區。
(1)服務器端業務處理
Java服務端在本身的用戶空間中,處理客戶端的請求。
(2)服務器端返回數據
Java服務端已構建好的響應,從用戶緩衝區寫入系統緩衝區。
(3)發送給客戶端
Linux內核經過網絡 I/O ,將內核緩衝區中的數據,寫入網卡,網卡經過底層的通信協議,會將數據發送給目標客戶端。
服務器端編程常常須要構造高性能的IO模型,常見的IO模型有四種:
(1)同步阻塞IO(Blocking IO)
首先,解釋一下這裏的阻塞與非阻塞:
阻塞IO,指的是須要內核IO操做完全完成後,才返回到用戶空間,執行用戶的操做。阻塞指的是用戶空間程序的執行狀態,用戶空間程序需等到IO操做完全完成。傳統的IO模型都是同步阻塞IO。在java中,默認建立的socket都是阻塞的。
其次,解釋一下同步與異步:
同步IO,是一種用戶空間與內核空間的調用發起方式。同步IO是指用戶空間線程是主動發起IO請求的一方,內核空間是被動接受方。異步IO則反過來,是指內核kernel是主動發起IO請求的一方,用戶線程是被動接受方。
(4)同步非阻塞IO(Non-blocking IO)
非阻塞IO,指的是用戶程序不須要等待內核IO操做完成後,內核當即返回給用戶一個狀態值,用戶空間無需等到內核的IO操做完全完成,能夠當即返回用戶空間,執行用戶的操做,處於非阻塞的狀態。
簡單的說:阻塞是指用戶空間(調用線程)一直在等待,並且別的事情什麼都不作;非阻塞是指用戶空間(調用線程)拿到狀態就返回,IO操做能夠幹就幹,不能夠幹,就去幹的事情。
非阻塞IO要求socket被設置爲NONBLOCK。
強調一下,這裏所說的NIO(同步非阻塞IO)模型,並不是Java的NIO(New IO)庫。
(3)IO多路複用(IO Multiplexing)
即經典的Reactor設計模式,有時也稱爲異步阻塞IO,Java中的Selector和Linux中的epoll都是這種模型。
(5)異步IO(Asynchronous IO)
異步IO,指的是用戶空間與內核空間的調用方式反過來。用戶空間線程是變成被動接受的,內核空間是主動調用者。
這一點,有點相似於Java中比較典型的模式是回調模式,用戶空間線程向內核空間註冊各類IO事件的回調函數,由內核去主動調用。
在linux中的Java進程中,默認狀況下全部的socket都是blocking IO。在阻塞式 I/O 模型中,應用程序在從IO系統調用開始,一直到到系統調用返回,這段時間是阻塞的。返回成功後,應用進程開始處理用戶空間的緩存數據。
舉個栗子,發起一個blocking socket的read讀操做系統調用,流程大概是這樣:
(1)當用戶線程調用了read系統調用,內核(kernel)就開始了IO的第一個階段:準備數據。不少時候,數據在一開始尚未到達(好比,尚未收到一個完整的Socket數據包),這個時候kernel就要等待足夠的數據到來。
(2)當kernel一直等到數據準備好了,它就會將數據從kernel內核緩衝區,拷貝到用戶緩衝區(用戶內存),而後kernel返回結果。
(3)從開始IO讀的read系統調用開始,用戶線程就進入阻塞狀態。一直到kernel返回結果後,用戶線程才解除block的狀態,從新運行起來。
因此,blocking IO的特色就是在內核進行IO執行的兩個階段,用戶線程都被block了。
BIO的優勢:
程序簡單,在阻塞等待數據期間,用戶線程掛起。用戶線程基本不會佔用 CPU 資源。
BIO的缺點:
通常狀況下,會爲每一個鏈接配套一條獨立的線程,或者說一條線程維護一個鏈接成功的IO流的讀寫。在併發量小的狀況下,這個沒有什麼問題。可是,當在高併發的場景下,須要大量的線程來維護大量的網絡鏈接,內存、線程切換開銷會很是巨大。所以,基本上,BIO模型在高併發場景下是不可用的。
在linux系統下,能夠經過設置socket使其變爲non-blocking。NIO 模型中應用程序在一旦開始IO系統調用,會出現如下兩種狀況:
(1)在內核緩衝區沒有數據的狀況下,系統調用會當即返回,返回一個調用失敗的信息。
(2)在內核緩衝區有數據的狀況下,是阻塞的,直到數據從內核緩衝複製到用戶進程緩衝。複製完成後,系統調用返回成功,應用進程開始處理用戶空間的緩存數據。
舉個栗子。發起一個non-blocking socket的read讀操做系統調用,流程是這個樣子:
(1)在內核數據沒有準備好的階段,用戶線程發起IO請求時,當即返回。用戶線程須要不斷地發起IO系統調用。
(2)內核數據到達後,用戶線程發起系統調用,用戶線程阻塞。內核開始複製數據。它就會將數據從kernel內核緩衝區,拷貝到用戶緩衝區(用戶內存),而後kernel返回結果。
(3)用戶線程才解除block的狀態,從新運行起來。通過屢次的嘗試,用戶線程終於真正讀取到數據,繼續執行。
NIO的特色:
應用程序的線程須要不斷的進行 I/O 系統調用,輪詢數據是否已經準備好,若是沒有準備好,繼續輪詢,直到完成系統調用爲止。
NIO的優勢:每次發起的 IO 系統調用,在內核的等待數據過程當中能夠當即返回。用戶線程不會阻塞,實時性較好。
NIO的缺點:須要不斷的重複發起IO系統調用,這種不斷的輪詢,將會不斷地詢問內核,這將佔用大量的 CPU 時間,系統資源利用率較低。
總之,NIO模型在高併發場景下,也是不可用的。通常 Web 服務器不使用這種 IO 模型。通常不多直接使用這種模型,而是在其餘IO模型中使用非阻塞IO這一特性。java的實際開發中,也不會涉及這種IO模型。
再次說明,Java NIO(New IO) 不是IO模型中的NIO模型,而是另外的一種模型,叫作IO多路複用模型( IO multiplexing )。
如何避免同步非阻塞NIO模型中輪詢等待的問題呢?這就是IO多路複用模型。
IO多路複用模型,就是經過一種新的系統調用,一個進程能夠監視多個文件描述符,一旦某個描述符就緒(通常是內核緩衝區可讀/可寫),內核kernel可以通知程序進行相應的IO系統調用。
目前支持IO多路複用的系統調用,有 select,epoll等等。select系統調用,是目前幾乎在全部的操做系統上都有支持,具備良好跨平臺特性。epoll是在linux 2.6內核中提出的,是select系統調用的linux加強版本。
IO多路複用模型的基本原理就是select/epoll系統調用,單個線程不斷的輪詢select/epoll系統調用所負責的成百上千的socket鏈接,當某個或者某些socket網絡鏈接有數據到達了,就返回這些能夠讀寫的鏈接。所以,好處也就顯而易見了——經過一次select/epoll系統調用,就查詢到到能夠讀寫的一個甚至是成百上千的網絡鏈接。
舉個栗子。發起一個多路複用IO的的read讀操做系統調用,流程是這個樣子:
在這種模式中,首先不是進行read系統調動,而是進行select/epoll系統調用。固然,這裏有一個前提,須要將目標網絡鏈接,提早註冊到select/epoll的可查詢socket列表中。而後,才能夠開啓整個的IO多路複用模型的讀流程。
(1)進行select/epoll系統調用,查詢能夠讀的鏈接。kernel會查詢全部select的可查詢socket列表,當任何一個socket中的數據準備好了,select就會返回。
當用戶進程調用了select,那麼整個線程會被block(阻塞掉)。
(2)用戶線程得到了目標鏈接後,發起read系統調用,用戶線程阻塞。內核開始複製數據。它就會將數據從kernel內核緩衝區,拷貝到用戶緩衝區(用戶內存),而後kernel返回結果。
(3)用戶線程才解除block的狀態,用戶線程終於真正讀取到數據,繼續執行。
多路複用IO的特色:
IO多路複用模型,創建在操做系統kernel內核可以提供的多路分離系統調用select/epoll基礎之上的。多路複用IO須要用到兩個系統調用(system call), 一個select/epoll查詢調用,一個是IO的讀取調用。
和NIO模型類似,多路複用IO須要輪詢。負責select/epoll查詢調用的線程,須要不斷的進行select/epoll輪詢,查找出能夠進行IO操做的鏈接。
另外,多路複用IO模型與前面的NIO模型,是有關係的。對於每個能夠查詢的socket,通常都設置成爲non-blocking模型。只是這一點,對於用戶程序是透明的(不感知)。
多路複用IO的優勢:
用select/epoll的優點在於,它能夠同時處理成千上萬個鏈接(connection)。與一條線程維護一個鏈接相比,I/O多路複用技術的最大優點是:系統沒必要建立線程,也沒必要維護這些線程,從而大大減少了系統的開銷。
Java的NIO(new IO)技術,使用的就是IO多路複用模型。在linux系統上,使用的是epoll系統調用。
多路複用IO的缺點:
本質上,select/epoll系統調用,屬於同步IO,也是阻塞IO。都須要在讀寫事件就緒後,本身負責進行讀寫,也就是說這個讀寫過程是阻塞的。
如何充分的解除線程的阻塞呢?那就是異步IO模型。
如何進一步提高效率,解除最後一點阻塞呢?這就是異步IO模型,全稱asynchronous I/O,簡稱爲AIO。
AIO的基本流程是:用戶線程經過系統調用,告知kernel內核啓動某個IO操做,用戶線程返回。kernel內核在整個IO操做(包括數據準備、數據複製)完成後,通知用戶程序,用戶執行後續的業務操做。
kernel的數據準備是將數據從網絡物理設備(網卡)讀取到內核緩衝區;kernel的數據複製是將數據從內核緩衝區拷貝到用戶程序空間的緩衝區。
(1)當用戶線程調用了read系統調用,馬上就能夠開始去作其它的事,用戶線程不阻塞。
(2)內核(kernel)就開始了IO的第一個階段:準備數據。當kernel一直等到數據準備好了,它就會將數據從kernel內核緩衝區,拷貝到用戶緩衝區(用戶內存)。
(3)kernel會給用戶線程發送一個信號(signal),或者回調用戶線程註冊的回調接口,告訴用戶線程read操做完成了。
(4)用戶線程讀取用戶緩衝區的數據,完成後續的業務操做。
異步IO模型的特色:
在內核kernel的等待數據和複製數據的兩個階段,用戶線程都不是block(阻塞)的。用戶線程須要接受kernel的IO操做完成的事件,或者說註冊IO操做完成的回調函數,到操做系統的內核。因此說,異步IO有的時候,也叫作信號驅動 IO 。
異步IO模型缺點:
須要完成事件的註冊與傳遞,這裏邊須要底層操做系統提供大量的支持,去作大量的工做。
目前來講, Windows 系統下經過 IOCP 實現了真正的異步 I/O。可是,就目前的業界形式來講,Windows 系統,不多做爲百萬級以上或者說高併發應用的服務器操做系統來使用。
而在 Linux 系統下,異步IO模型在2.6版本才引入,目前並不完善。因此,這也是在 Linux 下,實現高併發網絡編程時都是以 IO 複用模型模式爲主。
四種IO模型,理論上越日後,阻塞越少,效率也是最優。在這四種 I/O 模型中,前三種屬於同步 I/O,由於其中真正的 I/O 操做將阻塞線程。只有最後一種,纔是真正的異步 I/O 模型,惋惜目前Linux 操做系統尚欠完善。
本篇做爲[瘋狂創客圈]的百萬級流量高併發實戰的一篇基礎篇的文章,後續還有分佈式協調,分佈式網關等系列文章出來。
Java (Netty) 聊天程序【 億級流量】實戰 開源項目實戰
瘋狂創客圈 【 博客園 總入口 】