Linux中的五種I/O模型

概念說明

用戶空間和內核空間

現在操作系統都是採用虛擬存儲器,那麼對32位操作系統而言,它的尋址空間(虛擬存儲空間)爲4G(2的32次方)。操作系統的核心是內核,獨立於普通的應用程序,可以訪問受保護的內存空間,也有訪問底層硬件設備的所有權限。爲了保證用戶進程不能直接操作內核(kernel),保證內核的安全,操作系統將虛擬空間劃分爲兩部分,一部分是內核空間,一部分是用戶空間。

針對Linux OS而言,將最高的1G字節(虛擬地址0XC0000000~0XFFFFFFFF),供內核使用,稱爲內核空間,而將較低的3G字節(0X00000000~0XBFFFFFFF),供各個進程使用,稱爲用戶空間;

進程切換

爲了控制進程的執行,內核必須有能力掛起正在CPU上運行的進程,並恢復以前掛起的某個進程的運行,這種行爲稱爲進程的切換。因此可以說,任何進程都是在OS內核的支持下運行的,是與內核緊密相關的;

從一個進程的運行轉到另一個進程的運行,這個過程中經過下面的這些變化:

  • 保存處理機上下文,包括程序計數器和其他寄存器;
  • 更新PCB信息;
  • 把進程的PCB移到相應的隊列,如就緒、在某事件阻塞等隊列;
  • 選擇另一個進程執行,並更新其PCB;
  • 更新內存管理的數據結構;
  • 恢復處理機上下文;

總而言之就是很耗資源,詳細參考:進程切換

進程的阻塞

正在執行的進程,由於期待的某些事件未發生,如請求系統資源失敗、等待某種操作完成、新數據尚未到達或無新工作做等,則由系統自動執行阻塞原語(Block),使自己由運行狀態變爲阻塞狀態。可見,進程的阻塞是進程自身的一種主動行爲,也因此只有運行態的進程(獲得CPU)纔可能轉爲阻塞狀態。當進程進入阻塞狀態時,是不佔用CPU資源的

文件描述符fd

文件描述符(File descriptor)是計算機科學中的一個術語,是一個用於表述指向文件的引用的抽象化概念;

文件描述符形式上是一個非負整數,實際上,它是一個索引值,指向內核爲每一個進程所維護的該進程打開文件的記錄表。當程序打開一個現有文件或創建一個新文件時,內核向進程返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫往往會圍繞着文件描述符展開。但是文件描述符這一概念往往只適用於UNIX、Linux這樣的操作系統。

Linux中的文件描述符與打開文件之間的關係

緩存IO

緩存IO又稱作標準IO,大多數文件系統的默認IO操作都是緩存IO,在Linux的緩存IO機制中,操作系統會將IO的數據緩存在文件系統的頁緩存(page cache)中,也就是說,數據會先被拷貝到操作系統內核的緩衝區中,然後纔會從操作系統內核的緩衝區拷貝到應用程序的地址空間;

緩存IO的缺點:數據在傳輸過程中需要在應用程序地址空間和內核進行多次的數據拷貝操作,這些數據拷貝操作所帶來的CPU以及內存開銷是非常大的。

Linux IO模型

網絡IO的本質是socket的讀取,socket在Linux OS中被抽象爲流,IO可以理解爲對流的操作,對於一次IO訪問,數據會先被拷貝到操作系統內核緩衝區,然後從內核緩衝區拷貝到應用程序的地址空間,所以當一個read操作發生時,會經歷兩個階段:

  • 等待數據準備;
  • 將數據從內核拷貝到進程中;

對socket流而言:

  • 通常涉及等待網絡上的數據分組到達,然後被複制到內核的某個緩衝區;
  • 把數據從內核緩衝區複製到應用進程緩衝區;

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

  • 異步IO
  • 同步模型
    • 阻塞IO
    • 非阻塞IO
    • 多路複用IO
    • 信號驅動IO

從同步異步,阻塞非阻塞的維度劃分來看:

這裏寫圖片描述

每個 IO 模型都有自己的使用模式,它們對於特定的應用程序都有自己的優點。本節將簡要對其一一進行介紹。常見的IO模型有阻塞、非阻塞、IO多路複用,異步。以一個生動形象的例子來說明這四個概念。

週末我和女友去逛街,中午餓了,我們準備去吃飯。週末人多,吃飯需要排隊,我和女友有以下幾種方案。

同步阻塞IO(blocking IO)

場景描述

我和女友點完餐後,不知道什麼時候能做好,只好坐在餐廳裏面等,直到做好,然後吃完才離開。女友本想還和我一起逛街的,但是不知道飯能什麼時候做好,只好和我一起在餐廳等,而不能去逛街,直到吃完飯才能去逛街,中間等待做飯的時間浪費掉了。這就是典型的阻塞。

網絡模型

同步阻塞IO模型是最常用的一個模型,也是最簡單的模型。在Linux中,默認情況下所有socket都是blocking。其符合人們最常見的思考邏輯,阻塞就是進程“被”休息,CPU處理其他進程去了。

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

