總結網路IO模型與select模型的Python執行個體講解,python執行個體講解

來源:互聯網
上載者:User

總結網路IO模型與select模型的Python執行個體講解,python執行個體講解

網路I/O模型
人多了,就會有問題。web剛出現的時候,光顧的人很少。近年來網路應用規模逐漸擴大,應用的架構也需要隨之改變。C10k的問題,讓工程師們需要思考服務的效能與應用的並發能力。

網路應用需要處理的無非就是兩大類問題,網路I/O,資料計算。相對於後者,網路I/O的延遲,給應用帶來的效能瓶頸大於後者。網路I/O的模型大致有如下幾種:

  • 同步模型(synchronous I/O)
  • 阻塞I/O(bloking I/O)
  • 非阻塞I/O(non-blocking I/O)
  • 多工I/O(multiplexing I/O)
  • 訊號驅動式I/O(signal-driven I/O)
  • 非同步I/O(asynchronous I/O)

網路I/O的本質是socket的讀取,socket在linux系統被抽象為流,I/O可以理解為對流的操作。這個操作又分為兩個階段:

等待流資料準備(wating for the data to be ready)。
從核心向進程複製資料(copying the data from the kernel to the process)。
對於socket流而已,

第一步通常涉及等待網路上的資料分組到達,然後被複製到核心的某個緩衝區。
第二步把資料從核心緩衝區複製到應用進程緩衝區。
I/O模型:
舉個簡單比喻,來瞭解這幾種模型。網路IO好比釣魚,等待魚上鉤就是網路中等待資料準備好的過程,魚上鉤了,把魚拉上岸就是核心複製資料階段。釣魚的人就是一個應用進程。

阻塞I/O(bloking I/O)
阻塞I/O是最流行的I/O模型。它符合人們最常見的思考邏輯。阻塞就是進程 "被" 休息, CPU處理其它進程去了。在網路I/O的時候,進程發起recvform系統調用,然後進程就被阻塞了,什麼也不幹,直到資料準備好,並且將資料從核心複製到使用者進程,最後進程再處理資料,在等待資料到處理資料的兩個階段,整個進程都被阻塞。不能處理別的網路I/O。大致如:

這就好比我們去釣魚,拋竿之後就一直在岸邊等,直到等待魚上鉤。然後再一次拋竿,等待下一條魚上鉤,等待的時候,什麼事情也不做,大概會胡思亂想吧。

阻塞IO的特點就是在IO執行的兩個階段都被block了
非阻塞I/O(non-bloking I/O)
在網路I/O時候,非阻塞I/O也會進行recvform系統調用,檢查資料是否準備好,與阻塞I/O不一樣,"非阻塞將大的整片時間的阻塞分成N多的小的阻塞, 所以進程不斷地有機會 '被' CPU光顧"。

也就是說非阻塞的recvform系統調用調用之後,進程並沒有被阻塞,核心馬上返回給進程,如果資料還沒準備好,此時會返回一個error。進程在返回之後,可以幹點別的事情,然後再發起recvform系統調用。重複上面的過程,迴圈往複的進行recvform系統調用。這個過程通常被稱之為輪詢。輪詢檢查核心資料,直到資料準備好,再拷貝資料到進程,進行資料處理。需要注意,拷貝資料整個過程,進程仍然是屬於阻塞的狀態。

我們再用釣魚的方式來類別,當我們拋竿入水之後,就看下魚漂是否有動靜,如果沒有魚上鉤,就去幹點別的事情,比如再挖幾條蚯蚓。然後不久又來看看魚漂是否有魚上鉤。這樣往返的檢查又離開,直到魚上鉤,再進行處理。

非阻塞 IO的特點是使用者進程需要不斷的主動詢問kernel資料是否準備好。
多工I/O(multiplexing I/O)
可以看出,由於非阻塞的調用,輪詢佔據了很大一部分過程,輪詢會消耗大量的CPU時間。結合前面兩種模式。如果輪詢不是進程的使用者態,而是有人幫忙就好了。多工正好處理這樣的問題。

多工有兩個特別的系統調用select或poll。select調用是核心層級的,select輪詢相對非阻塞的輪詢的區別在於---前者可以等待多個socket,當其中任何一個socket的資料准好了,就能返回進行可讀,然後進程再進行recvform系統調用,將資料由核心拷貝到使用者進程,當然這個過程是阻塞的。多工有兩種阻塞,select或poll調用之後,會阻塞進程,與第一種阻塞不同在於,此時的select不是等到socket資料全部到達再處理, 而是有了一部分資料就會調用使用者進程來處理。如何知道有一部分資料到達了呢?監視的事情交給了核心,核心負責資料到達的處理。也可以理解為"非阻塞"吧。

