Java高並發教程:高並發IO的底層原理
IO讀寫的基礎原理
程序進行IO讀寫依賴於操作系統底層的IO讀寫,主要為read、write兩大系統調用。在不同的操作系統中,IO讀寫的系統調用的名稱可能不完全一樣,但是基本功能是一樣的。
首先我們必須要明白的的是,read系統調用,並不是直接從物理設備把數據讀取到內存中;write系統調用,也不是直接把數據寫入到物理設備。上層應用無論時調用操作系統的read,還是write都會涉及緩存區。即,調用read,是把數據從內核緩存區復制到進程緩存區,wirte是把數據從進程緩存區復制到內核緩存區。
所以,程序的IO操作實際上是緩存的復制,並不是實際物理設備的讀寫,這項底層的讀寫交換,是由操作系統內核(Kernel)來完成的。
操作系統基礎知識:
1.操作系統與外部設備之間:主要通過中斷機制來實現。
2.操作系統與上層應用程序:主要通過異常與系統調用兩個機制來實現。
內核緩存區與進程緩存區
緩存的目的是減少頻繁地與設備之間的物理交換,因為外部設備的直接讀寫,都需要操作系統中斷,而中斷耗時耗力,所以緩存很有必要。
- 在Linux系統中,操作系統內核只有一個內核緩沖區。
- 每個用戶程序(進程),有自己獨立的緩存區,叫做進程緩存區。
用戶程序的IO讀寫程序,在大多數情況下,並沒有進行實際的IO操作,而是在進程緩沖區和內核緩沖區之間直接進行數據的交換。
系統調用read&write的流程
到這里,還是需要舉個例子:
比如在Java服務器端,完成一次socket請求和響應,完整的流程如下:·
- 客戶端請求:Linux通過網卡讀取客戶端的請求數據,將數據讀取到內核緩沖區。·
- 獲取請求數據:Java服務器通過read系統調用,從Linux內核緩沖區讀取數據,再送入Java進程緩沖區。·
- 服務器端業務處理:Java服務器在自己的用戶空間中處理客戶端的請求。
- 服務器端返回數據:Java服務器完成處理后,構建好的響應數據,將這些數據從用戶緩沖區寫入內核緩沖區。這里用到的是write系統調用。·
- 發送給客戶端:Linux內核通過網絡IO,將內核緩沖區中的數據寫入網卡,網卡通過底層的通信協議,會將數據發送給目標客戶端。
四種主要的IO模型
1.同步阻塞IO(Blocking IO)
阻塞IO指的是需要內核IO操作徹底完成后,才返回到用戶空間執行用戶的操作。同步IO指的是用戶空間的線程是主動發起IO請求的一方,內核空間是被動接收方。
總之,阻塞IO的特點是:在內核進行IO執行的兩個階段,用戶線程都被阻塞了。
阻塞IO的優點是:應用的程序開發非常簡單;在阻塞等待數據期間,用戶線程掛起。在阻塞期間,用戶線程基本不會占用CPU資源。
阻塞IO的缺點是:一般情況下,會為每個連接配備一個獨立的線程;反過來說,就是一個線程維護一個連接的IO操作。在並發量小的情況下,這樣做沒有什么問題。但是,當在高並發的應用場景下,需要大量的線程來維護大量的網絡連接,內存、線程切換開銷會非常巨大。因此,基本上阻塞IO模型在高並發應用場景下是不可用的。
2.同步非阻塞IO(Non-blocking IO)
非阻塞IO,指的是用戶空間的程序不需要等待內核IO操作徹底完成,可以立即返回用戶空間執行用戶的操作,即處於非阻塞的狀態,與此同時內核會立即返回給用戶一個狀態值。
簡單來說:阻塞是指用戶空間(調用線程)一直在等待,而不能干別的事情;非阻塞是指用戶空間(調用線程)拿到內核返回的狀態值就返回自己的空間,IO操作可以干就干,不可以干,就去干別的事情。
在NIO模型中,應用程序一旦開始IO系統調用,會出現以下兩種情況:
- 在內核緩沖區中沒有數據的情況下,系統調用會立即返回,返回一個調用失敗的信息。
- 在內核緩沖區中有數據的情況下,是阻塞的,直到數據從內核緩沖復制到用戶進程緩沖。復制完成后,系統調用返回成功,應用進程開始處理用戶空間的緩存數據。
同步非阻塞IO的特點:應用程序的線程需要不斷地進行IO系統調用,輪詢數據是否已經准備好,如果沒有准備好,就繼續輪詢,直到完成IO系統調用為止。
同步非阻塞IO的優點:每次發起的IO系統調用,在內核等待數據過程中可以立即返回。用戶線程不會阻塞,實時性較好。
同步非阻塞IO的缺點:不斷地輪詢內核,這將占用大量的CPU時間,效率低下。
總體來說,在高並發應用場景下,同步非阻塞IO也是不可用的。一般Web服務器不使用這種IO模型。這種IO模型一般很少直接使用,而是在其他IO模型中使用非阻塞IO這一特性。在Java的實際開發中,也不會涉及這種IO模型。
注意:這里所說的NIO(同步非阻塞IO)模型,並非Java的NIO(New IO)庫。
3.IO多路復用(IO Multiplexing)
IO多路復用可以解決非阻塞IO模型中存在的輪詢等待問題。在IO多路復用模型中,引入一種新的系統調用(Linux中,為select/epoll),查詢IO的就緒狀態。通過該調用,一個進程可以監視多個文件描述符,一旦某個描述符就緒(一般是內核緩存區可讀/可寫),內核就能夠將就緒的裝填返回給應用程序。隨后,應用程序根據就緒的狀態,進行相應IO系統調用。
在IO多路復用模型中通過select/epoll系統調用,單個應用程序的線程,可以不斷地輪詢成百上千的socket連接,當某個或者某些socket網絡連接有IO就緒的狀態,就返回對應的可以執行的讀寫操作。
- 選擇器注冊。在這種模式中,首先,將需要read操作的目標socket網絡連接,提前注冊到select/epoll選擇器中,Java中對應的選擇器類是Selector類。然后,才可以開啟整個IO多路復用模型的輪詢流程。
- 就緒狀態的輪詢。通過選擇器的查詢方法,查詢注冊過的所有socket連接的就緒狀態。通過查詢的系統調用,內核會返回一個就緒的socket列表。當任何一個注冊過的socket中的數據准備好了,內核緩沖區有數據(就緒)了,內核就將該socket加入到就緒的列表中。
- 用戶線程獲得了就緒狀態的列表后,根據其中的socket連接,發起read系統調用,用戶線程阻塞。內核開始復制數據,將數據從內核緩沖區復制到用戶緩沖區。
- 復制完成后,內核返回結果,用戶線程才會解除阻塞的狀態,用戶線程讀取到了數據,繼續執行。
注意:當用戶進程調用了select查詢方法,那么整個線程會被阻塞掉。
IO多路復用模型的IO涉及兩種系統調用(System Call),另一種是select/epoll(就緒查詢),一種是IO操作。IO多路復用模型建立在操作系統的基礎設施之上,即操作系統的內核必須能夠提供多路分離的系統調用select/epoll。
IO多路復用模型的優點:與一個線程維護一個連接的阻塞IO模式相比,使用select/epoll的最大優勢在於,一個選擇器查詢線程可以同時處理成千上萬個連接(Connection)。系統不必創建大量的線程,也不必維護這些線程,從而大大減小了系統的開銷。Java語言的NIO(New IO)技術,使用的就是IO多路復用模型。在Linux系統上,使用的是epoll系統調用。
IO多路復用模型的缺點:本質上,select/epoll系統調用是阻塞式的,屬於同步IO。都需要在讀寫事件就緒后,由系統調用本身負責進行讀寫,也就是說這個讀寫過程是阻塞的。
4.異步IO(Asynchronous IO)
異步IO模型,簡稱AIO,用戶線程通過系統調用,先向內核注冊某個IO操作。內核在整個IO操作完成后,通知用戶程序,用戶執行后續的業務操作。
異步IO模型的流程,如下:
- 當用戶線程發起了read系統調用,立刻就可以開始做其他事情,用戶線程不阻塞。
- 內核開始IO的第一階段:准備數據。等到數據准備好了,內核就會將數據從內核緩存區復制到用戶緩存區。
- 內核會給用戶線程發送一個信號,或者回調用戶線程注冊的回調接口,告訴用戶線程read操作完成了。
- 用戶線程讀取用戶緩存區的數據,完成后續的業務操作。
理論上來說,異步IO是真正的異步輸入輸出,它的吞吐量高於IO多路復用模型的吞吐量。但是它依賴操作系統的實現,比如Linux的異步IO模型目前並不完善,在性能上沒有優勢。所以,大名鼎鼎的Netty框架,使用的就是IO多路復用模型,而不是異步IO模型。
參考資料
- 《Netty、Redis、Zookeeper高並發實戰》