Select 模型簡介

http://python.jobbole.com/84058/

多路複用I/O

簡明網絡I/O模型文章可以知道常用的IO模型。其中同步模型中,使用多路複用I/O可以提高服務器的性能。

在多路複用的模型中,比較常用的有select模型和poll模型。這兩個都是系統接口,由操作系統提供。當然,Pythonselect模塊進行了更高級的封裝。selectpoll的底層原理都差不多。下面就介紹select

select 原理

網絡通信被Unix系統抽象爲文件的讀寫,通常是一個設備,由設備驅動程序提供,驅動可以知道自身的數據是否可用。支持阻塞操作的設備驅動通常會實現一組自身的等待隊列,如讀/寫等待隊列用於支持上層(用戶層)所需的blocknon-block操作。設備的文件的資源如果可用(可讀或者可寫)則會通知進程,反之則會讓進程睡眠,等到數據到來可用的時候,再喚醒進程。

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

select 回顯服務器

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

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:

        # 調用 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信息。

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

1

2

3

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

server.bind((HOST, PORT))

server.listen(5)

上述代碼使用socket初始化一個TCP套接字,並綁定主機地址和端口,然後設置服務器監聽。

1

inputs = [server, sys.stdin]

這裏定義了一個需要select監聽的列表,列表裏面是需要監聽的對象(等於系統監聽的文件描述符)。這裏監聽socket套接字和用戶的輸入。

然後代碼進行一個服務器無線循環。

1

2

3

4

5

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有一個套接字對象(文件描述符可讀)。

1

2

3

4

5

6

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循環。如果沒有新的套接字,將會執行下面的代碼:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

try:

    # 讀取客戶端連接發送的數據

    data = sock.recv(BUFFER_SIZE)

    if data:

        sock.send(data)

        if data.endswith('rnrn'):

            # 移除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返回,最後也會執行下面的代碼:

1

2

3

elif sock == sys.stdin:

    junk = sys.stdin.readlines()

    running = False

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

任何時候,inputs監聽的對象有數據,下一次調用select的時候,就會繁返回readable,只要返回,就會對readable進行for循環,直到for循環結束在進行下一次select

主要注意,套接字建立連接是一次IO,連接的數據抵達也是一次IO

select的不足

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

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

許多高性能的軟件如nginxnodejs都是基於epoll進行的異步。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章