對于一個network IO (這里我們以read舉例)辩撑,它會涉及到兩個系統(tǒng)對象:
● 一個是調(diào)用這個IO的process (or thread)
● 一個就是系統(tǒng)內(nèi)核(kernel)
當(dāng)一個read操作發(fā)生時界斜,它會經(jīng)歷兩個階段:
● 等待數(shù)據(jù)準(zhǔn)備,比如accept(), recv()等待數(shù)據(jù) (Waiting for the data to be ready)
● 將數(shù)據(jù)從內(nèi)核拷貝到進(jìn)程中, 比如 accept()接受到請求,recv()接收連接發(fā)送的數(shù)據(jù)后需要復(fù)制到內(nèi)核,再從內(nèi)核復(fù)制到進(jìn)程用戶空間(Copying the data from the kernel to the process)
對于socket流而言,數(shù)據(jù)的流向經(jīng)歷兩個階段:
● 第一步通常涉及等待網(wǎng)絡(luò)上的數(shù)據(jù)分組到達(dá),然后被復(fù)制到內(nèi)核的某個緩沖區(qū)合冀。
● 第二步把數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到應(yīng)用進(jìn)程緩沖區(qū)锄蹂。
IO multiplexing這個詞可能有點陌生,但是如果我說select/epoll水慨,大概就都能明白了得糜。有些地方也稱這種IO方式為事件驅(qū)動IO(event driven IO)。我們都知道晰洒,select/epoll的好處就在于單個process就可以同時處理多個網(wǎng)絡(luò)連接的IO朝抖。它的基本原理就是select/epoll這個function會不斷的輪詢所負(fù)責(zé)的所有socket,當(dāng)某個socket有數(shù)據(jù)到達(dá)了谍珊,就通知用戶進(jìn)程治宣。它的流程如圖:
當(dāng)用戶進(jìn)程調(diào)用了select,那么整個進(jìn)程會被block砌滞,而同時侮邀,kernel會“監(jiān)視”所有select負(fù)責(zé)的socket,當(dāng)任何一個socket中的數(shù)據(jù)準(zhǔn)備好了贝润,select就會返回绊茧。這個時候用戶進(jìn)程再調(diào)用read操作,將數(shù)據(jù)從kernel拷貝到用戶進(jìn)程打掘。
這個圖和blocking IO的圖其實并沒有太大的不同华畏,事實上還更差一些。因為這里需要使用兩個系統(tǒng)調(diào)用(select和recvfrom)尊蚁,而blocking IO只調(diào)用了一個系統(tǒng)調(diào)用(recvfrom)亡笑。但是,用select的優(yōu)勢在于它可以同時處理多個connection.
強(qiáng)調(diào):
1. 如果處理的連接數(shù)不是很高的話横朋,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好仑乌,可能延遲還更大。select/epoll的優(yōu)勢并不是對于單個連接能處理得更快琴锭,而是在于能處理更多的連接晰甚。
2. 在多路復(fù)用模型中,對于每一個socket祠够,一般都設(shè)置成為non-blocking压汪,但是,如上圖所示古瓤,整個用戶的process其實是一直被block的。只不過process是被select這個函數(shù)block,而不是被socket IO給block落君。
結(jié)論: select的優(yōu)勢在于可以處理多個連接穿香,不適用于單個連接。
#服務(wù)端
from socket import *
import select
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1',8093))
server.listen(5)
server.setblocking(False)
print('starting...')
rlist=[server,]
wlist=[]
wdata={}
while True:
rl,wl,xl=select.select(rlist,wlist,[],0.5)
print(wl)
for sock in rl:
if sock == server:
conn,addr=sock.accept()
rlist.append(conn)
else:
try:
data=sock.recv(1024)
if not data:
sock.close()
rlist.remove(sock)
continue
wlist.append(sock)
wdata[sock]=data.upper()
except Exception:
sock.close()
rlist.remove(sock)
for sock in wl:
sock.send(wdata[sock])
wlist.remove(sock)
wdata.pop(sock)
#客戶端
from socket import *
client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8093))
while True:
msg=input('>>: ').strip()
if not msg:continue
client.send(msg.encode('utf-8'))
data=client.recv(1024)
print(data.decode('utf-8'))
client.close()
select網(wǎng)絡(luò)IO模型
在IO multiplexing Model中绎速,
實際中皮获,對于每一個socket,一般都設(shè)置成為non-blocking纹冤,因為只有設(shè)置成non-blocking 才能使單個線程/進(jìn)程不被阻塞(或者說鎖兹鞅Α),可以繼續(xù)處理其他socket萌京。如上圖所示雁歌,整個用戶的process其實是一直被block的。只不過process是被select這個函數(shù)block知残,而不是被socket IO給block靠瞎。
select監(jiān)聽fd變化的過程分析:
用戶進(jìn)程創(chuàng)建socket對象,拷貝監(jiān)聽的fd到內(nèi)核空間求妹,每一個fd會對應(yīng)一張系統(tǒng)文件表乏盐,內(nèi)核空間的fd響應(yīng)到數(shù)據(jù)后,就會發(fā)送信號給用戶進(jìn)程數(shù)據(jù)已到制恍;
用戶進(jìn)程再發(fā)送系統(tǒng)調(diào)用父能,比如(accept)將內(nèi)核空間的數(shù)據(jù)copy到用戶空間,同時作為接受數(shù)據(jù)端內(nèi)核空間的數(shù)據(jù)清除净神,這樣重新監(jiān)聽時fd再有新的數(shù)據(jù)又可以響應(yīng)到了(發(fā)送端因為基于TCP協(xié)議所以需要收到應(yīng)答后才會清除)法竞。
所以, IO多路復(fù)用,本質(zhì)上不會有并發(fā)的功能强挫,因為任何時候還是只有一個進(jìn)程或線程進(jìn)行工作岔霸,它之所以能提高效率是因為select\epoll 把進(jìn)來的socket放到他們的 '監(jiān)視' 列表里面,當(dāng)任何socket有可讀可寫數(shù)據(jù)立馬處理俯渤,那如果select\epoll 手里同時檢測著很多socket呆细, 一有動靜馬上返回給進(jìn)程處理,總比一個一個socket過來,阻塞等待,處理高效率八匠。
該模型的優(yōu)點:
相比其他模型絮爷,使用select() 的事件驅(qū)動模型只用單線程(進(jìn)程)執(zhí)行,占用資源少梨树,不消耗太多 CPU坑夯,同時能夠為多客戶端提供服務(wù)。如果試圖建立一個簡單的事件驅(qū)動的服務(wù)器程序抡四,這個模型有一定的參考價值
該模型的缺點:
首先select()接口并不是實現(xiàn)“事件驅(qū)動”的最好選擇柜蜈。因為當(dāng)需要探測的句柄值較大時仗谆,select()接口本身需要消耗大量時間去輪詢各個句柄。很多操作系統(tǒng)提供了更為高效的接口淑履,如linux提供了epoll隶垮,BSD提供了kqueue,Solaris提供了/dev/poll秘噪,…狸吞。如果需要實現(xiàn)更高效的服務(wù)器程序,類似epoll這樣的接口更被推薦指煎。遺憾的是不同的操作系統(tǒng)特供的epoll接口有很大差異蹋偏,所以使用類似于epoll的接口實現(xiàn)具有較好跨平臺能力的服務(wù)器會比較困難。