Python開發【第十章】:I/O多工、非同步I/O(綜合篇),python綜合篇
近期心得:國慶節放假再加上近期工作太忙,已經有半個月沒更新部落格了,程式更別說了,也沒怎麼去寫,自己給自己著實放了個大假。談談感受的話,沒有python的日子,每天看書、看電影、各種玩,還有爸媽伺候著,簡直過的太爽了,哈哈哈哈。好了,迴歸正題,前兩個月實在感覺太累了,導致工作學習效率都特別低,俗話說徐徐漸進勞逸結合才能有高產出,好在總算歇過來了,最近要把之前欠的部落格慢慢找補回來,哈哈,python,額又會來了。。。。。
一、概念說明
同步IO和非同步IO,阻塞IO和非阻塞IO分別是什麼,到底有什麼區別?不同的人在不同的環境給出的答案是不同的。所以先限定一下本文的環境。本文討論的背景是Linux環境下的network IO
在進行解釋之前,首先要說明幾個概念:
- 使用者空間和核心空間
- 進程切換
- 進程的阻塞
- 檔案描述符
- 緩衝 I/O
使用者空間與核心空間
現在作業系統都是採用虛擬儲存空間,那麼對32位作業系統而言,它的定址空間(虛擬儲存空間)為4G(2的32次方)。作業系統的核心是核心,獨立於普通的應用程式,可以訪問受保護的記憶體空間,也有訪問底層硬體裝置的所有許可權。為了保證使用者進程不能直接操作核心(kernel),保證核心的安全,操心系統將虛擬空間劃分為兩部分,一部分為核心空間,一部分為使用者空間。針對linux作業系統而言,將最高的1G位元組(從虛擬位址0xC0000000到0xFFFFFFFF),供核心使用,稱為核心空間,而將較低的3G位元組(從虛擬位址0x00000000到0xBFFFFFFF),供各個進程使用,稱為使用者空間。
進程切換
為了控制進程的執行,核心必須有能力掛起正在CPU上啟動並執行進程,並恢複以前掛起的某個進程的執行。這種行為被稱為進程切換。因此可以說,任何進程都是在作業系統核心的支援下啟動並執行,是與核心緊密相關的。
從一個進程的運行轉到另一個進程上運行,這個過程中經過下面這些變化:
1. 儲存處理機上下文,包括程式計數器和其他寄存器。
2. 更新PCB資訊。
3. 把進程的PCB移入相應的隊列,如就緒、在某事件阻塞等隊列。
4. 選擇另一個進程執行,並更新其PCB。
5. 更新記憶體管理的資料結構。
6. 恢複處理機上下文。
註:總而言之就是很耗資源,具體的可以參考這篇文章:進程切換
進程的阻塞
正在執行的進程,由於期待的某些事件未發生,如請求系統資源失敗、等待某種操作的完成、新資料尚未到達或無新工作做等,則由系統自動執行阻塞原語(Block),使自己由運行狀態變為阻塞狀態。可見,進程的阻塞是進程自身的一種主動行為,也因此只有處於運行態的進程(獲得CPU),才可能將其轉為阻塞狀態。當進程進入阻塞狀態,是不佔用CPU資源的
。
檔案描述符fd
檔案描述符(File descriptor)是電腦科學中的一個術語,是一個用於表述指向檔案的引用的抽象化概念。
檔案描述符在形式上是一個非負整數。實際上,它是一個索引值,指向核心為每一個進程所維護的該進程開啟檔案的記錄表。當程式開啟一個現有檔案或者建立一個新檔案時,核心向進程返回一個檔案描述符。在程式設計中,一些涉及底層的程式編寫往往會圍繞著檔案描述符展開。但是檔案描述符這一概念往往只適用於UNIX、Linux這樣的作業系統。
緩衝 I/O
緩衝 I/O 又被稱作標準 I/O,大多數檔案系統的預設 I/O 操作都是緩衝 I/O。在 Linux 的緩衝 I/O 機制中,作業系統會將 I/O 的資料緩衝在檔案系統的頁緩衝( page cache )中,也就是說,資料會先被拷貝到作業系統核心的緩衝區中,然後才會從作業系統核心的緩衝區拷貝到應用程式的地址空間。
緩衝 I/O 的缺點:
資料在傳輸過程中需要在應用程式地址空間和核心進行多次資料拷貝操作,這些資料拷貝操作所帶來的 CPU 以及記憶體開銷是非常大的。
二、I/O模式
剛才說了,對於一次IO訪問(以read舉例),資料會先被拷貝到作業系統核心的緩衝區中,然後才會從作業系統核心的緩衝區拷貝到應用程式的地址空間。所以說,當一個read操作發生時,它會經曆兩個階段:
1. 等待資料準備 (Waiting for the data to be ready)
2. 將資料從核心拷貝到進程中 (Copying the data from the kernel to the process)
正式因為這兩個階段,linux系統產生了下面五種網路模式的方案。
- 阻塞 I/O(blocking IO)
- 非阻塞 I/O(nonblocking IO)
- I/O 多工( IO multiplexing)
- 訊號驅動 I/O( signal driven IO)
- 非同步 I/O(asynchronous IO)
註:由於signal driven IO在實際中並不常用,所以我這隻提及剩下的四種IO Model
阻塞 I/O(blocking IO)
在linux中,預設情況下所有的socket都是blocking,一個典型的讀操作流程大概是這樣:
當使用者進程調用了recvfrom這個系統調用,kernel就開始了IO的第一個階段:準備資料(對於網路IO來說,很多時候資料在一開始還沒有到達。比如,還沒有收到一個完整的UDP包。這個時候kernel就要等待足夠的資料到來)。這個過程需要等待,也就是說資料被拷貝到作業系統核心的緩衝區中是需要一個過程的。而在使用者進程這邊,整個進程會被阻塞(當然,是進程自己選擇的阻塞)。當kernel一直等到資料準備好了,它就會將資料從kernel中拷貝到使用者記憶體,然後kernel返回結果,使用者進程才解除block的狀態,重新運行起來
所以,blocking IO的特點就是在IO執行的兩個階段都被block了,大大消耗了程式執行的時間
非阻塞 I/O(nonblocking IO)
linux下,可以通過設定socket使其變為non-blocking。當對一個non-blocking socket執行讀操作時,流程是這個樣子:
當使用者進程發出read操作時,如果kernel中的資料還沒有準備好,那麼它並不會block使用者進程,而是立刻返回一個error。從使用者進程角度講 ,它發起一個read操作後,並不需要等待,而是馬上就得到了一個結果。使用者進程判斷結果是一個error時,它就知道資料還沒有準備好,於是它可以再次發送read操作。一旦kernel中的資料準備好了,並且又再次收到了使用者進程的system call,那麼它馬上就將資料拷貝到了使用者記憶體,然後返回
所以,nonblocking IO的特點是使用者進程需要不斷的主動詢問kernel資料好了沒有,wait for data階段進程沒有等待,但是copydata從核心拷貝到使用者進程時,程式是阻塞狀態
I/O 多工( IO multiplexing)
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拷貝到使用者進程
所以,I/O 多工特點是通過一種機制一個進程能同時等待多個檔案描述符,而這些檔案描述符(通訊端描述符)其中的任意一個進入讀就緒狀態,select()函數就可以返回
這個圖和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
非同步 I/O(asynchronous IO)
linux下的asynchronous IO其實用得很少。先看一下它的流程
使用者進程發起read操作之後,立刻就可以開始去做其它的事。而另一方面,從kernel的角度,當它受到一個asynchronous read之後,首先它會立刻返回,所以不會對使用者進程產生任何block。然後,kernel會等待資料準備完成,然後將資料拷貝到使用者記憶體,當這一切都完成之後,kernel會給使用者進程發送一個signal,告訴它read操作完成了
三、總結
blocking和non-blocking的區別
調用blocking IO會一直block住對應的進程直到操作完成,而non-blocking IO在kernel還準備資料的情況下會立刻返回。
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操作的狀態,也不需要主動的去拷貝資料