這裏寫圖片描述

流程描述

當用戶進程調用了recv()/recvfrom()這個系統調用:

Kernel就開始了IO的第一階段:準備數據(對於網絡IO來說,很多時候數據在一開始沒有到達,比如,還沒有收到一個完整的UDP包,這個時候Kernel就要等待足夠的數據到來)。這個過程需要等待,也就是說數據被拷貝到OS內核的緩衝區是需要一個過程的。而在用戶進程這邊,整個進程會被阻塞(當然,是進程自己選擇阻塞)。

第二個階段:當Kernel一直等到數據準備好了,它就會將數據從Kernel中拷貝到用戶內存,然後Kernel返回結果,用戶進程才解除block的狀態,重新運行起來;

所以,同步阻塞(blocking IO)的特點就是在IO執行的兩個階段都被block了;

優點:

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

缺點:

  • 對用戶來說處於等待就要付出性能的代價;

同步非阻塞IO(nonblocking IO)

場景描述:

我女友不甘心白白在這等,又想去逛商場,又擔心飯好了。所以我們逛一會,回來詢問服務員飯好了沒有,來來回回好多次,飯都還沒吃都快累死了啦。這就是非阻塞。需要不斷的詢問,是否準備好了。

網絡模型:

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

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

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

在Linux下,可以通過設置socket使其變爲non-blocking,當對一個non-blocking socket執行讀操作的時候,流程如圖所示:

這裏寫圖片描述

流程描述:

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

non-blocking IO的特點是用戶進程需要不斷的主動詢問Kernel數據準備好了沒有。

同步非阻塞相比同步阻塞方式:

  • 優點:能夠在等待完成時間裏幹其他活了(包括提交其他任務,也就是“後臺”可以有多個任務同時執行);
  • 任務完成的響應延遲增大了,因爲每過一段時間纔去輪詢一次read操作,而任務可能在兩次輪詢之間的任意時間完成,這會導致整體數據吞吐量的降低;

IO多路複用(IO multiplexing)

場景描述:

與第二個方案差不多,餐廳安裝了電子屏幕用來顯示點餐的狀態,這樣我和女友逛街一會,回來就不用去詢問服務員了,直接看電子屏幕就可以了。這樣每個人的餐是否好了,都直接看電子屏幕就可以了,這就是典型的IO多路複用。

網絡模型:

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

IO多路複用有幾個特別的系統調用select、poll、epoll函數,select調用是內核級別的,select輪詢相對於非阻塞的輪詢的區別在於前者可以等待多個socket,能實現同時對多個IO端口進行監聽,當其中任何一個socket的數據準備好了,就能返回進行可讀,然後進程再進行recvfrom系統調用,將數據由內核拷貝到用戶進程,當然這個過程是阻塞的。

select或poll調用之後,會阻塞進程,與同步阻塞(blocking IO)不同的是,此時的select不是等到socket數據全部到達再處理,而是有一部分數據就會調用用戶進程來處理,如何知道有一部分數據到達了呢?監視的事情交給了內核,內核負責數據到達的處理,也可以理解爲“非阻塞”吧

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

對於多路複用,也就是輪詢多個socket,多路複用既然可以處理多個IO,也就帶來了新的問題,多個IO之間的順序變得不確定了,當然也可以針對不同的編號。具體流程,如下圖所示:

這裏寫圖片描述

流程描述:

IO multiplexing就是我們說的select、poll、epoll,有些地方稱這些IO方式爲event driven IO,select/poll好處就是單個process就可以同時處理多個網絡連接的IO。它的基本原理就是select、poll、epoll這些函數會不斷的輪詢所負責的所有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 multiplexing Model中,實際中,對於每一個socket,一般都設置成爲non-blocking,但是,如上圖所示,整個用戶的process 其實一直被block,只不過process 是被select這個函數block,而不是被socket IO給block。所以,IO多路複用是阻塞在select,epoll這樣的系統調用上,而沒有阻塞在真正的I/O系統調用如recvfrom上。

在IO編程過程中,當需要同時處理多個客戶端接入請求時,可以利用多線程或I/O多路複用技術進行處理。I/O多路複用技術通過把多個I/O阻塞複用到同一個select的阻塞上,從而使得系統在單線程的情況下可以同時處理多個客戶端的請求,與傳統的多線程/多進程模型比,I/O多路複用的最大優勢就是系統開銷小,系統不需要創建新的額外進程或線程,也不需要維護這些進程和線程的運行,降低了系統的維護工作量,節省了系統資源,I/O多路複用的主要應用場景如下:

  • 服務器需要同時處理多個處於監聽狀態或者多個連接狀態的套接字;
  • 服務器需要同時處理多種網絡協議的套接字;

