0163 IO原理理解與IO模型

基本概念
Linux內核將所有外部設備都看做一個文件來操作。那麼我們對與外部設備的操作都可以看做是對文件進行操作。我們對一個文件的讀寫,都通過調用內核提供的系統調用;內核給我們返回一個file descriptor(fd,文件描述符),對一個socket的讀寫也會有相應的描述符,稱爲socketfd(socket描述符)。描述符就是一個數字(可以理解爲一個索引),指向內核中一個結構體(文件路徑,數據區,等一些屬性)。應用程序對文件的讀寫就通過對描述符的讀寫完成。
一個基本的IO,它會涉及到兩個系統對象,一個是調用這個IO的進程對象(用戶進程),另一個就是系統內核(kernel)。當一個read操作發生時,它會經歷兩個階段:

通過read系統調用向內核發起讀請求。
內核向硬件發送讀指令,並等待讀就緒。
DMA把將要讀取的數據複製到描述符所指向的內核緩存區中。
內核將數據從內核緩存區拷貝到用戶進程空間中。


IO操作的幾種方式
1)同步IO:當用戶發出IO請求操作之後,內核會去查看要讀取的數據是否就緒,如果數據沒有就緒,就一直等待。需要通過用戶線程或者內核不斷地去輪詢數據是否就緒,當數據就緒時,再將數據從內核拷貝到用戶空間。
2)異步IO:只有IO請求操作的發出是由用戶線程來進行的,IO操作的兩個階段都是由內核自動完成,然後發送通知告知用戶線程IO操作已經完成。也就是說在異步IO中,不會對用戶線程產生任何阻塞。
3)阻塞IO:當用戶線程發起一個IO請求操作(以讀請求操作爲例),內核查看要讀取的數據還沒就緒,當前線程被掛起,阻塞等待結果返回。
4)非阻塞IO:如果數據沒有就緒,則會返回一個標誌信息告知用戶線程當前要讀的數據沒有就緒。當前線程在拿到此次請求結果的過程中,可以做其它事情。

I/O 模型
Linux系統下根據阻塞與非阻塞,同步與異步實現了五種IO模型

同步阻塞I/O模型
最常見的I/O模型是阻塞I/O模型,缺省情形下,所有文件操作都是阻塞的。我們以套接口爲例來講解此模型。在進程空間中調用recvfrom,其系統調用直到數據報到達且被拷貝到應用進程的緩衝區中或者發生錯誤才返回,期間一直在等待。我們就說進程在從調用recvfrom開始到它返回的整段時間內是被阻塞的。


同步非阻塞I/O模型
進程把一個套接口設置成非阻塞是在通知內核:當所請求的I/O操作不能滿足要求時候,不把本進程投入睡眠,而是返回一個錯誤。也就是說當數據沒有到達時並不等待,而是以一個錯誤返回。


I/O複用模型
linux提供select/poll,進程通過將一個或多個fd傳遞給select或poll系統調用,阻塞在select;這樣select/poll可以幫我們偵測許多fd是否就緒。但是select/poll是順序掃描fd是否就緒,而且支持的fd數量有限。linux還提供了一個epoll系統調用,epoll是基於事件驅動方式,而不是順序掃描,當有fd就緒時,立即回調函數rollback;


信號驅動異步I/O模型
首先開啓套接口信號驅動I/O功能, 並通過系統調用sigaction安裝一個信號處理函數(此係統調用立即返回,進程繼續工作,它是非阻塞的)。當數據報準備好被讀時,就爲該進程生成一個SIGIO信號。隨即可以在信號處理程序中調用recvfrom來讀數據報,井通知主循環數據已準備好被處理中。也可以通知主循環,讓它來讀數據報。


異步I/O模型
告知內核啓動某個操作,並讓內核在整個操作完成後(包括將數據從內核拷貝到用戶自己的緩衝區)通知我們。這種模型與信號驅動模型的主要區別是:信號驅動I/O:由內核通知我們何時可以啓動一個I/O操作;異步I/O模型:由內核通知我們I/O操作何時完成。


總結
前四種都是同步IO,在內核數據copy到用戶空間時都是阻塞的。
最後一種是異步IO,通過API把IO操作交由操作系統處理,當前進程不關心具體IO的實現,通過回調函數,或者信號量通知當前進程直接對IO返回結果進行處理。

AIO,BIO,NIO
AIO異步非阻塞IO,AIO方式適用於連接數目多且連接比較長(重操作)的架構,比如相冊服務器,充分調用OS參與併發操作,編程比較複雜,JDK7開始支持。

NIO同步非阻塞IO,適用於連接數目多且連接比較短(輕操作)的架構,比如聊天服務器,併發侷限於應用中,編程比較複雜,JDK1.4開始支持。

