【目錄】
一 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
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/152388.html
標籤:Python
上一篇:Java8新特性代碼示例淺聊

