協程的IO
asyncio 作爲實現異步編程的庫,任務執行中遇到系統IO的時能夠自動切換到其他任務。協程使用的IO模型是IO多路複用。在 asyncio 低階API 一篇中提到過 “以Linux系統爲例,IO模型有阻塞,非阻塞,IO多路複用等。asyncio 常用的是IO多路複用模型的epool
和 kqueue
”。本篇就介紹一下IO多路複用技術以及操作系統的IO,爲後續內容做一個鋪墊。
什麼是IO
根據馮.諾依曼結構,它將計算機分成分爲5個部分:運算器、控制器、存儲器、輸入設備、輸出設備。
涉及計算機核心與其他設備間數據遷移的過程就是IO
常見的IO包括:文件的讀寫、網絡請求。
以文件讀寫爲例,一個應用程序讀一個文件。一個應用程序就是一個進程,操作系統爲每一個進程分配的內存分爲兩個部分,分別是用戶空間和內核空間。以32位系統爲例,用戶空間分配3GB,內核空間分爲1GB。IO操作因爲都是和硬件設備交互,所以不能讓用戶進程直接操作,而是需要進程調用操作提供提供的API來完成。
應用程序讀一個文件的流程是:
- 應用程序調用系統提供讀文件的命令
- 系統將磁盤中文件內容讀取到內核空間
- 系統將文件內容從內核空間拷貝的用戶空間
- 應用程序讀取用戶空間中的文件內容
從文件IO總結IO的基本流程爲:
總結來看,IO操作的基本流程是:
- 應用程序發起IO調用
- 操作系統完成IO操作
阻塞IO模型
阻塞IO模型就是應用程序發起IO調用之後一直阻塞等待,一直等到數據從內核空間拷貝用戶空間,此次調用纔算完成。
流程圖如下:
存在問題:
如果內核數據一直沒準備好,那用戶進程將一直阻塞,CPU空轉而浪費時間。併發大的情況下將導致進程數量變大,限制併發數量。
非阻塞IO模型
應用程序發起IO調用,如果內核空間數據還沒讀取完成,可以先返回錯誤信息給用戶進程,讓它不需要等待,而是通過輪詢的方式再來請求。這就是非阻塞IO,流程圖如下:
非阻塞IO的流程如下:
- 應用進程向操作系統內核,發起讀取數據。
- 操作系統內核數據沒有準備好,立即返回錯誤碼。
- 應用程序輪詢調用,繼續向操作系統內核發起讀取數據。
- 操作系統內核空間讀取數據完成,從內核緩衝區拷貝到用戶空間。
- 完成調用,返回成功提示。
存在問題:
它相對於阻塞IO,雖然大幅提升了性能,但是它依然存在性能問題,即頻繁的輪詢,導致頻繁的系統調用,同樣會消耗大量的CPU資源。
IO多路複用模型
非阻塞IO的問題
非阻塞IO模型下併發情況下應用程序可能會發送上千次請求,如果每一次請求的IO都需要輪詢獲取結果,那麼應用就需要創建上千個線程去輪詢監聽數據是否拷貝完成。
這麼多的線程不斷調用系統函數 recvfrom 請求數據,首先服務器不能支持這麼多請求,其次這種方式太浪費資源了,線程是我們操作系統的寶貴資源,大量的線程用來去讀取數據了,那麼就意味着能做其它事情的線程就會少。如何解決這個問題呢?使用IO多路複用可以將輪詢監聽的線程降低到1個。
IO多路複用介紹
IO多路複用的原理:
可以由一個線程監控多個網絡請求,當有數據準備好之後再通知對應的線程去讀取數據。這樣就可以只需要一個線程完成數據是否就緒狀態的查詢。通過複用一個輪詢的線程節省出大量的線程資源出來,這個就是IO複用模型的思路。
IO多路複用的流程:
- 應用程序調用IO請求返回一個文件描述符
- IO多路複用的函數(select、poll、epoll)同時監控多個文件描述符
- 當某一個文件描述符的狀態變成就緒時,IO多路複用函數通知對應應用程序
- 應用程序讀取文件,數據從內核空間拷貝的用戶空間,完成數據IO
IO多路複用使用的函數有三種,分別是:select、poll、epoll。三者在實現上有一些區別。IO多路複用實現的核心思想是監聽文件描述符fd的狀態,當fd狀態就緒時通知對應的應用讀取數據。
select
應用進程通過調用select函數,可以同時監控多個文件描述符。在select函數監控的fd中,只要有任何一個數據狀態準備就緒了,select函數就會返回可讀狀態,這時應用進程再發起recvfrom請求去讀取數據。
select缺點:
- 監聽的IO最大連接數有限,在Linux系統上一般爲1024。
- select函數是通過遍歷fdset,找到就緒的描述符fd。遍歷的時間性能消耗較大
poll
由於select存在連接數限制,所以後來又提出了poll。poll模型裏面通過使用鏈表的形式來保存自己監控的fd信息,連接數限制問題。
缺點:
select和poll一樣,還是需要通過遍歷文件描述符來獲取已經就緒的socket。如果同時連接的大量客戶端在一時刻可能只有極少處於就緒狀態,伴隨着監視的描述符數量的增長,效率也會線性下降。
epoll
epoll並不是像select一樣去遍歷事件列表逐個輪詢的監控fd的事件狀態,而是事先就建立了fd與之對應的回調函數,當事件激活後主動回調將fd加入到就緒鏈表中,這也就避免了遍歷事件列表的這個操作。
這裏去掉了遍歷文件描述符的低性能操作,而是採用監聽事件回調的的機制。這就是epoll的亮點。
小結
需要注意的是IO多路複用也是阻塞的IO,只不過它能併發處理的IO效率更高。
信號驅動模型
信號驅動IO不再用主動詢問的方式去確認數據是否就緒,而是向內核發送一個信號(調用sigaction的時候建立一個SIGIO的信號),然後應用用戶進程可以去做別的事,不用阻塞。當內核數據準備好後,再通過SIGIO信號通知應用進程,數據準備好後的可讀狀態。應用用戶進程收到信號之後,立即調用recvfrom,去讀取數據。
信號驅動IO模型,在應用進程發出信號後,是立即返回的,不會阻塞進程。它已經有異步操作的感覺了。但是數據複製到應用緩衝的時候,應用進程還是阻塞的。
回過頭來看下,不管是非阻塞IO、IO多路複用還是信號驅動,在數據從內核複製到應用緩衝的時候,都是阻塞的。
異步IO模型
非阻塞IO、IO多路複用還是信號驅動在數據從內核複製到應用緩衝的時候,都是阻塞的,因此都不是真正的異步。
異步IO實現了IO全流程的非阻塞,就是應用進程發出系統調用後,是立即返回的,但是立即返回的不是處理結果,而是表示提交成功類似的意思。等內核數據準備好,將數據拷貝到用戶進程緩衝區,發送信號通知用戶進程IO操作執行完畢。
異步IO的原理很簡單,只需要向內核發送一次請求,就可以完成數據狀態詢問和數據拷貝的所有操作,並且不用阻塞等待結果。
同步、異步、阻塞、非阻塞總結
相關術語:
- 同步阻塞(blocking-IO)簡稱BIO
- 同步非阻塞(non-blocking-IO)簡稱NIO
- 異步非阻塞(asynchronous-non-blocking-IO)簡稱AIO