1. 程式人生 > >I/O模型

I/O模型

libuv func io模型 conn sock 簡單 回調 bsp read

網絡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)。

對於socket流而言

  • 第一步:通常涉及等待網絡上的數據分組到達,然後被復制到內核的某個緩沖區。
  • 第二步:把數據從內核緩沖區復制到應用進程緩沖區。

網絡應用需要處理的無非就是兩大類問題,網絡IO,數據計算。相對於後者,網絡IO的延遲,給應用帶來的性能瓶頸大於後者。網絡IO的模型大致有如下幾種:

  • 同步模型(synchronous IO)

  • 阻塞IO(bloking IO)

  • 非阻塞IO(non-blocking IO)

  • 多路復用IO(multiplexing IO)

  • 信號驅動式IO(signal-driven IO)

  • 異步IO(asynchronous IO)

同步阻塞IO:

在linux中,默認情況下所有的socket都是blocking。它符合人們最常見的思考邏輯。阻塞就是進程 "被" 休息, CPU處理其它進程去了。

在這個IO模型中,用戶空間的應用程序執行一個系統調用(recvform),這會導致應用程序阻塞,什麽也不幹,直到數據準備好,並且將數據從內核復制到用戶進程,最後進程再處理數據,在等待數據到處理數據的兩個階段,整個進程都被阻塞。不能處理別的網絡IO。調用應用程序處於一種不再消費 CPU 而只是簡單等待響應的狀態,因此從處理的角度來看,這是非常有效的。在調用recv()/recvfrom()函數時,發生在內核中等待數據和復制數據的過程,大致如下圖:

技術分享

流程分析

當用戶進程調用了recv()/recvfrom()這個系統調用,kernel就開始了IO的第一個階段:準備數據(對於網絡IO來說,很多時候數據在一開始還沒有到達。比如,還沒有收到一個完整的UDP包。這個時候kernel就要等待足夠的數據到來)。這個過程需要等待,也就是說數據被拷貝到操作系統內核的緩沖區中是需要一個過程的。而在用戶進程這邊,整個進程會被阻塞(當然,是進程自己選擇的阻塞)。第二個階段:當kernel一直等到數據準備好了,它就會將數據從kernel中拷貝到用戶內存,然後kernel返回結果,用戶進程才解除block的狀態,重新運行起來。所以,blocking IO的特點就是在IO執行的兩個階段都被block了。

優點:

  • 能夠及時返回數據,無延遲;
  • 對內核開發者來說這是省事了;

缺點:對用戶來說處於等待就要付出性能的代價了;

同步非阻塞IO(nonblocking IO):

同步非阻塞就是 “每隔一會兒瞄一眼進度條” 的輪詢(polling)方式。在這種模型中,設備是以非阻塞的形式打開的。這意味著 IO 操作不會立即完成,read 操作可能會返回一個錯誤代碼,說明這個命令不能立即滿足(EAGAIN 或 EWOULDBLOCK)。

在網絡IO時候,非阻塞IO也會進行recvform系統調用,檢查數據是否準備好,與阻塞IO不一樣,”非阻塞將大的整片時間的阻塞分成N多的小的阻塞, 所以進程不斷地有機會 ‘被’ CPU光顧”。

也就是說非阻塞的recvform系統調用調用之後,進程並沒有被阻塞,內核馬上返回給進程,如果數據還沒準備好,此時會返回一個error。進程在返回之後,可以幹點別的事情,然後再發起recvform系統調用。重復上面的過程,循環往復的進行recvform系統調用。這個過程通常被稱之為輪詢。輪詢檢查內核數據,直到數據準備好,再拷貝數據到進程,進行數據處理。需要註意,拷貝數據整個過程,進程仍然是屬於阻塞的狀態。

技術分享

流程:

當用戶進程發出read操作時,如果kernel中的數據還沒有準備好,那麽它並不會block用戶進程,而是立刻返回一個error。從用戶進程角度講,它發起一個read操作後,並不需要等待,而是馬上就得到了一個結果。用戶進程判斷結果是一個error時,它就知道數據還沒有準備好,於是它可以再次發送read操作。一旦kernel中的數據準備好了,並且又再次收到了用戶進程的system call,那麽它馬上就將數據拷貝到了用戶內存,然後返回。所以,nonblocking IO的特點是用戶進程需要不斷的主動詢問kernel數據好了沒有。

