五種IO模型祥解

在學習nginx的架構時講到:NGINX是最知名的模塊化,事件驅動,異步,單線程Web服務器和Web代理之一。所以就稍微瞭解下網絡IO模型的基礎知識。
IO有內存IO、網絡IO和磁盤IO三種,通常我們說的IO指的是後兩者。網絡IO的本質是socket的讀取,socket在linux系統被抽象爲流,IO可以理解爲對流的操作。對於一次IO訪問(以read舉例)當一個read操作發生時,它會經歷兩個階段:

  1. 第一階段:等待數據準備,數據從磁盤拷貝到內核空間 (Waiting for the data to be ready)。
  2. 第二階段:將數據從內核空間拷貝到進程空間 (Copying the data from the kernel to the process)。

網絡IO的模型大致有如下幾種:

  1. 阻塞IO(bloking IO)
  2. 非阻塞IO(non-blocking IO)
  3. 多路複用IO(multiplexing IO) select,poll,epoll
  4. 信號驅動式IO(signal-driven IO)
  5. 異步IO(asynchronous IO)

1 阻塞式IO模型

這裏寫圖片描述
去餐館吃飯,點一個自己最愛吃的蓋澆飯,然後在原地等着一直到蓋澆飯做好,自己端到餐桌就餐。這就是典型的同步阻塞。當廚師給你做飯的時候,你需要一直在那裏等着。

對於network io來說,很多時候數據在一開始還沒有到達(比如,還沒有收到一個完整的UDP包),這個時候kernel就要等待足夠的數據到來。而在用戶進程這邊,整個進程會被阻塞。當kernel一直等到數據準備好了,它就會將數據從kernel中拷貝到用戶內存,然後kernel返回結果,用戶進程才解除block的狀態,重新運行起來。
所以,blocking IO的特點就是在IO執行的兩個階段(等待數據和拷貝數據兩個階段)都被block了。

2 非阻塞IO模型

這裏寫圖片描述
接着上面的例子,你每次點完飯就在那裏等着,突然有一天你發現自己真傻。於是,你點完之後,就回桌子那裏坐着,然後估計差不多了,就問老闆飯好了沒,如果好了就去端,沒好的話就等一會再去問,依次循環直到飯做好。這就是同步非阻塞。

當用戶進程發出read操作時,如果kernel中的數據還沒有準備好,那麼它並不會block用戶進程,而是立刻返回一個error。從用戶進程角度講,它發起一個read操作後,並不需要等待,而是馬上就得到了一個結果。用戶進程判斷結果是一個error時,它就知道數據還沒有準備好,於是它可以再次發送read操作。一旦kernel中的數據準備好了,並且又再次收到了用戶進程的system call,那麼它馬上就將數據拷貝到了用戶內存,然後返回。
在非阻塞式IO中,用戶進程其實是需要不斷的主動詢問kernel數據準備好了沒有。用戶線程每次請求IO都可以立即返回,但是爲了拿到數據,需不斷輪詢,無謂地消耗了大量的CPU。

3 I/O複用模型

這裏寫圖片描述
接着上面的列子,你點一份飯然後循環的去問好沒好顯然有點得不償失,還不如就等在那裏直到準備好,但是當你點了好幾樣飯菜的時候,你每次都去問一下所有飯菜的狀態(未做好/已做好)肯定比你每次阻塞在那裏等着好多了。當然,你問的時候是需要阻塞的,一直到有準備好的飯菜或者你等的不耐煩(超時)。這就引出了IO複用,也叫多路IO就緒通知。這是一種進程預先告知內核的能力,讓內核發現進程指定的一個或多個IO條件就緒了,就通知進程。使得一個進程能在一連串的事件上等待。

IO多路複用建立在內核提供的阻塞函數select上,用戶先將需要進行IO操作的socket添加到select中,kernel會“監視”所有select負責的socket,當任何一個socket中的數據準備好了,select就會返回。這個時候用戶進程再調用read操作,將數據從kernel拷貝到用戶進程。
select/epoll的好處就在於單個process就可以同時處理多個網絡連接的IO。它的基本原理就是select/epoll這個function會不斷的輪詢所負責的所有socket,當某個socket有數據到達了,就通知用戶進程。

這個圖和blocking IO的圖其實並沒有太大的不同,事實上還更差一些。因爲這裏需要使用兩個系統調用(select和recvfrom),而blocking IO只調用了一個系統調用(recvfrom)。但是,用select的優勢在於它可以同時處理多個connection。(多說一句:所以,如果處理的連接數不是很高的話,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延遲還更大。select/epoll的優勢並不是對於單個連接能處理得更快,而是在於能處理更多的連接。)

爲什麼epoll,kqueue比select高級?
答案是,他們無輪詢。因爲他們用callback取代了。想想看,當套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成調度,不管哪個Socket是活躍的,都遍歷一遍。這會浪費很多CPU時間。如果能給套接字註冊某個回調函數,當他們活躍時,自動完成相關操作,那就避免了輪詢,這正是epoll與kqueue做的。

4 信號驅動IO模型

這裏寫圖片描述
上文的就餐方式還是需要你每次都去問一下飯菜狀況。於是,你再次不耐煩了,就跟老闆說,哪個飯菜好了就通知我一聲吧。然後就自己坐在桌子那裏幹自己的事情。更甚者,你可以把手機號留給老闆,自己出門,等飯菜好了直接發條短信給你。這就類似信號驅動的IO模型。

應用進程告訴內核:當你的數據報準備好的時候,給我發送一個信號哈,並且調用我的信號處理函數來獲取數據報。這個模型是由信號進行驅動。

5 異步IO模型

真正的異步IO需要操作系統更強的支持。Linux下的asynchronous IO其實用得不多,從內核2.6版本纔開始引入。
這裏寫圖片描述
之前的就餐方式,到最後總是需要你自己去把飯菜端到餐桌。這下你也不耐煩了,於是就告訴老闆,能不能飯好了直接端到你的面前或者送到你的家裏(外賣)。這就是異步非阻塞IO了。

用戶進程發起read操作之後,立刻就可以開始去做其它的事。而另一方面,從kernel的角度,當它受到一個asynchronous read之後,首先它會立刻返回,所以不會對用戶進程產生任何block。然後,kernel會等待數據準備完成,然後將數據拷貝到用戶內存,當這一切都完成之後,kernel會給用戶進程發送一個signal,告訴它read操作完成了。

IO多路複用模型中,數據到達內核後通知用戶線程,用戶線程負責從內核空間拷貝數據;
而在異步IO模型中,當用戶線程收到通知時,數據已經被操作系統從內核拷貝到用戶指定的緩衝區內,用戶線程直接使用即可。

相比於IO多路複用,異步IO並不常用,因爲目前操作系統對異步IO的支持並不完善,IO多路複用也基本夠用. 有很多做法是用IO多路複用模型模擬異步IO(IO事件觸發時不直接通知用戶線程,而是將數據讀寫完畢後放到用戶指定的緩衝區中)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章