-
- 網絡IO模型淺析:編寫一個web服務器應用程式(httpd、Nginx),幾種常用的網絡IO模型(IO模型為內核提供,開發的web應用程式調用這些模型來達到網絡IO的目的,httpd調用實現的是IO多路復用):
-
-
數據的一次磁盤IO,都會由兩階段組成:
- 第一階段:數據從磁盤復制到內核內存,即上圖第3步
- 第二階段:數據從內核內存復制到進程內存,即上圖第4步
-
同步異步,阻塞非阻塞區別聯系
-
實際上同步與異步是針對應用程式與內核的交互而言的,關注的是消息通知機制
-
同步:調用者進程發出調用后,調用者進程就處于等待狀態,被調用者進程不會返回任何消息,但一旦返回結果,就是最終結果。
- 同步過程中進程觸發IO操作并等待(也就是我們說的阻塞)或者輪詢的去查看IO操作(也就是我們說的非阻塞)是否完成。 同步有阻塞和非阻塞之分
- 異步:調用者進程發出調用后,被調用進程立即返回消息,但返回的不是最終結果,即告訴調用者進程不必等待,調用者進程可以處理其他事情,被調用者進程處理完后,則將最終結果通過狀態、通知機制等來通知調用者進程。異步一定是非阻塞的
-
同步:調用者進程發出調用后,調用者進程就處于等待狀態,被調用者進程不會返回任何消息,但一旦返回結果,就是最終結果。
-
阻塞與非阻塞更關注的是調用者進程等待被調用者進程返回調用結果時的狀態(即有無返回結果)
- 阻塞:調用結果返回之前,調用者進程會被掛起,調用者進程只有在得到返回結果之后才能繼續后續的工作
- 非阻塞:調用者進程在結果返回之前,不會被掛起,即返回臨時結果給調用者進程,讓它繼續處理后續的操作(隔斷時間再來詢問之前的操作是否完成。這樣的非阻塞忙輪詢,也算是非阻塞的一種,或者是直到被調用者進程處理完后,通知調用者進程,調用者進程才來拿結果,這樣的完全非阻塞)
-
實際上同步與異步是針對應用程式與內核的交互而言的,關注的是消息通知機制
-
網絡IO模型類型:(一個進程只能處理一個套接字的I/O事件)服務器端用戶空間線程接到客戶端發起的網絡IO后,才會向內核空間發起網絡IO,所以web服務器的進程需要處理這兩路網絡IO
-
阻塞I/O模型(BIO,Blocked IO):當用戶空間進程在連接套接字中的緩沖區中接收到客戶端的請求URL時,該程式(如httpd)在它所在的用戶空間內存中分析后,即向內核發起read(socket, buffer)系統調用,這是用戶空間進程被阻塞,內核開始準備數據,即將磁盤數據拷貝到內核socketbuffer中(此時用戶空間是阻塞的),之后用戶空間進程需要調用read讀取socket中buffer的數據,內核read()函數會將數據返回(拷貝)給用戶進程的內存空間(這個過程也是由內核來完成的拷貝,此時用戶空間進程也是阻塞的)
- 內核空間的buffer和cache可以用free來查看
- 整個過程都是阻塞的,所以這樣的IO模型效率很低
-
阻塞I/O模型(BIO,Blocked IO):當用戶空間進程在連接套接字中的緩沖區中接收到客戶端的請求URL時,該程式(如httpd)在它所在的用戶空間內存中分析后,即向內核發起read(socket, buffer)系統調用,這是用戶空間進程被阻塞,內核開始準備數據,即將磁盤數據拷貝到內核socketbuffer中(此時用戶空間是阻塞的),之后用戶空間進程需要調用read讀取socket中buffer的數據,內核read()函數會將數據返回(拷貝)給用戶進程的內存空間(這個過程也是由內核來完成的拷貝,此時用戶空間進程也是阻塞的)
-
read(socket, buffer);
process(buffer);
- 在《Socket套接字.doc》中已經介紹了流和套接字的關系,我們知道內核將數據從磁盤拷貝到內核空間的過程中,因為數據都是流式的,所以每一次的磁盤IO的過程都是要通過內核來創建一個socket文件來表示這個流數據的,并返回一個fd給用戶進程,故而在用戶進程read(socket,buffer)時,通過fd就能知道讀取的是哪個流數據
-
- 非阻塞I/O模型(NIO,Non-blocked IO):將 連接套接字 設置為NONBLOCK。當內核數據沒有準備好時,內核立即返回EWOULDBLOCK錯誤給用戶空間進程,此時用戶空間進程便可以處理其他操作,但會不斷發起系統調用read,一旦內核緩沖區的數據已經存在(即連接套接字中被寫入數據),在用戶進程發起最后一次read調用后,數據才復制到用戶空間中處理封裝應用層首部等,這樣就是叫非阻塞忙輪詢(polling)
-
while(read(socket, buffer) != SUCCESS)
process(buffer);
- 用戶需要不斷地調用read,嘗試讀取socket中的數據,直到讀取成功后,才繼續處理接收的數據。整個IO請求的過程中,雖然用戶線程每次發起IO請求后可以立即返回,但是為了等到數據,仍需要不斷地輪詢、重復請求,消耗了大量的CPU的資源
-
-
I/O復用模型(阻塞IO復用):httpd中的prefork和worker都是用的此模型
-
多路復用IO模型簡介:
- 當web服務器進程向內核空間調用系統調用時,便處于阻塞狀態被掛起,如果服務器端的磁盤讀取很慢,客戶端由于嫌響應速度過慢而取消這次操作(如ctrl+c),但是服務器進程處于掛起狀態,便無法接收到客戶端發出的取消信息的,所以是無法取消的。為了避免這種情況,后來在內核開發了一種多路IO復用的程式,便可以讓用戶及時取消操作
- 如果需要使用這種IO復用,用戶空間進程不是調用read發起的IO請求交給內核,而是直接調用select或poll系統調用,用戶空間進程會被阻塞等待select系統調用返回(用戶空間進程阻塞在select上不同于阻塞在內核的磁盤IO上,它還可以接收其他信號進來比如網絡IO請求,這樣就可以讓一個進程同時處理多個IO請求,select代理此時并不阻塞,可以接收其他請求),它會代替用戶空間進程給內核發起IO請求,讓內核準備數據,同時輪詢所有socket是否有數據。當數據到達時,其中一個socket被激活,select或poll函數返回,用戶空間進程才正式調用read讀取socket中的數據,正因為阻塞I/O只能阻塞一個I/O操作,而I/O復用模型能夠阻塞多個I/O操作,所以才叫做多路復用。
- select代理在接收用戶空間進程的請求時,請求的數量是有限制的,select程式要求不能超過1024個(這個數量是內核源碼的合理設定,如果想要超過1024可以修改源碼,不過超過1024未必有很好的性能),httpd程式的prefork模型中,只能承載1024個請求并發,就是因為prefork就是基于調用select來實現的,prefork的主進程將請求接入,然后生成一個子進程來響應,所以生成的子進程最多只能有1024個(當有1024個進程時,一個子進程剛好處理一個IO請求),如果請求超過上限就可能會拒絕掉請求
- 當一個用戶進程處理多個網絡連接IO時,就會等待多個流數據,而select函數有一個參數是文件描述符集合,對這些文件描述符進行循環監聽。當select函數返回后,用戶空間進程從select那里僅僅知道了,有I/O事件發生了,卻并不知道是哪那幾個流(可能有一個,多個,甚至全部),用戶空間進程只能無差別輪詢所有流,找出能讀出數據,或者寫入數據的流,對他們進行操作。所以select具有O(n)的無差別輪詢復雜度,同時處理的流越多,無差別輪詢時間就越長。
-
多路復用IO模型簡介:
-
select(socket);
while(1) {
sockets = select();
for(socket in sockets) {
if(can_read(socket)) {
read(socket, buffer);
process(buffer);
}
}
}
- while循環前將socket添加到select監視中,然后在while內select一直調用read獲取被激活的socket,一旦socket可讀,read函數將socket中的數據讀取出來
-
-
信號驅動I/O模型(signal driven I/O, SIGIO):雖然上述方式允許單線程內處理多個IO請求,但是用戶空間進程將每個IO請求交給代理select后,用戶空間進程還是阻塞的(阻塞在select函數上),平均時間甚至比同步阻塞IO模型還要長。那么就有了事件驅動型IO,httpd的event模型和nginx用的此種IO模型
-
IO多路復用模型使用了Reactor設計模式實現了這一機制
- 通過Reactor的方式,用戶線程的IO請求統一交給handle_events事件循環進行處理。用戶線程注冊事件處理器之后可以繼續執行做其他的工作(異步,用戶空間進程非阻塞),而Reactor線程負責調用內核的select函數檢查socket狀態。當有socket被激活時,Reactor線程會將被激活的socket標記處理,通知給用戶空間進程,知道是哪個socket有數據,方便read數據。之后用戶空間進程通過系統調用read來獲取數據(此階段還是阻塞的)
- 由于數據讀取的第一階段,用戶空間進程是非阻塞的,這樣一個進程就能處理多個客戶端的IO請求
- epoll可以理解為event poll,不同于忙輪詢和無差別輪詢,epoll會把哪個流發生了怎樣的I/O事件通知用戶空間進程。所以說epoll實際上是事件驅動(每個事件關聯上fd)的,此時我們對這些流的操作都是有意義的。(復雜度降低到了O(1))
- select和epoll最大的區別就是:select只是告訴用戶空間進程一定數目的流有事件了,至于哪個流有事件,還得一個一個地去輪詢,而epoll會把發生的事件關聯上fd之后告訴用戶空間進程,通過發生的事件,就自然而然定位到哪個流了
-
當一個進程的第一次IO請求的數據準備好后,進程會進入調用read的阻塞狀態,這時這個進程的第二次IO請求的數據也準備好了,那么內核在通知該進程就通知不到,進程如果沒有接收消息,消息會自動消失,此時就有了水平觸發和邊緣觸發兩種通知機制:
- 水平觸發通知多次,通知到進程來調用read處理數據為止
- 邊緣觸發通知一次,進程之后通過回調函數來調用read處理數據
-
IO多路復用模型使用了Reactor設計模式實現了這一機制
-
-
異步I/O模型(AIO, asynchronous I/O):
- 用戶空間進程發起read操作之后,立刻就可以開始去做其它的事。而從kernel的角度看,收到用戶空間進程的asynchronous read之后,首先它會立刻返回,所以不會對用戶進程產生任何block
- 然后,kernel會將數據準備完成,即從磁盤拷貝到內核空間,然后再從內核空間拷貝到用戶空間內存,當這一切都完成之后,kernel才會給用戶進程發送一個signal,告訴它read操作完成了
- 整個過程用戶空間進程沒有任何阻塞
-
-
總結:
-
阻塞、非阻塞、多路IO復用,都是同步IO;異步必定是非阻塞的;
- 真正的異步IO需要CPU的深度參與。換句話說,只有用戶線程在操作IO的時候根本不去考慮IO的執行全部都交給CPU去完成,而自己只等待一個完成信號的時候,才是真正的異步IO。所以,拉一個子線程去輪詢、去死循環,或者使用select、poll、epool,都不是異步。
- 同步:不管是BIO,NIO,還是IO多路復用,第二步數據從內核緩存寫入用戶緩存,一定是由用戶空間進程自行寫入用戶空間緩存,再處理數據。
- 異步:第二步數據是內核寫入的,并放在了用戶線程指定的緩存區,寫入完畢后通知用戶線程。
-
阻塞、非阻塞、多路IO復用,都是同步IO;異步必定是非阻塞的;
-
本文來自投稿,不代表Linux運維部落立場,如若轉載,請注明出處:http://www.www58058.com/95575