BIO同步阻塞IO,適用於連接數目比較小且固定的架構,這種方式對服務器資源要求比較高,併發侷限於應用中,JDK1.4以前的唯一選擇,但程序直觀簡單易理解。

Java對BIO、NIO、AIO的支持:
Java BIO : 同步並阻塞,服務器實現模式爲一個連接一個線程,即客戶端有連接請求時服務器端就需要啓動一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷,當然可以通過線程池機制改善。
Java NIO : 同步非阻塞,服務器實現模式爲一個請求一個線程,即客戶端發送的連接請求都會註冊到多路複用器上,多路複用器輪詢到連接有I/O請求時才啓動一個線程進行處理。(底層是epoll)
Java AIO(NIO.2) : 異步非阻塞,服務器實現模式爲一個有效請求一個線程,客戶端的I/O請求都是由OS先完成了再通知服務器應用去啓動線程進行處理。

epoll,select/poll
都是IO複用,I/O多路複用就通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。但select,poll,epoll本質上都是同步I/O,因爲他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。
epoll的效率更高,優化了select的輪詢操作,通過callback事件響應方式。
epoll除了提供select/poll那種IO事件的水平觸發(Level Triggered)外,還提供了邊緣觸發(Edge Triggered),這就使得用戶空間程序有可能緩存IO狀態,減少epoll_wait/epoll_pwait的調用,提高應用程序效率。

select的幾大缺點:
(1)每次調用select,都需要把fd集合從用戶態拷貝到內核態,這個開銷在fd很多時會很大

(2)同時每次調用select都需要在內核遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大

(3)select支持的文件描述符數量太小了,默認是1024

poll實現
poll的實現和select非常相似,只是描述fd集合的方式不同,poll使用pollfd結構而不是select的fd_set結構.

epoll
  epoll既然是對select和poll的改進,就應該能避免上述的三個缺點。那epoll都是怎麼解決的呢?在此之前,我們先看一下epoll和select和poll的調用接口上的不同,select和poll都只提供了一個函數——select或者poll函數。而epoll提供了三個函數,epoll_create,epoll_ctl和epoll_wait,epoll_create是創建一個epoll句柄;epoll_ctl是註冊要監聽的事件類型;epoll_wait則是等待事件的產生。

  對於第一個缺點,epoll的解決方案在epoll_ctl函數中。每次註冊新的事件到epoll句柄中時(在epoll_ctl中指定EPOLL_CTL_ADD),會把所有的fd拷貝進內核,而不是在epoll_wait的時候重複拷貝。epoll保證了每個fd在整個過程中只會拷貝一次。

  對於第二個缺點,epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對應的設備等待隊列中,而只在epoll_ctl時把current掛一遍(這一遍必不可少)併爲每個fd指定一個回調函數,當設備就緒,喚醒等待隊列上的等待者時,就會調用這個回調函數,而這個回調函數會把就緒的fd加入一個就緒鏈表)。epoll_wait的工作實際上就是在這個就緒鏈表中查看有沒有就緒的fd(利用schedule_timeout()實現睡一會,判斷一會的效果,和select實現中的第7步是類似的)。

  對於第三個缺點,epoll沒有這個限制,它所支持的FD上限是最大可以打開文件的數目,這個數字一般遠大於2048,舉個例子,在1GB內存的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統內存關係很大。

使用mmap加速內核與用戶空間的消息傳遞。無論是select,poll還是epoll都需要內核把FD消息通知給用戶空間,如何避免不必要的內存拷貝就很重要,在這點上,epoll是通過內核於用戶空間mmap同一塊內存實現的。

總結
select,poll實現需要自己不斷輪詢所有fd集合,直到設備就緒,期間可能要睡眠和喚醒多次交替。而epoll其實也需要調用epoll_wait不斷輪詢就緒鏈表,期間也可能多次睡眠和喚醒交替,但是它是設備就緒時,調用回調函數,把就緒fd放入就緒鏈表中,並喚醒在epoll_wait中進入睡眠的進程。雖然都要睡眠和交替,但是select和poll在“醒着”的時候要遍歷整個fd集合,而epoll在“醒着”的時候只要判斷一下就緒鏈表是否爲空就行了,這節省了大量的CPU時間。這就是回調機制帶來的性能提升。

select,poll每次調用都要把fd集合從用戶態往內核態拷貝一次,並且要把current往設備等待隊列中掛一次,而epoll只要一次拷貝,而且把current往等待隊列上掛也只掛一次(在epoll_wait的開始,注意這裏的等待隊列並不是設備等待隊列,只是一個epoll內部定義的等待隊列)。這也能節省不少的開銷。
————————————————
版權聲明:本文爲CSDN博主「lhrimperial」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/u013857458/article/details/82419293

發佈了21 篇原創文章 · 獲贊 8 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章