優點:能夠在等待任務完成的時間裏幹其他活了(包括提交其他任務,也就是 “後臺” 可以有多個任務在同時執行)。

缺點:任務完成的響應延遲增大了,因為每過一段時間才去輪詢一次read操作,而任務可能在兩次輪詢之間的任意時間完成。這會導致整體數據吞吐量的降低。

IO 多路復用(IO multiplexing)

由於同步非阻塞方式需要不斷主動輪詢,輪詢占據了很大一部分過程,輪詢會消耗大量的CPU時間,而 “後臺” 可能有多個任務在同時進行,人們就想到了循環查詢多個任務的完成狀態,只要有任何一個任務完成,就去處理它。如果輪詢不是進程的用戶態,而是有人幫忙就好了。那麽這就是所謂的 “IO 多路復用”。UNIX/Linux 下的 select、poll、epoll 就是幹這個的(epoll 比 poll、select 效率高,做的事情是一樣的)。

IO多路復用有兩個特別的系統調用select、poll、epoll函數。select調用是內核級別的,select輪詢相對非阻塞的輪詢的區別在於—前者可以等待多個socket,能實現同時對多個IO端口進行監聽,當其中任何一個socket的數據準好了,就能返回進行可讀,然後進程再進行recvform系統調用,將數據由內核拷貝到用戶進程,當然這個過程是阻塞的。select或poll調用之後,會阻塞進程,與blocking IO阻塞不同在於,此時的select不是等到socket數據全部到達再處理, 而是有了一部分數據就會調用用戶進程來處理。如何知道有一部分數據到達了呢?監視的事情交給了內核,內核負責數據到達的處理。也可以理解為"非阻塞"吧。

I/O復用模型會用到select、poll、epoll函數,這幾個函數也會使進程阻塞,但是和阻塞I/O所不同的的,這兩個函數可以同時阻塞多個I/O操作。而且可以同時對多個讀操作,多個寫操作的I/O函數進行檢測,直到有數據可讀或可寫時(註意不是全部數據可讀或可寫),才真正調用I/O操作函數。

技術分享

IO multiplexing就是我們說的select,poll,epoll,有些地方也稱這種IO方式為event driven IO。select/epoll的好處就在於單個process就可以同時處理多個網絡連接的IO。它的基本原理就是select,poll,epoll這個function會不斷的輪詢所負責的所有socket,當某個socket有數據到達了,就通知用戶進程。

當用戶進程調用了select,那麽整個進程會被block,而同時,kernel會“監視”所有select負責的socket,當任何一個socket中的數據準備好了,select就會返回。這個時候用戶進程再調用read操作,將數據從kernel拷貝到用戶進程。

多路復用的特點是通過一種機制一個進程能同時等待IO文件描述符,內核監視這些文件描述符(套接字描述符),其中的任意一個進入讀就緒狀態,select, poll,epoll函數就可以返回。對於監視的方式,又可以分為 select, poll, epoll三種方式。

上面的圖和blocking IO的圖其實並沒有太大的不同,事實上,還更差一些。因為這裏需要使用兩個system call (select 和 recvfrom),而blocking IO只調用了一個system call (recvfrom)。但是,用select的優勢在於它可以同時處理多個connection。

所以,如果處理的連接數不是很高的話,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延遲還更大。(select/epoll的優勢並不是對於單個連接能處理得更快,而是在於能處理更多的連接。)

異步非阻塞IO:

linux下的asynchronous IO其實用得很少。先看一下它的流程:

技術分享

相對於同步IO,異步IO不是順序執行。用戶進程進行aio_read系統調用之後,無論內核數據是否準備好,都會直接返回給用戶進程,然後用戶態進程可以去做別的事情。等到socket數據準備好了,內核直接復制數據給進程,然後從內核向進程發送通知。IO兩個階段,進程都是非阻塞的。Linux提供了AIO庫函數實現異步,但是用的很少。目前有很多開源的異步IO庫,例如libevent、libev、libuv。

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

I/O模型