對於多工,也就是輪詢多個socket。釣魚的時候,我們雇了一個幫手,他可以同時拋下多個釣魚竿,任何一杆的魚一上鉤,他就會拉杆。他只負責幫我們釣魚,並不會幫我們處理,所以我們還得在一幫等著,等他把收杆。我們再處理魚。多工既然可以處理多個I/O,也就帶來了新的問題,多個I/O之間的順序變得不確定了,當然也可以針對不同的編號。

多工特點是通過一種機制一個進程能同時等待IO檔案描述符,核心監視這些檔案描述符(通訊端描述符),其中的任意一個進入讀就緒狀態,select, poll,epoll函數就可以返回。對於監視的方式,又可以分為 select, poll, epoll三種方式。
瞭解了前面三種模式,在使用者進程進行系統調用的時候,他們在等待資料到來的時候,處理的方式不一樣,直接等待,輪詢,select或poll輪詢,第一個過程有的阻塞,有的不阻塞,有的可以阻塞又可以不阻塞。當時第二個過程都是阻塞的。從整個I/O過程來看,他們都是順序執行的,因此可以歸為同步模型(asynchronous)。都是進程主動向核心檢查。

非同步I/O(asynchronous I/O)
相對於同步I/O,非同步I/O不是順序執行。使用者進程進行aio_read系統調用之後,無論核心資料是否準備好,都會直接返回給使用者進程,然後使用者態進程可以去做別的事情。等到socket資料準備好了,核心直接複製資料給進程,然後從核心向進程發送通知。I/O兩個階段,進程都是非阻塞的。

比之前的釣魚方式不一樣,這一次我們雇了一個釣魚高手。他不僅會釣魚,還會在魚上鉤之後給我們發簡訊,通知我們魚已經準備好了。我們只要委託他去拋竿,然後就能跑去幹別的事情了,直到他的簡訊。我們再回來處理已經上岸的魚。

同步和非同步區別
通過對上述幾種模型的討論,需要區分阻塞和非阻塞,同步和非同步。他們其實是兩組概念。區別前一組比較容易,後一種往往容易和前面混合。在我看來,所謂同步就是在整個I/O過程。尤其是拷貝資料的過程是阻塞進程的,並且都是應用進程態去檢查核心態。而非同步則是整個過程I/O過程使用者進程都是非阻塞的,並且當拷貝資料的時是由核心發送通知給使用者進程。

對於同步模型,主要是第一階段處理方法不一樣。而非同步模型,兩個階段都不一樣。這裡我們忽略了訊號驅動模式。這幾個名詞還是容易讓人迷惑,只有同步模型才考慮阻塞和非阻塞,因為非同步肯定是非阻塞,非同步非阻塞的說法感覺畫蛇添足。


Select 模型
同步模型中,使用多工I/O可以提高伺服器的效能。
在多工模型中,比較常用的有select模型和poll模型。這兩個都是系統介面,由作業系統提供。當然,Python的select模組進行了更進階的封裝。select與poll的底層原理都差不多。千呼萬喚始出來,本文的重點select模型。
1.select 原理
網路通訊被Unix系統抽象為檔案的讀寫,通常是一個裝置,由裝置驅動程式提供,驅動可以知道自身的資料是否可用。支援阻塞操作的裝置驅動通常會實現一組自身的等待隊列,如讀/寫等待隊列用於支援上層(使用者層)所需的block或non-block操作。裝置的檔案的資源如果可用(可讀或者可寫)則會通知進程,反之則會讓進程睡眠,等到資料到來可用的時候,再喚醒進程。

這些裝置的檔案描述符被放在一個數組中,然後select調用的時候遍曆這個數組,如果對於的檔案描述符可讀則會返回改檔案描述符。當遍曆結束之後,如果仍然沒有一個可用裝置檔案描述符,select讓使用者進程則會睡眠,直到等待資源可用的時候在喚醒,遍曆之前那個監視的數組。每次遍曆都是線性。

2.select 回顯伺服器
select涉及系統調用和作業系統相關的知識,因此單從字面上理解其原理還是比較乏味。用代碼來示範最好不過了。使用python的select模組很容易寫出下面一個回顯伺服器:

import selectimport socketimport sysHOST = 'localhost'PORT = 5000BUFFER_SIZE = 1024server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)server.bind((HOST, PORT))server.listen(5)inputs = [server, sys.stdin]running = Truewhile True:  try:    # 調用 select 函數,阻塞等待    readable, writeable, exceptional = select.select(inputs, [], [])  except select.error, e:    break  # 資料抵達,迴圈  for sock in readable:    # 建立串連    if sock == server:      conn, addr = server.accept()      # select 監聽的socket      inputs.append(conn)    elif sock == sys.stdin:      junk = sys.stdin.readlines()      running = False    else:      try:        # 讀取用戶端串連發送的資料        data = sock.recv(BUFFER_SIZE)        if data:          sock.send(data)          if data.endswith('\r\n\r\n'):            # 移除select監聽的socket            inputs.remove(sock)            sock.close()        else:          # 移除select監聽的socket          inputs.remove(sock)          sock.close()      except socket.error, e:        inputs.remove(sock)server.close()

