Linux 五種IO模型
那麼,在正式開始講Linux IO模型前,比如:同步IO和非同步IO,阻塞IO和非阻塞IO分別是什麼,到底有什麼區別?不同的人在不同的上下文下給出的答案是不同的。所以先限定一下本文的上下文。
1 概念說明#
在進行解釋之前,首先要說明幾個概念:
使用者空間和核心空間
程序切換
程序的阻塞
檔案描述符
快取 IO
1.1 使用者空間與核心空間##
現在作業系統都是採用虛擬儲存器,那麼對32位作業系統而言,它的定址空間(虛擬儲存空間)為4G(2的32次方)。作業系統的核心是核心,獨立於普通的應用程式,可以訪問受保護的記憶體空間,也有訪問底層硬體裝置的所有許可權
作業系統將虛擬空間劃分為兩部分,一部分為核心空間,一部分為使用者空間
。針對linux作業系統而言,將最高的1G位元組(從虛擬地址0xC0000000到0xFFFFFFFF),供核心使用,稱為核心空間,而將較低的3G位元組(從虛擬地址0x00000000到0xBFFFFFFF),供各個程序使用,稱為使用者空間。
1.2 程序切換##
為了控制程序的執行,核心必須有能力掛起正在CPU上執行的程序,並恢復以前掛起的某個程序的執行。這種行為被稱為程序切換。因此可以說,任何程序都是在作業系統核心的支援下執行的,是與核心緊密相關的。
從一個程序的執行轉到另一個程序上執行,這個過程中經過下面這些變化:
儲存處理機上下文,包括程式計數器和其他暫存器。
更新PCB資訊。
把程序的PCB移入相應的佇列,如就緒、在某事件阻塞等佇列。
選擇另一個程序執行,並更新其PCB。
更新記憶體管理的資料結構。
恢復處理機上下文。
注:總而言之就是很耗資源,具體的可以參考這篇文章:程序切換。
1.3 程序的阻塞##
正在執行的程序,由於期待的某些事件未發生,如請求系統資源失敗、等待某種操作的完成、新資料尚未到達或無新工作做等,則由系統自動執行阻塞原語(Block),使自己由執行狀態變為阻塞狀態。可見,程序的阻塞是程序自身的一種主動行為,也因此只有處於執行態的程序(獲得CPU),才可能將其轉為阻塞狀態。當程序進入阻塞狀態,是不佔用CPU資源的
1.4 檔案描述符fd##
檔案描述符(File descriptor)是電腦科學中的一個術語,是一個用於表述指向檔案的引用的抽象化概念
。
檔案描述符在形式上是一個非負整數。實際上,它是一個索引值,指向核心為每一個程序所維護的該程序開啟檔案的記錄表
。當程式開啟一個現有檔案或者建立一個新檔案時,核心向程序返回一個檔案描述符。在程式設計中,一些涉及底層的程式編寫往往會圍繞著檔案描述符展開。但是檔案描述符這一概念往往只適用於UNIX、Linux這樣的作業系統。
1.5 快取 IO##
快取 IO 又被稱作標準 IO,大多數檔案系統的預設 IO 操作都是快取 IO
。在 Linux 的快取 IO 機制中,作業系統會將 IO 的資料快取在檔案系統的頁快取( page cache )中,也就是說,資料會先被拷貝到作業系統核心的緩衝區中,然後才會從作業系統核心的緩衝區拷貝到應用程式的地址空間
。
快取 IO 的缺點:
資料在傳輸過程中需要在應用程式地址空間和核心進行多次資料拷貝操作
,這些資料拷貝操作所帶來的 CPU 以及記憶體開銷是非常大的。
2 Linux IO模型#
網路IO的本質是socket的讀取,socket在linux系統被抽象為流,IO可以理解為對流的操作
。剛才說了,對於一次IO訪問(以read舉例),資料會先被拷貝到作業系統核心的緩衝區中,然後才會從作業系統核心的緩衝區拷貝到應用程式的地址空間
。所以說,當一個read操作發生時,它會經歷兩個階段:
第一階段:等待資料準備 (Waiting for the data to be ready)。
第二階段:將資料從核心拷貝到程序中 (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)
注:由於signal driven IO在實際中並不常用,所以我這隻提及剩下的四種IO Model。
在深入介紹Linux IO各種模型之前,讓我們先來探索一下基本 Linux IO 模型的簡單矩陣。如下圖所示:
輸入圖片說明每個 IO 模型都有自己的使用模式,它們對於特定的應用程式都有自己的優點。本節將簡要對其一一進行介紹。常見的IO模型有阻塞、非阻塞、IO多路複用,非同步
。以一個生動形象的例子來說明這四個概念。週末我和女友去逛街,中午餓了,我們準備去吃飯。週末人多,吃飯需要排隊,我和女友有以下幾種方案。
2.1 同步阻塞 IO(blocking IO)##
2.1.1 場景描述###
我和女友點完餐後,不知道什麼時候能做好,只好坐在餐廳裡面等,直到做好,然後吃完才離開。女友本想還和我一起逛街的,但是不知道飯能什麼時候做好,只好和我一起在餐廳等,而不能去逛街,直到吃完飯才能去逛街,中間等待做飯的時間浪費掉了。
這就是典型的阻塞
。
2.1.2 網路模型###
同步阻塞 IO 模型是最常用的一個模型,也是最簡單的模型
。在linux中,預設情況下所有的socket都是blocking
。它符合人們最常見的思考邏輯。阻塞就是程序 "被" 休息, CPU處理其它程序去了
。
在這個IO模型中,使用者空間的應用程式執行一個系統呼叫(recvform),這會導致應用程式阻塞,什麼也不幹,直到資料準備好,並且將資料從核心複製到使用者程序,最後程序再處理資料,在等待資料到處理資料的兩個階段
,整個程序都被阻塞。不能處理別的網路IO。呼叫應用程式處於一種不再消費 CPU 而只是簡單等待響應的狀態
,因此從處理的角度來看,這是非常有效的。在呼叫recv()/recvfrom()函式時,發生在核心中等待資料和複製資料的過程,大致如下圖:
2.1.3 流程描述###
當用戶程序呼叫了recv()/recvfrom()這個系統呼叫,kernel就開始了IO的第一個階段:準備資料
(對於網路IO來說,很多時候資料在一開始還沒有到達。比如,還沒有收到一個完整的UDP包。這個時候kernel就要等待足夠的資料到來)。這個過程需要等待,也就是說資料被拷貝到作業系統核心的緩衝區中是需要一個過程的。而在使用者程序這邊,整個程序會被阻塞(當然,是程序自己選擇的阻塞)。第二個階段:當kernel一直等到資料準備好了,它就會將資料從kernel中拷貝到使用者記憶體
,然後kernel返回結果,使用者程序才解除block的狀態,重新執行起來。
所以,blocking IO的特點就是在IO執行的兩個階段都被block了。
優點:
能夠及時返回資料,無延遲;
對核心開發者來說這是省事了;
缺點:
- 對使用者來說處於等待就要付出效能的代價了;
2.2 同步非阻塞 IO(nonblocking IO)##
2.2.1 場景描述###
我女友不甘心白白在這等,又想去逛商場,又擔心飯好了。所以我們逛一會,回來詢問服務員飯好了沒有,來來回回好多次,飯都還沒吃都快累死了啦。
這就是非阻塞
。需要不斷的詢問,是否準備好了。
2.2.2 網路模型###
同步非阻塞就是 “每隔一會兒瞄一眼進度條” 的輪詢(polling)方式
。在這種模型中,裝置是以非阻塞的形式開啟的
。這意味著 IO 操作不會立即完成,read 操作可能會返回一個錯誤程式碼,說明這個命令不能立即滿足(EAGAIN 或 EWOULDBLOCK)。
在網路IO時候,非阻塞IO也會進行recvform系統呼叫,檢查資料是否準備好,與阻塞IO不一樣,"非阻塞將大的整片時間的阻塞分成N多的小的阻塞, 所以程序不斷地有機會 '被' CPU光顧"。
也就是說非阻塞的recvform系統呼叫呼叫之後,程序並沒有被阻塞,核心馬上返回給程序,如果資料還沒準備好,此時會返回一個error
。程序在返回之後,可以乾點別的事情,然後再發起recvform系統呼叫。重複上面的過程,迴圈往復的進行recvform系統呼叫。這個過程通常被稱之為輪詢
。輪詢檢查核心資料,直到資料準備好,再拷貝資料到程序,進行資料處理。需要注意,拷貝資料整個過程,程序仍然是屬於阻塞的狀態
。
在linux下,可以通過設定socket使其變為non-blocking。當對一個non-blocking socket執行讀操作時
,流程如圖所示:
2.2.3 流程描述###
當用戶程序發出read操作時,如果kernel中的資料還沒有準備好,那麼它並不會block使用者程序,而是立刻返回一個error。從使用者程序角度講,它發起一個read操作後,並不需要等待,而是馬上就得到了一個結果。使用者程序判斷結果是一個error時,它就知道資料還沒有準備好,於是它可以再次傳送read操作。一旦kernel中的資料準備好了,並且又再次收到了使用者程序的system call,那麼它馬上就將資料拷貝到了使用者記憶體,然後返回。
所以,nonblocking IO的特點是使用者程序需要不斷的主動詢問kernel資料好了沒有。
同步非阻塞方式相比同步阻塞方式:
優點:能夠在等待任務完成的時間裡幹其他活了(包括提交其他任務,也就是 “後臺” 可以有多個任務在同時執行)。
缺點:任務完成的響應延遲增大了,因為每過一段時間才去輪詢一次read操作,而任務可能在兩次輪詢之間的任意時間完成。這會導致整體資料吞吐量的降低。
2.3 IO 多路複用( IO multiplexing)##
2.3.1 場景描述###
與第二個方案差不多,餐廳安裝了電子螢幕用來顯示點餐的狀態,這樣我和女友逛街一會,回來就不用去詢問服務員了,直接看電子螢幕就可以了。這樣每個人的餐是否好了,都直接看電子螢幕就可以了,
這就是典型的IO多路複用
。
2.3.2 網路模型###
由於同步非阻塞方式需要不斷主動輪詢,輪詢佔據了很大一部分過程,輪詢會消耗大量的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操作函式。
對於多路複用,也就是輪詢多個socket。多路複用既然可以處理多個IO,也就帶來了新的問題,多個IO之間的順序變得不確定了
,當然也可以針對不同的編號。具體流程,如下圖所示:
2.3.3 流程描述###
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 multiplexing Model中,實際中,對於每一個socket,一般都設定成為non-blocking
,但是,如上圖所示,整個使用者的process其實是一直被block的。只不過process是被select這個函式block,而不是被socket IO給block
。所以IO多路複用是阻塞在select,epoll這樣的系統呼叫之上,而沒有阻塞在真正的I/O系統呼叫如recvfrom之上。
在I/O程式設計過程中,當需要同時處理多個客戶端接入請求時,可以利用多執行緒或者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多路複用歸為同步阻塞模式
。
2.4 訊號驅動式IO(signal-driven IO)##
訊號驅動式I/O:首先我們允許Socket進行訊號驅動IO,並安裝一個訊號處理函式,程序繼續執行並不阻塞。當資料準備好時,程序會收到一個SIGIO訊號,可以在訊號處理函式中呼叫I/O操作函式處理資料。過程如下圖所示:
輸入圖片說明2.5 非同步非阻塞 IO(asynchronous IO)##
2.5.1 場景描述###
女友不想逛街,又餐廳太吵了,回家好好休息一下。於是我們叫外賣,打個電話點餐,然後我和女友可以在家好好休息一下,飯好了送貨員送到家裡來。這就是典型的非同步,只需要打個電話說一下,然後可以做自己的事情,飯好了就送來了。
2.5.2 網路模型###
相對於同步IO,非同步IO不是順序執行。使用者程序進行aio_read系統呼叫之後,無論核心資料是否準備好,都會直接返回給使用者程序,然後使用者態程序可以去做別的事情
。等到socket資料準備好了,核心直接複製資料給程序,然後從核心向程序傳送通知
。IO兩個階段,程序都是非阻塞的
。
Linux提供了AIO庫函式實現非同步,但是用的很少。目前有很多開源的非同步IO庫,例如libevent、libev、libuv。非同步過程如下圖所示:
輸入圖片說明2.5.3 流程描述###
使用者程序發起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 呼叫進行阻塞
。
2.6 關於非同步阻塞##
有時我們的 API 只提供非同步通知方式,例如在 node.js 裡,但業務邏輯需要的是做完一件事後做另一件事
,例如資料庫連線初始化後才能開始接受使用者的 HTTP 請求。這樣的業務邏輯就需要呼叫者是以阻塞方式來工作
。
為了在非同步環境裡模擬 “順序執行” 的效果,就需要把同步程式碼轉換成非同步形式,這稱為 CPS(Continuation Passing Style)變換
。BYVoid 大神的 continuation.js 庫就是一個 CPS 變換的工具。使用者只需用比較符合人類常理的同步方式書寫程式碼,CPS 變換器會把它轉換成層層巢狀的非同步回撥形式
。
另外一種使用阻塞方式的理由是降低響應延遲
。如果採用非阻塞方式,一個任務 A 被提交到後臺,就開始做另一件事 B,但 B 還沒做完,A 就完成了,這時要想讓 A 的完成事件被儘快處理(比如 A 是個緊急事務),要麼丟棄做到一半的 B,要麼儲存 B 的中間狀態並切換回 A,任務的切換是需要時間的(不管是從磁碟載入到記憶體,還是從記憶體載入到快取記憶體),這勢必降低 A 的響應速度。因此,對實時系統或者延遲敏感的事務,有時採用阻塞方式比非阻塞方式更好
。
3 五種IO模型總結#
3.1 blocking和non-blocking區別##
呼叫blocking IO會一直block住對應的程序直到操作完成,而non-blocking IO在kernel還準備資料的情況下會立刻返回。
3.2 synchronous IO和asynchronous IO區別##
在說明synchronous IO和asynchronous IO的區別之前,需要先給出兩者的定義。POSIX的定義是這樣子的:
A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
An asynchronous I/O operation does not cause the requesting process to be blocked;
兩者的區別就在於synchronous IO做”IO operation”的時候會將process阻塞
。按照這個定義,之前所述的blocking IO,non-blocking IO,IO multiplexing都屬於synchronous IO。
有人會說,non-blocking IO並沒有被block啊。這裡有個非常“狡猾”的地方,定義中所指的”IO operation”是指真實的IO操作
,就是例子中的recvfrom這個system call。non-blocking IO在執行recvfrom這個system call的時候,如果kernel的資料沒有準備好,這時候不會block程序。但是,當kernel中資料準備好的時候,recvfrom會將資料從kernel拷貝到使用者記憶體中,這個時候程序是被block了
,在這段時間內,程序是被block的。
而asynchronous IO則不一樣,當程序發起IO 操作之後,就直接返回再也不理睬了,直到kernel傳送一個訊號,告訴程序說IO完成
。在這整個過程中,程序完全沒有被block。
各個IO Model的比較如圖所示:
輸入圖片說明通過上面的圖片,可以發現non-blocking IO和asynchronous IO的區別還是很明顯的。在non-blocking IO中,雖然程序大部分時間都不會被block,但是它仍然要求程序去主動的check
,並且當資料準備完成以後,也需要程序主動的再次呼叫recvfrom來將資料拷貝到使用者記憶體。而asynchronous IO則完全不同。它就像是使用者程序將整個IO操作交給了他人(kernel)完成,然後他人做完後發訊號通知
。在此期間,使用者程序不需要去檢查IO操作的狀態,也不需要主動的去拷貝資料
。
作者:書海陶然
連結:https://www.jianshu.com/p/486b0965c296
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。