【目錄】 一 IO模型介紹 二 阻塞IO(blocking IO) 三 非阻塞IO(non-blocking IO) 四 多路復用IO(IO multiplexing) 五 非同步IO(Asynchronous I/O) 六 IO模型比較分析 七 selectors模塊 本文討論的背景是Linux環境 ...
【目錄】
一 IO模型介紹
二 阻塞IO(blocking IO)
三 非阻塞IO(non-blocking IO)
四 多路復用IO(IO multiplexing)
五 非同步IO(Asynchronous I/O)
六 IO模型比較分析
七 selectors模塊
本文討論的背景是Linux環境下的network IO
一、 IO模型介紹
回顧:同步/非同步 阻塞/非阻塞
1、IO分類
同步(synchronous) IO
非同步(asynchronous) IO
阻塞(blocking) IO
非阻塞(non-blocking)IO
2、IO模型
五種IO Model:
* blocking IO、 * nonblocking IO、* IO multiplexing 、* signal driven IO 、* asynchronous IO
(由於signal driven IO(信號驅動IO)在實際中並不常用,所以主要介紹其餘四種 IO Model)
3、IO發生時涉及的對象和步驟
對於一個network IO (這裡我們以read舉例),它會涉及到兩個系統對象——
一個是 調用這個IO的process (or thread),另一個就是 系統內核(kernel)。
當一個read操作發生時,該操作會經歷兩個階段:
#1)等待數據準備 (Waiting for the data to be ready)
#2)將數據從內核拷貝到進程中(Copying the data from the kernel to the process)
記住這兩點很重要,因為這些IO模型的區別就是在兩個階段上各有不同的情況。
4、補充
#1、輸入操作:read、readv、recv、recvfrom、recvmsg共5個函數,如果會阻塞狀態,則會經歷wait data和copy data兩個階段,如果設置為非阻塞則在wait 不到data時拋出異常
#2、輸出操作:write、writev、send、sendto、sendmsg共5個函數,在發送緩衝區滿了會阻塞在原地,如果設置為非阻塞,則會拋出異常
#3、接收外來鏈接:accept,與輸入操作類似
#4、發起外出鏈接:connect,與輸出操作類似
二、 阻塞IO(blocking IO)
1、圖解
在linux中,預設情況下所有的socket都是blocking,一個典型的讀操作流程大概是這樣:
分析版——
2、過程描述:
kernel(內核 / 操作系統):
當用戶進程調用了recvfrom這個系統調用,kernel就開始了IO的第一個階段:準備數據。
對於network io來說,很多時候數據在一開始還沒有到達(比如,還沒有收到一個完整的UDP包),這個時候kernel就要等待足夠的數據到來。
用戶進程:
而在用戶進程這邊,整個進程會被阻塞。當kernel一直等到數據準備好了,它就會將數據從kernel中拷貝到用戶記憶體,然後kernel返回結果,用戶進程才解除block的狀態,重新運行起來。
3、特點:
blocking IO的特點:在IO執行的兩個階段(等待數據和拷貝數據兩個階段)都被block了
4、補充:
所謂阻塞型介面是指系統調用(一般是IO介面)不返回調用結果並讓當前線程一直阻塞,只有當該系統調用獲得結果或者超時出錯時才返回。
實際上,除非特別指定,幾乎所有的IO介面 ( 包括socket介面 ) 都是阻塞型的。
如何解決調用阻塞問題——
在伺服器端使用多線程 / 多進程(讓每個連接都擁有獨立的線程(或進程),這樣任何一個連接的阻塞都不會影響其他的連接)
或者 使用 線程池 / 連接池(“線程池”旨在減少創建和銷毀線程的頻率,其維持一定合理數量的線程,並讓空閑的線程重新承擔新的執行任務。“連接池”維持連接的緩存池,儘量重用已有的連接、減少創建和關閉連接的頻率。這兩種技術都可以很好的降低系統開銷,都被廣泛應用很多大型系統,如websphere、tomcat和各種資料庫等。)
上述兩種手段,都沒有很好地解決阻塞問題,該阻塞等待的還是如此——因此,嘗試使用 非阻塞IO
""" 我們之前寫的都是阻塞IO模型 協程除外 """ import socket server = socket.socket() server.bind(('127.0.0.1',8080)) server.listen(5) while True: conn, addr = server.accept() while True: try: data = conn.recv(1024) if len(data) == 0:break print(data) conn.send(data.upper()) except ConnectionResetError as e: break conn.close() # 在服務端開設多進程或者多線程 進程池線程池 其實還是沒有解決IO問題 該等的地方還是得等 沒有規避 只不過多個人等待的彼此互不幹擾典型的阻塞IO模型
三、 非阻塞IO(non-blocking IO)
1、圖解
Linux下,可以通過設置socket使其變為non-blocking。當對一個non-blocking socket執行讀操作時,流程是這個樣子:
2、過程描述
# 當用戶進程發出read操作時,如果kernel中的數據還沒有準備好,那麼它並不會block用戶進程,而是立刻返回一個error。
從用戶進程角度講 ,它發起一個read操作後,並不需要等待,而是馬上就得到了一個結果。
用戶進程判斷結果是一個error時,它就知道數據還沒有準備好,於是用戶就可以在本次到下次再發起read詢問的時間間隔內做其他事情,或者直接再次發送read操作。
一旦kernel中的數據準備好了,並且又再次收到了用戶進程的system call,那麼它馬上就將數據拷貝到了用戶記憶體(這一階段仍然是阻塞的),然後返回。
# 也就是說非阻塞的recvform系統調用調用之後,進程並沒有被阻塞,內核馬上返回給進程,如果數據還沒準備好,此時會返回一個error。進程在返回之後,可以乾點別的事情,然後再發起recvform系統調用。
重覆上面的過程,迴圈往複的進行recvform系統調用。這個過程通常被稱之為輪詢。輪詢檢查內核數據,直到數據準備好,再拷貝數據到進程,進行數據處理。
需要註意,拷貝數據整個過程,進程仍然是屬於阻塞的狀態。
3、特點
在非阻塞式IO中,用戶進程其實是需要不斷的主動詢問kernel數據準備好了沒有。
非阻塞的recvform系統調用調用之後,進程並沒有被阻塞,內核馬上返回給進程,如果數據還沒準備好,此時會返回一個error。進程在返回之後,可以乾點別的事情,然後再發起recvform系統調用。
""" 要自己實現一個非阻塞IO模型 """ import socket import time server = socket.socket() server.bind(('127.0.0.1', 8081)) server.listen(5) server.setblocking(False) # 將所有的網路阻塞變為非阻塞 r_list = [] del_list = [] while True: try: conn, addr = server.accept() r_list.append(conn) except BlockingIOError: # time.sleep(0.1) # print('列表的長度:',len(r_list)) # print('做其他事') for conn in r_list: try: data = conn.recv(1024) # 沒有消息 報錯 if len(data) == 0: # 客戶端斷開鏈接 conn.close() # 關閉conn # 將無用的conn從r_list刪除 del_list.append(conn) continue conn.send(data.upper()) except BlockingIOError: continue except ConnectionResetError: conn.close() del_list.append(conn) # 揮手無用的鏈接 for conn in del_list: r_list.remove(conn) del_list.clear() # 客戶端 import socket client = socket.socket() client.connect(('127.0.0.1',8081)) while True: client.send(b'hello world') data = client.recv(1024) print(data)需要自己實現一個非阻塞IO模型
4、補充——實際應用中,非阻塞IO模型 絕不被推薦。
優點:
能夠在等待任務完成的時間里乾其他活了(包括提交其他任務,也就是 “後臺” 可以有多個任務在“”同時“”執行)。
缺點:
#1. 迴圈調用recv()將大幅度推高CPU占用率;這也是我們在代碼中會設置 time.sleep( )的原因,
否則在低配主機下極容易出現卡機情況
#2. 任務完成的響應延遲增大了,因為每過一段時間才去輪詢一次read操作,而任務可能在兩次輪詢之間的任意時間完成。
這會導致整體數據吞吐量的降低。
四、 多路復用IO(IO multiplexing)——又名:事件驅動IO(event driven IO)
IO multiplexing,即 使用 select/epoll,其好處就在於單個process就可以同時處理多個網路連接的IO。
它的基本原理就是select/epoll 這個function會不斷的輪詢所負責的所有socket,當某個socket有數據到達了,就通知用戶進程。
它的流程如圖:
2、過程描述:
# 當用戶進程調用了select,那麼整個進程會被block,而同時,kernel會“監視”所有select負責的socket,
當任何一個socket中的數據準備好了,select就會返回。
這個時候用戶進程再調用read操作,將數據從kernel拷貝到用戶進程。
# 這個圖和blocking IO的圖其實並沒有太大的不同,事實上還更差一些。
因為這裡需要使用兩個系統調用(select和recvfrom),而blocking IO只調用了一個系統調用(recvfrom)。
但是,用select的優勢在於它可以同時處理多個connection。
3、強調--特點:
(1) 如果處理的連接數不是很高的話,使用select/epoll 的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延遲還更大。select/epoll的優勢並不是對於單個連接能處理得更快,而是在於能處理更多的連接。
(2)在多路復用模型中,對於每一個socket,一般都設置成為non-blocking,但是,如上圖所示,整個用戶的process其實是一直被block的。只不過process是被select這個函數block,而不是被socket IO給block。
結論: select的優勢在於可以處理多個連接,不適用於單個連接
""" 當監管的對象只有一個的時候 其實IO多路復用連阻塞IO都比不上!!! 但是IO多路復用可以一次性監管很多個對象 server = socket.socket() conn,addr = server.accept() 監管機制是操作系統本身就有的 如果你想要用該監管機制(select) 需要你導入對應的select模塊 """ import socket import select server = socket.socket() server.bind(('127.0.0.1',8080)) server.listen(5) server.setblocking(False) read_list = [server] while True: r_list, w_list, x_list = select.select(read_list, [], []) """ 幫你監管 一旦有人來了 立刻給你返回對應的監管對象 """ # print(res) # ([<socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080)>], [], []) # print(server) # print(r_list) for i in r_list: # """針對不同的對象做不同的處理""" if i is server: conn, addr = i.accept() # 也應該添加到監管的隊列中 read_list.append(conn) else: res = i.recv(1024) if len(res) == 0: i.close() # 將無效的監管對象 移除 read_list.remove(i) continue print(res) i.send(b'heiheiheiheihei') # 客戶端 import socket client = socket.socket() client.connect(('127.0.0.1',8080)) while True: client.send(b'hello world') data = client.recv(1024) print(data)多路復用代碼例子
"""
監管機制其實有很多——
# select機制 windows linux都有
# poll機制 只在linux有
poll和select都可以監管多個對象 但是poll監管的數量更多
上述select和poll機制其實都不是很完美 當監管的對象特別多的時候
可能會出現 極其大的延時響應
# epoll機制 只在linux有
它給每一個監管對象都綁定一個回調機制
一旦有響應 回調機制立刻發起提醒
# 針對不同的操作系統還需要考慮不同檢測機制 書寫代碼太多繁瑣
有一個能夠根據你跑的平臺的不同自動幫你選擇對應的監管機制——selectors模塊
"""
五 、非同步IO(Asynchronous I/O)
Linux下的asynchronous IO其實用得不多,從內核2.6版本才開始引入。先看一下它的流程:
用戶進程發起read操作之後,立刻就可以開始去做其它的事。
而另一方面,從kernel的角度,當它受到一個asynchronous read之後,首先它會立刻返回,所以不會對用戶進程產生任何block。
然後,kernel會等待數據準備完成,然後將數據拷貝到用戶記憶體,
當這一切都完成之後,kernel會給用戶進程發送一個signal,告訴它read操作完成了
""" 非同步IO模型是所有模型中效率最高的 也是使用最廣泛的 相關的模塊和框架 模塊:asyncio模塊 非同步框架:sanic tronado twisted 速度快!!! """ import threading import asyncio @asyncio.coroutine def hello(): print('hello world %s'%threading.current_thread()) yield from asyncio.sleep(1) # 換成真正的IO操作 print('hello world %s' % threading.current_thread()) loop = asyncio.get_event_loop() tasks = [hello(),hello()] loop.run_until_complete(asyncio.wait(tasks)) loop.close()非同步IO模型
六、 IO模型比較分析
1、blocking 和 non-blocking的區別
調用blocking IO會一直block住對應的進程直到操作完成,而non-blocking IO在kernel還在準備數據的情況下會立刻返回。
2、synchronous IO (同步)和 asynchronous IO(非同步)的區別
兩者的定義:
A synchronous I/O operation causes the requesting process to be blocked until that I/O operationcompletes;
An asynchronous I/O operation does not cause the requesting process to be blocked
兩者的區別就在於synchronous IO做”IO operation”的時候會將process阻塞。
按照這個定義,四個IO模型可以分為兩大類,
之前所述的 blocking IO,non-blocking IO,IO multiplexing都屬於 synchronous IO這一類,
而 asynchronous IO 為另一類 。
各個IO Model的比較如圖所示:
七、 selectors模塊(暫略)
參考資料:
https://zhuanlan.zhihu.com/p/112185254