打破砂鍋問到底之Python同步和非同步IO
Python同步和非同步IO一直都是新手心目中比較難搞懂的問題,那麼現在就一起來追根究底探究一下。先說個容易懂得事情,同步呢,就是你去麥當勞訂個漢堡,你一直在服務檯等著漢堡好了交到你手上。非同步就是,你去麥當勞訂漢堡,然後你不等漢堡好了沒有就去隔壁商城逛街了,直到麥當勞的服務員喊你過去拿漢堡為止。總之1句話,同步有等待,非同步沒有等待!
Linux作業系統基礎知識
使用者空間和核心空間
作業系統的核心是核心,獨立於普通的應用程式,可以訪問受保護的記憶體空間,也有訪問底層硬體裝置的所有許可權。為了保證使用者程序不能直接操作核心保證核心的安全,操心繫統將虛擬空間劃分為兩部分,一部分為核心空間,一部分為使用者空間。
對32位作業系統而言,它的定址空間(虛擬儲存空間)為4G)。針對linux作業系統而言,將最高的1G位元組(從虛擬地址0xC0000000到0xFFFFFFFF),供核心使用,稱為核心空間,而將較低的3G位元組(從虛擬地址0x00000000到0xBFFFFFFF),供各個程序使用,稱為使用者空間。
檔案描述符
File descriptor用於表述只想檔案的引用的抽象概念。檔案描述符在形式上是一個非負整數。實際上,它是一個索引值,指向核心為每一個程序所維護的該程序開啟檔案的記錄表。當程式開啟一個現有檔案或者建立一個新檔案時,核心向程序返回一個檔案描述符。在程式設計中,一些涉及底層的程式編寫往往會圍繞著檔案描述符展開。但是檔案描述符這一概念往往只適用於UNIX、Linux這樣的作業系統。
程序阻塞
正在執行的程序,由於期待的某些事件未發生,如請求系統資源失敗、等待某種操作的完成、新資料尚未到達或無新工作做等,則由系統自動執行阻塞原語(Block),使自己由執行狀態變為阻塞狀態。
可見,程序的阻塞是程序自身的一種主動行為,也因此只有處於執行態的程序(獲得CPU),才可能將其轉為阻塞狀態。當程序進入阻塞狀態,是不佔用CPU資源的。
程序切換
為了控制程序的執行,核心必須有能力掛起正在CPU上執行的程序,並恢復以前掛起的某個程序的執行。這種行為被稱為程序切換。因此可以說,任何程序都是在作業系統核心的支援下執行的,是與核心緊密相關的。
從一個程序的執行轉到另一個程序上執行,這個過程中經過下面這些變化:
1. 儲存處理機上下文,包括程式計數器和其他暫存器。
上下文就是核心再次喚醒當前程序時所需要的狀態,由一些物件(程式計數器、狀態暫存器、使用者棧等各種核心資料結構)的值組成。
這些值包括描繪地址空間的頁表、包含程序相關資訊的程序表、檔案表等。
2. 更新PCB資訊。
3. 把程序的PCB移入相應的佇列,如就緒、在某事件阻塞等佇列。
4. 選擇另一個程序執行,並更新其PCB。
5. 更新記憶體管理的資料結構。
6. 恢復處理機上下文。
總而言之就是很耗資源,具體的可以參考這篇文章:程序切換
直接IO和快取IO
快取 I/O 又被稱作標準 I/O,大多數檔案系統的預設 I/O 操作都是快取 I/O。在 Linux 的快取 I/O 機制中,作業系統會將 I/O 的資料快取在檔案系統的頁快取( page cache )中,也就是說,資料會先被拷貝到作業系統核心的緩衝區中,然後才會從作業系統核心的緩衝區拷貝到應用程式的地址空間。
以write為例,資料會先被拷貝程序緩衝區,在拷貝到作業系統核心的緩衝區中,然後才會寫到儲存裝置中。
直接I/O的write:(少了拷貝到程序緩衝區這一步)
快取 I/O 的缺點:
資料在傳輸過程中需要在應用程式地址空間和核心進行多次資料拷貝操作,這些資料拷貝操作所帶來的 CPU 以及記憶體開銷是非常大的。
IO 模式
對於一次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)
阻塞IO
在linux中,預設情況下所有的socket都是blocking,一個典型的讀操作流程大概是這樣:
read為例:
程序發起read,進行recvfrom系統呼叫;
核心開始第一階段,準備資料(從磁碟拷貝到核心緩衝區),程序請求的資料並不是一下就能準備好;準備資料是要消耗時間的;
在這個過程中,整個使用者程序將會被阻塞(程序自己選擇的阻塞),等待資料;
直到資料從核心拷貝到了使用者空間,核心返回結果,程序解除阻塞,重新執行起來。
因此,核心準備資料和資料從核心拷貝到程序記憶體地址這兩個過程都是阻塞的。
非阻塞IO模型
可以通過設定socket使其變為non-blocking。當對一個non-blocking socket執行讀操作時,流程是這個樣子:
當用戶程序發出read操作時,如果kernel中的資料還沒有準備好;
那麼它並不會block使用者程序,而是立刻返回一個error,從使用者程序角度講 ,它發起一個read操作後,並不需要等待,而是馬上就得到了一個結果;
使用者程序判斷結果是一個error時,它就知道資料還沒有準備好,於是它可以再次傳送read操作。一旦kernel中的資料準備好了,並且又再次收到了使用者程序的system call;
那麼它馬上就將資料拷貝到了使用者記憶體,然後返回。
所以,nonblocking IO的特點是使用者程序在核心準備資料的階段需要不斷的主動詢問資料好了沒有。
IO多路複用
I/O多路複用使用select, poll, epoll監聽多個io物件,當io物件有變化(有資料)的時候就通知使用者程序。好處就是單個程序可以處理多個socket。當然具體區別我們後面再討論,現在先來看下I/O多路複用的流程:
當用戶程序呼叫了select,那麼整個程序會被block;
而同時,kernel會“監視”所有select負責的socket;
當任何一個socket中的資料準備好了,select就會返回;
這個時候使用者程序再呼叫read操作,將資料從kernel拷貝到使用者程序。
所以,I/O 多路複用的特點是通過一種機制一個程序能同時等待多個檔案描述符,而這些檔案描述符(套接字描述符)其中的任意一個進入讀就緒狀態,select()函式就可以返回。
和阻塞IO相比,這裡需要使用兩個system call (select 和 recvfrom),而blocking IO只調用了一個system call (recvfrom)。但是,用select的優勢在於它可以同時處理多個connection。
所以,如果處理的連線數不是很高的話,使用select/epoll的web server不一定比使用多執行緒 + 阻塞 IO的web server效能更好,可能延遲還更大。
select/epoll的優勢並不是對於單個連線能處理得更快,而是在於能處理更多的連線。
在IO multiplexing Model中,實際中,對於每一個socket,一般都設定成為non-blocking,但是,如上圖所示,整個使用者的process其實是一直被block的。只不過process是被select這個函式block,而不是被socket IO給block。
非同步 IO
使用者程序發起read操作之後,立刻就可以開始去做其它的事。
而另一方面,從kernel的角度,當它受到一個asynchronous read之後,首先它會立刻返回,所以不會對使用者程序產生任何block。
然後,kernel會等待資料準備完成,然後將資料拷貝到使用者記憶體,當這一切都完成之後,kernel會給使用者程序傳送一個signal,告訴它read操作完成了。
小結
在non-blocking IO中,雖然程序大部分時間都不會被block,但是它仍然要求程序去主動的check,並且當資料準備完成以後,也需要程序主動的再次呼叫recvfrom來將資料拷貝到使用者記憶體。
而asynchronous IO則完全不同。它就像是使用者程序將整個IO操作交給了他人(kernel)完成,然後他人做完後發訊號通知。在此期間,使用者程序不需要去檢查IO操作的狀態,也不需要主動的去拷貝資料。