總結(jié)網(wǎng)絡(luò)IO模型與select模型的Python實(shí)例講解
網(wǎng)絡(luò)I/O模型
人多了,就會(huì)有問(wèn)題。web剛出現(xiàn)的時(shí)候,光顧的人很少。近年來(lái)網(wǎng)絡(luò)應(yīng)用規(guī)模逐漸擴(kuò)大,應(yīng)用的架構(gòu)也需要隨之改變。C10k的問(wèn)題,讓工程師們需要思考服務(wù)的性能與應(yīng)用的并發(fā)能力。
網(wǎng)絡(luò)應(yīng)用需要處理的無(wú)非就是兩大類(lèi)問(wèn)題,網(wǎng)絡(luò)I/O,數(shù)據(jù)計(jì)算。相對(duì)于后者,網(wǎng)絡(luò)I/O的延遲,給應(yīng)用帶來(lái)的性能瓶頸大于后者。網(wǎng)絡(luò)I/O的模型大致有如下幾種:
- 同步模型(synchronous I/O)
- 阻塞I/O(bloking I/O)
- 非阻塞I/O(non-blocking I/O)
- 多路復(fù)用I/O(multiplexing I/O)
- 信號(hào)驅(qū)動(dòng)式I/O(signal-driven I/O)
- 異步I/O(asynchronous I/O)
網(wǎng)絡(luò)I/O的本質(zhì)是socket的讀取,socket在linux系統(tǒng)被抽象為流,I/O可以理解為對(duì)流的操作。這個(gè)操作又分為兩個(gè)階段:
等待流數(shù)據(jù)準(zhǔn)備(wating for the data to be ready)。
從內(nèi)核向進(jìn)程復(fù)制數(shù)據(jù)(copying the data from the kernel to the process)。
對(duì)于socket流而已,
第一步通常涉及等待網(wǎng)絡(luò)上的數(shù)據(jù)分組到達(dá),然后被復(fù)制到內(nèi)核的某個(gè)緩沖區(qū)。
第二步把數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到應(yīng)用進(jìn)程緩沖區(qū)。
I/O模型:
舉個(gè)簡(jiǎn)單比喻,來(lái)了解這幾種模型。網(wǎng)絡(luò)IO好比釣魚(yú),等待魚(yú)上鉤就是網(wǎng)絡(luò)中等待數(shù)據(jù)準(zhǔn)備好的過(guò)程,魚(yú)上鉤了,把魚(yú)拉上岸就是內(nèi)核復(fù)制數(shù)據(jù)階段。釣魚(yú)的人就是一個(gè)應(yīng)用進(jìn)程。
阻塞I/O(bloking I/O)
阻塞I/O是最流行的I/O模型。它符合人們最常見(jiàn)的思考邏輯。阻塞就是進(jìn)程 "被" 休息, CPU處理其它進(jìn)程去了。在網(wǎng)絡(luò)I/O的時(shí)候,進(jìn)程發(fā)起recvform系統(tǒng)調(diào)用,然后進(jìn)程就被阻塞了,什么也不干,直到數(shù)據(jù)準(zhǔn)備好,并且將數(shù)據(jù)從內(nèi)核復(fù)制到用戶(hù)進(jìn)程,最后進(jìn)程再處理數(shù)據(jù),在等待數(shù)據(jù)到處理數(shù)據(jù)的兩個(gè)階段,整個(gè)進(jìn)程都被阻塞。不能處理別的網(wǎng)絡(luò)I/O。大致如下圖:
這就好比我們?nèi)メ烎~(yú),拋竿之后就一直在岸邊等,直到等待魚(yú)上鉤。然后再一次拋竿,等待下一條魚(yú)上鉤,等待的時(shí)候,什么事情也不做,大概會(huì)胡思亂想吧。
阻塞IO的特點(diǎn)就是在IO執(zhí)行的兩個(gè)階段都被block了
非阻塞I/O(non-bloking I/O)
在網(wǎng)絡(luò)I/O時(shí)候,非阻塞I/O也會(huì)進(jìn)行recvform系統(tǒng)調(diào)用,檢查數(shù)據(jù)是否準(zhǔn)備好,與阻塞I/O不一樣,"非阻塞將大的整片時(shí)間的阻塞分成N多的小的阻塞, 所以進(jìn)程不斷地有機(jī)會(huì) '被' CPU光顧"。
也就是說(shuō)非阻塞的recvform系統(tǒng)調(diào)用調(diào)用之后,進(jìn)程并沒(méi)有被阻塞,內(nèi)核馬上返回給進(jìn)程,如果數(shù)據(jù)還沒(méi)準(zhǔn)備好,此時(shí)會(huì)返回一個(gè)error。進(jìn)程在返回之后,可以干點(diǎn)別的事情,然后再發(fā)起recvform系統(tǒng)調(diào)用。重復(fù)上面的過(guò)程,循環(huán)往復(fù)的進(jìn)行recvform系統(tǒng)調(diào)用。這個(gè)過(guò)程通常被稱(chēng)之為輪詢(xún)。輪詢(xún)檢查內(nèi)核數(shù)據(jù),直到數(shù)據(jù)準(zhǔn)備好,再拷貝數(shù)據(jù)到進(jìn)程,進(jìn)行數(shù)據(jù)處理。需要注意,拷貝數(shù)據(jù)整個(gè)過(guò)程,進(jìn)程仍然是屬于阻塞的狀態(tài)。
我們?cè)儆冕烎~(yú)的方式來(lái)類(lèi)別,當(dāng)我們拋竿入水之后,就看下魚(yú)漂是否有動(dòng)靜,如果沒(méi)有魚(yú)上鉤,就去干點(diǎn)別的事情,比如再挖幾條蚯蚓。然后不久又來(lái)看看魚(yú)漂是否有魚(yú)上鉤。這樣往返的檢查又離開(kāi),直到魚(yú)上鉤,再進(jìn)行處理。
非阻塞 IO的特點(diǎn)是用戶(hù)進(jìn)程需要不斷的主動(dòng)詢(xún)問(wèn)kernel數(shù)據(jù)是否準(zhǔn)備好。
多路復(fù)用I/O(multiplexing I/O)
可以看出,由于非阻塞的調(diào)用,輪詢(xún)占據(jù)了很大一部分過(guò)程,輪詢(xún)會(huì)消耗大量的CPU時(shí)間。結(jié)合前面兩種模式。如果輪詢(xún)不是進(jìn)程的用戶(hù)態(tài),而是有人幫忙就好了。多路復(fù)用正好處理這樣的問(wèn)題。
多路復(fù)用有兩個(gè)特別的系統(tǒng)調(diào)用select或poll。select調(diào)用是內(nèi)核級(jí)別的,select輪詢(xún)相對(duì)非阻塞的輪詢(xún)的區(qū)別在于---前者可以等待多個(gè)socket,當(dāng)其中任何一個(gè)socket的數(shù)據(jù)準(zhǔn)好了,就能返回進(jìn)行可讀,然后進(jìn)程再進(jìn)行recvform系統(tǒng)調(diào)用,將數(shù)據(jù)由內(nèi)核拷貝到用戶(hù)進(jìn)程,當(dāng)然這個(gè)過(guò)程是阻塞的。多路復(fù)用有兩種阻塞,select或poll調(diào)用之后,會(huì)阻塞進(jìn)程,與第一種阻塞不同在于,此時(shí)的select不是等到socket數(shù)據(jù)全部到達(dá)再處理, 而是有了一部分?jǐn)?shù)據(jù)就會(huì)調(diào)用用戶(hù)進(jìn)程來(lái)處理。如何知道有一部分?jǐn)?shù)據(jù)到達(dá)了呢?監(jiān)視的事情交給了內(nèi)核,內(nèi)核負(fù)責(zé)數(shù)據(jù)到達(dá)的處理。也可以理解為"非阻塞"吧。
對(duì)于多路復(fù)用,也就是輪詢(xún)多個(gè)socket。釣魚(yú)的時(shí)候,我們雇了一個(gè)幫手,他可以同時(shí)拋下多個(gè)釣魚(yú)竿,任何一桿的魚(yú)一上鉤,他就會(huì)拉桿。他只負(fù)責(zé)幫我們釣魚(yú),并不會(huì)幫我們處理,所以我們還得在一幫等著,等他把收桿。我們?cè)偬幚眙~(yú)。多路復(fù)用既然可以處理多個(gè)I/O,也就帶來(lái)了新的問(wèn)題,多個(gè)I/O之間的順序變得不確定了,當(dāng)然也可以針對(duì)不同的編號(hào)。
多路復(fù)用的特點(diǎn)是通過(guò)一種機(jī)制一個(gè)進(jìn)程能同時(shí)等待IO文件描述符,內(nèi)核監(jiān)視這些文件描述符(套接字描述符),其中的任意一個(gè)進(jìn)入讀就緒狀態(tài),select, poll,epoll函數(shù)就可以返回。對(duì)于監(jiān)視的方式,又可以分為 select, poll, epoll三種方式。
了解了前面三種模式,在用戶(hù)進(jìn)程進(jìn)行系統(tǒng)調(diào)用的時(shí)候,他們?cè)诘却龜?shù)據(jù)到來(lái)的時(shí)候,處理的方式不一樣,直接等待,輪詢(xún),select或poll輪詢(xún),第一個(gè)過(guò)程有的阻塞,有的不阻塞,有的可以阻塞又可以不阻塞。當(dāng)時(shí)第二個(gè)過(guò)程都是阻塞的。從整個(gè)I/O過(guò)程來(lái)看,他們都是順序執(zhí)行的,因此可以歸為同步模型(asynchronous)。都是進(jìn)程主動(dòng)向內(nèi)核檢查。
異步I/O(asynchronous I/O)
相對(duì)于同步I/O,異步I/O不是順序執(zhí)行。用戶(hù)進(jìn)程進(jìn)行aio_read系統(tǒng)調(diào)用之后,無(wú)論內(nèi)核數(shù)據(jù)是否準(zhǔn)備好,都會(huì)直接返回給用戶(hù)進(jìn)程,然后用戶(hù)態(tài)進(jìn)程可以去做別的事情。等到socket數(shù)據(jù)準(zhǔn)備好了,內(nèi)核直接復(fù)制數(shù)據(jù)給進(jìn)程,然后從內(nèi)核向進(jìn)程發(fā)送通知。I/O兩個(gè)階段,進(jìn)程都是非阻塞的。
比之前的釣魚(yú)方式不一樣,這一次我們雇了一個(gè)釣魚(yú)高手。他不僅會(huì)釣魚(yú),還會(huì)在魚(yú)上鉤之后給我們發(fā)短信,通知我們魚(yú)已經(jīng)準(zhǔn)備好了。我們只要委托他去拋竿,然后就能跑去干別的事情了,直到他的短信。我們?cè)倩貋?lái)處理已經(jīng)上岸的魚(yú)。
同步和異步的區(qū)別
通過(guò)對(duì)上述幾種模型的討論,需要區(qū)分阻塞和非阻塞,同步和異步。他們其實(shí)是兩組概念。區(qū)別前一組比較容易,后一種往往容易和前面混合。在我看來(lái),所謂同步就是在整個(gè)I/O過(guò)程。尤其是拷貝數(shù)據(jù)的過(guò)程是阻塞進(jìn)程的,并且都是應(yīng)用進(jìn)程態(tài)去檢查內(nèi)核態(tài)。而異步則是整個(gè)過(guò)程I/O過(guò)程用戶(hù)進(jìn)程都是非阻塞的,并且當(dāng)拷貝數(shù)據(jù)的時(shí)是由內(nèi)核發(fā)送通知給用戶(hù)進(jìn)程。
對(duì)于同步模型,主要是第一階段處理方法不一樣。而異步模型,兩個(gè)階段都不一樣。這里我們忽略了信號(hào)驅(qū)動(dòng)模式。這幾個(gè)名詞還是容易讓人迷惑,只有同步模型才考慮阻塞和非阻塞,因?yàn)楫惒娇隙ㄊ欠亲枞?,異步非阻塞的說(shuō)法感覺(jué)畫(huà)蛇添足。
Select 模型
同步模型中,使用多路復(fù)用I/O可以提高服務(wù)器的性能。
在多路復(fù)用的模型中,比較常用的有select模型和poll模型。這兩個(gè)都是系統(tǒng)接口,由操作系統(tǒng)提供。當(dāng)然,Python的select模塊進(jìn)行了更高級(jí)的封裝。select與poll的底層原理都差不多。千呼萬(wàn)喚始出來(lái),本文的重點(diǎn)select模型。
1.select 原理
網(wǎng)絡(luò)通信被Unix系統(tǒng)抽象為文件的讀寫(xiě),通常是一個(gè)設(shè)備,由設(shè)備驅(qū)動(dòng)程序提供,驅(qū)動(dòng)可以知道自身的數(shù)據(jù)是否可用。支持阻塞操作的設(shè)備驅(qū)動(dòng)通常會(huì)實(shí)現(xiàn)一組自身的等待隊(duì)列,如讀/寫(xiě)等待隊(duì)列用于支持上層(用戶(hù)層)所需的block或non-block操作。設(shè)備的文件的資源如果可用(可讀或者可寫(xiě))則會(huì)通知進(jìn)程,反之則會(huì)讓進(jìn)程睡眠,等到數(shù)據(jù)到來(lái)可用的時(shí)候,再喚醒進(jìn)程。
這些設(shè)備的文件描述符被放在一個(gè)數(shù)組中,然后select調(diào)用的時(shí)候遍歷這個(gè)數(shù)組,如果對(duì)于的文件描述符可讀則會(huì)返回改文件描述符。當(dāng)遍歷結(jié)束之后,如果仍然沒(méi)有一個(gè)可用設(shè)備文件描述符,select讓用戶(hù)進(jìn)程則會(huì)睡眠,直到等待資源可用的時(shí)候在喚醒,遍歷之前那個(gè)監(jiān)視的數(shù)組。每次遍歷都是線(xiàn)性的。
2.select 回顯服務(wù)器
select涉及系統(tǒng)調(diào)用和操作系統(tǒng)相關(guān)的知識(shí),因此單從字面上理解其原理還是比較乏味。用代碼來(lái)演示最好不過(guò)了。使用python的select模塊很容易寫(xiě)出下面一個(gè)回顯服務(wù)器:
import select import socket import sys HOST = 'localhost' PORT = 5000 BUFFER_SIZE = 1024 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind((HOST, PORT)) server.listen(5) inputs = [server, sys.stdin] running = True while True: try: # 調(diào)用 select 函數(shù),阻塞等待 readable, writeable, exceptional = select.select(inputs, [], []) except select.error, e: break # 數(shù)據(jù)抵達(dá),循環(huán) for sock in readable: # 建立連接 if sock == server: conn, addr = server.accept() # select 監(jiān)聽(tīng)的socket inputs.append(conn) elif sock == sys.stdin: junk = sys.stdin.readlines() running = False else: try: # 讀取客戶(hù)端連接發(fā)送的數(shù)據(jù) data = sock.recv(BUFFER_SIZE) if data: sock.send(data) if data.endswith('\r\n\r\n'): # 移除select監(jiān)聽(tīng)的socket inputs.remove(sock) sock.close() else: # 移除select監(jiān)聽(tīng)的socket inputs.remove(sock) sock.close() except socket.error, e: inputs.remove(sock) server.close()
運(yùn)行上述代碼,使用curl訪(fǎng)問(wèn)http://localhost:5000,即可看命令行返回請(qǐng)求的HTTP request信息。
下面詳細(xì)解析上述代碼的原理。
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind((HOST, PORT)) server.listen(5)
上述代碼使用socket初始化一個(gè)TCP套接字,并綁定主機(jī)地址和端口,然后設(shè)置服務(wù)器監(jiān)聽(tīng)。
inputs = [server, sys.stdin]
這里定義了一個(gè)需要select監(jiān)聽(tīng)的列表,列表里面是需要監(jiān)聽(tīng)的對(duì)象(等于系統(tǒng)監(jiān)聽(tīng)的文件描述符)。這里監(jiān)聽(tīng)socket套接字和用戶(hù)的輸入。
然后代碼進(jìn)行一個(gè)服務(wù)器無(wú)線(xiàn)循環(huán)。
try: # 調(diào)用 select 函數(shù),阻塞等待 readable, writeable, exceptional = select.select(inputs, [], []) except select.error, e: break
調(diào)用了select函數(shù),開(kāi)始循環(huán)遍歷監(jiān)聽(tīng)傳入的列表inputs。如果沒(méi)有curl服務(wù)器,此時(shí)沒(méi)有建立tcp客戶(hù)端連接,因此改列表內(nèi)的對(duì)象都是數(shù)據(jù)資源不可用。因此select阻塞不返回。
客戶(hù)端輸入curl http://localhost:5000之后,一個(gè)套接字通信開(kāi)始,此時(shí)input中的第一個(gè)對(duì)象server由不可用變成可用。因此select函數(shù)調(diào)用返回,此時(shí)的readable有一個(gè)套接字對(duì)象(文件描述符可讀)。
for sock in readable: # 建立連接 if sock == server: conn, addr = server.accept() # select 監(jiān)聽(tīng)的socket inputs.append(conn)
select返回之后,接下來(lái)遍歷可讀的文件對(duì)象,此時(shí)的可讀中只有一個(gè)套接字連接,調(diào)用套接字的accept()方法建立TCP三次握手的連接,然后把該連接對(duì)象追加到inputs監(jiān)視列表中,表示我們要監(jiān)視該連接是否有數(shù)據(jù)IO操作。
由于此時(shí)readable只有一個(gè)可用的對(duì)象,因此遍歷結(jié)束。再回到主循環(huán),再次調(diào)用select,此時(shí)調(diào)用的時(shí)候,不僅會(huì)遍歷監(jiān)視是否有新的連接需要建立,還是監(jiān)視剛才追加的連接。如果curl的數(shù)據(jù)到了,select再返回到readable,此時(shí)在進(jìn)行for循環(huán)。如果沒(méi)有新的套接字,將會(huì)執(zhí)行下面的代碼:
try: # 讀取客戶(hù)端連接發(fā)送的數(shù)據(jù) data = sock.recv(BUFFER_SIZE) if data: sock.send(data) if data.endswith('\r\n\r\n'): # 移除select監(jiān)聽(tīng)的socket inputs.remove(sock) sock.close() else: # 移除select監(jiān)聽(tīng)的socket inputs.remove(sock) sock.close() except socket.error, e: inputs.remove(sock)
通過(guò)套接字連接調(diào)用recv函數(shù),獲取客戶(hù)端發(fā)送的數(shù)據(jù),當(dāng)數(shù)據(jù)傳輸完畢,再把監(jiān)視的inputs列表中除去該連接。然后關(guān)閉連接。
整個(gè)網(wǎng)絡(luò)交互過(guò)程就是如此,當(dāng)然這里如果用戶(hù)在命令行中輸入中斷,inputs列表中監(jiān)視的sys.stdin也會(huì)讓select返回,最后也會(huì)執(zhí)行下面的代碼:
elif sock == sys.stdin: junk = sys.stdin.readlines() running = False
有人可能有疑問(wèn),在程序處理sock連接的是時(shí)候,假設(shè)又輸入了curl對(duì)服務(wù)器請(qǐng)求,將會(huì)怎么辦?此時(shí)毫無(wú)疑問(wèn),inputs里面的server套接字會(huì)變成可用。等現(xiàn)在的for循環(huán)處理完畢,此時(shí)select調(diào)用就會(huì)返回server。如果inputs里面還有上一個(gè)過(guò)程的conn連接,那么也會(huì)循環(huán)遍歷inputs的時(shí)候,再一次針對(duì)新的套接字accept到inputs列表進(jìn)行監(jiān)視,然后繼續(xù)循環(huán)處理之前的conn連接。如此有條不紊的進(jìn)行,直到for循環(huán)結(jié)束,進(jìn)入主循環(huán)調(diào)用select。
任何時(shí)候,inputs監(jiān)聽(tīng)的對(duì)象有數(shù)據(jù),下一次調(diào)用select的時(shí)候,就會(huì)繁返回readable,只要返回,就會(huì)對(duì)readable進(jìn)行for循環(huán),直到for循環(huán)結(jié)束在進(jìn)行下一次select。
主要注意,套接字建立連接是一次IO,連接的數(shù)據(jù)抵達(dá)也是一次IO。
3.select的不足
盡管select用起來(lái)挺爽,跨平臺(tái)的特性。但是select還是存在一些問(wèn)題。
select需要遍歷監(jiān)視的文件描述符,并且這個(gè)描述符的數(shù)組還有最大的限制。隨著文件描述符數(shù)量的增長(zhǎng),用戶(hù)態(tài)和內(nèi)核的地址空間的復(fù)制所引發(fā)的開(kāi)銷(xiāo)也會(huì)線(xiàn)性增長(zhǎng)。即使監(jiān)視的文件描述符長(zhǎng)時(shí)間不活躍了,select還是會(huì)線(xiàn)性?huà)呙琛?/p>
為了解決這些問(wèn)題,操作系統(tǒng)又提供了poll方案,但是poll的模型和select大致相當(dāng),只是改變了一些限制。目前Linux最先進(jìn)的方式是epoll模型。
許多高性能的軟件如nginx, nodejs都是基于epoll進(jìn)行的異步。
相關(guān)文章
Python對(duì)小數(shù)進(jìn)行除法運(yùn)算的正確方法示例
這篇文章主要介紹了Python對(duì)小數(shù)進(jìn)行除法運(yùn)算的正確方法示例,正確的方法是需要轉(zhuǎn)換成浮點(diǎn)數(shù),否則永遠(yuǎn)不會(huì)得到正確結(jié)果,需要的朋友可以參考下2014-08-08django 發(fā)送郵件和緩存的實(shí)現(xiàn)代碼
這篇文章主要介紹了django 發(fā)送郵件和緩存的實(shí)現(xiàn)代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-07-07Python 利用高德地圖api實(shí)現(xiàn)經(jīng)緯度與地址的批量轉(zhuǎn)換
這篇文章主要介紹了Python 利用高德地圖api實(shí)現(xiàn)經(jīng)緯度與地址的批量轉(zhuǎn)換,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08Python 機(jī)器學(xué)習(xí)工具包SKlearn的安裝與使用
Sklearn(全稱(chēng) SciKit-Learn),是基于 Python 語(yǔ)言的機(jī)器學(xué)習(xí)工具包。本文將簡(jiǎn)單的介紹SKlearn安裝與使用,想要入坑機(jī)器學(xué)習(xí)的同學(xué)可以參考下2021-05-05解決django中ModelForm多表單組合的問(wèn)題
今天小編就為大家分享一篇解決django中ModelForm多表單組合的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-07-07