瞭解了前面三種IO模式,在用戶進程進行系統調用的時候,他們在等待數據到來的時候,處理的方式不一樣,直接等待,輪詢,select或poll輪詢,兩個階段過程:

  • 第一個階段有的阻塞,有的不阻塞,有的可以阻塞又可以不阻塞;
  • 第二個階段都是阻塞的;

從整個IO過程來看,它們都是順序執行的,因此可歸爲同步模型(synchronous),都是進程主動等待且向內核檢查狀態。

高併發的程序一般使用同步非阻塞方式而非多線程 + 同步阻塞方式。要理解這一點,首先要扯到併發和並行的區別。比如去某部門辦事需要依次去幾個窗口,辦事大廳裏的人數就是併發數,而窗口個數就是並行度。也就是說併發數是指同時進行的任務數(如同時服務的 HTTP 請求),而並行數是可以同時工作的物理資源數量(如 CPU 核數)。通過合理調度任務的不同階段,併發數可以遠遠大於並行度,這就是區區幾個 CPU 可以支持上萬個用戶併發請求的奧祕。在這種高併發的情況下,爲每個任務(用戶請求)創建一個進程或線程的開銷非常大。而同步非阻塞方式可以把多個 IO 請求丟到後臺去,這就可以在一個進程裏服務大量的併發 IO 請求。

IO多路複用是同步阻塞模型還是異步阻塞模型,在此給大家分析下:

此處仍然不太清楚的,強烈建議大家在細究《聊聊同步、異步、阻塞與非阻塞》中講同步與異步的根本性區別,同步是需要主動等待消息通知,而異步則是被動接收消息通知,通過回調、通知、狀態等方式來被動獲取消息。IO多路複用在阻塞到select階段時,用戶進程是主動等待並調用select函數獲取數據就緒狀態消息,並且其進程狀態爲阻塞。所以,把IO多路複用歸爲同步阻塞模式。

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

信號驅動式IO:首先允許socket進行信號驅動IO,並安裝一個信號處理函數,進程繼續運行並不阻塞。當數據準備好時,進程會受到SIGIO信號,可以在信號處理函數中調用I/O操作函數處理數據。如圖所示:

這裏寫圖片描述

異步非阻塞 IO(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操作完成了。

在Linux中,通知的方式是“信號”:

  • 如果這個進程正在用戶態忙着做別的事(例如在計算兩個矩陣的乘積),那就強行打斷之,調用事先註冊的信號處理函數,這個函數可以決定何時以及如何處理這個異步任務。由於信號處理函數是突然闖進來的,因此跟中斷處理程序一樣,有很多事情是不能做的,因此保險起見,一般是把事件 “登記” 一下放進隊列,然後返回該進程原來在做的事。

  • 如果這個進程正在內核態忙着做別的事,例如以同步阻塞方式讀寫磁盤,那就只好把這個通知掛起來了,等到內核態的事情忙完了,快要回到用戶態的時候,再觸發信號通知。

  • 如果這個進程現在被掛起了,例如無事可做 sleep 了,那就把這個進程喚醒,下次有 CPU 空閒的時候,就會調度到這個進程,觸發信號通知。

異步 API 說來輕巧,做來難,這主要是對 API 的實現者而言的。Linux 的異步 IO(AIO)支持是 2.6.22 才引入的,還有很多系統調用不支持異步 IO。Linux 的異步 IO 最初是爲數據庫設計的,因此通過異步 IO 的讀寫操作不會被緩存或緩衝,這就無法利用操作系統的緩存與緩衝機制。

很多人把 Linux 的 O_NONBLOCK 認爲是異步方式,但事實上這是前面講的同步非阻塞方式。需要指出的是,雖然 Linux 上的 IO API 略顯粗糙,但每種編程框架都有封裝好的異步 IO 實現。操作系統少做事,把更多的自由留給用戶,正是 UNIX 的設計哲學,也是 Linux 上編程框架百花齊放的一個原因。

從前面 IO 模型的分類中,我們可以看出 AIO 的動機:

  • 同步阻塞模型需要在 IO 操作開始時阻塞應用程序。這意味着不可能同時重疊進行處理和 IO 操作。
  • 同步非阻塞模型允許處理和 IO 操作重疊進行,但是這需要應用程序根據重現的規則來檢查 IO 操作的狀態。
  • 這樣就剩下異步非阻塞 IO 了,它允許處理和 IO 操作重疊進行,包括 IO 操作完成的通知。

IO多路複用除了需要阻塞之外,select 函數所提供的功能(異步阻塞 IO)與 AIO 類似。不過,它是對通知事件進行阻塞,而不是對 IO 調用進行阻塞。

小結

  • 同步IO模型要求用戶代碼自動執行I/O操作(將數據從內核緩衝區讀入用戶緩衝區,或將數據從用戶緩衝區寫入內核緩衝區)。而異步IO機制則由內核來執行I/O操作(數據在內核緩衝區和用戶緩衝區之間移動是由內核在“後臺”完成的);

  • 同步I/O嚮應用程序通知的是I/O就緒事件,異步I/O嚮應用程序通知的是I/O完成事件;

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