運行上述代碼,使用curl訪問http://localhost:5000,即可看命令列返回請求的HTTP request資訊。

下面詳細解析上述代碼的原理。

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)server.bind((HOST, PORT))server.listen(5)

上述代碼使用socket初始化一個TCP通訊端,並綁定主機地址和連接埠,然後設定伺服器監聽。

inputs = [server, sys.stdin]

這裡定義了一個需要select監聽的列表,列表裡面是需要監聽的對象(等於系統監聽的檔案描述符)。這裡監聽socket通訊端和使用者的輸入。

然後代碼進行一個伺服器無線迴圈。

try:  # 調用 select 函數,阻塞等待  readable, writeable, exceptional = select.select(inputs, [], [])except select.error, e:  break

調用了select函數,開始迴圈遍曆監聽傳入的列表inputs。如果沒有curl伺服器,此時沒有建立tcp用戶端串連,因此改列表內的對象都是資料資源不可用。因此select阻塞不返回。

用戶端輸入curl http://localhost:5000之後,一個通訊端通訊開始,此時input中的第一個對象server由不可用變成可用。因此select函數調用返回,此時的readable有一個通訊端對象(檔案描述符可讀)。

for sock in readable:  # 建立串連  if sock == server:    conn, addr = server.accept()    # select 監聽的socket    inputs.append(conn)

select返回之後,接下來遍曆可讀的檔案對象,此時的可讀中只有一個通訊端串連,調用通訊端的accept()方法建立TCP三向交握的串連,然後把該連線物件追加到inputs監視列表中,表示我們要監視該串連是否有資料IO操作。

由於此時readable只有一個可用的對象,因此遍曆結束。再回到主迴圈,再次調用select,此時調用的時候,不僅會遍曆監視是否有新的串連需要建立,還是監視剛才追加的串連。如果curl的資料到了,select再返回到readable,此時在進行for迴圈。如果沒有新的通訊端,將會執行下面的代碼:

try:  # 讀取用戶端串連發送的資料  data = sock.recv(BUFFER_SIZE)  if data:    sock.send(data)    if data.endswith('\r\n\r\n'):      # 移除select監聽的socket      inputs.remove(sock)      sock.close()  else:    # 移除select監聽的socket    inputs.remove(sock)    sock.close()except socket.error, e:  inputs.remove(sock)

通過通訊端串連調用recv函數,擷取用戶端發送的資料,當資料轉送完畢,再把監視的inputs列表中除去該串連。然後關閉串連。

整個網路互動過程就是如此,當然這裡如果使用者在命令列中輸入中斷,inputs列表中監視的sys.stdin也會讓select返回,最後也會執行下面的代碼:

elif sock == sys.stdin:  junk = sys.stdin.readlines()  running = False

有人可能有疑問,在程式處理sock串連的是時候,假設又輸入了curl對伺服器請求,將會怎麼辦?此時毫無疑問,inputs裡面的server通訊端會變成可用。等現在的for迴圈處理完畢,此時select調用就會返回server。如果inputs裡面還有上一個過程的conn串連,那麼也會迴圈遍曆inputs的時候,再一次針對新的通訊端accept到inputs列表進行監視,然後繼續迴圈處理之前的conn串連。如此有條不紊的進行,直到for迴圈結束,進入主迴圈調用select。

任何時候,inputs監聽的對象有資料,下一次調用select的時候,就會繁返回readable,只要返回,就會對readable進行for迴圈,直到for迴圈結束在進行下一次select。

主要注意,通訊端建立串連是一次IO,串連的資料抵達也是一次IO。

3.select的不足
儘管select用起來挺爽,跨平台的特性。但是select還是存在一些問題。
select需要遍曆監視的檔案描述符,並且這個描述符的數組還有最大的限制。隨著檔案描述符數量的增長,使用者態和核心的地址空間的複製所引發的開銷也會線性增長。即使監視的檔案描述符長時間不活躍了,select還是會線性掃描。

為瞭解決這些問題,作業系統又提供了poll方案,但是poll的模型和select大致相當,只是改變了一些限制。目前Linux最先進的方式是epoll模型。

許多高效能的軟體如nginx, nodejs都是基於epoll進行的非同步。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.