1. 程式人生 > >多路復用IO(了解)

多路復用IO(了解)

event lec 遺憾 接口 方式 recvfrom exce code 不一定

多路復用IO(IO multiplexing)

IO multiplexing這個詞可能有點陌生,但是如果我說select/epoll,大概就都能明白了。有些地方也稱這種IO方式為事件驅動IO

(event driven IO)。我們都知道,select/epoll的好處就在於單個process就可以同時處理多個網絡連接的IO。它的基本原理就是select/epoll這個function會不斷的輪詢所負責的所有socket,當某個socket有數據到達了,就通知用戶進程。它的流程如圖:

技術分享圖片

當用戶進程調用了select,那麽整個進程會被block,而同時,kernel會“監視”所有select負責的socket,
當任何一個socket中的數據準備好了,select就會返回。這個時候用戶進程再調用read操作,將數據從kernel拷貝到用戶進程。  
這個圖和blocking IO的圖其實並沒有太大的不同,事實上還更差一些。因為這裏需要使用兩個系統調用\(select和recvfrom\),
而blocking IO只調用了一個系統調用\(recvfrom\)。但是,用select的優勢在於它可以同時處理多個connection。

強調:

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的優勢在於可以處理多個連接,不適用於單個連接

select網絡IO模型示例

#服務端
import socket
import select

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("127.0.0.1", 8080))
server.listen(5)
server.setblocking(False)    # 默認是True為阻塞
print("starting.....")

rlist = [server, ]     # 專門存收消息的套接字(server和conn)
wlist = []     # 專門存發的套接字
wdata = {}

while True:
    rl, wl, xl = select.select(rlist, wlist, [], 0.5)     # []存放異常列表,0.5超時時間(每個0.5s問操作系統)
    print("rl", rl)
    print("wl", wl)

    for sock in rl:
        if sock == server:
            conn, addr = sock.accept()
            rlist.append(conn)
        else:
            try:
                data = sock.recv(1024)
                # if not data:       # 針對linux系統
                #     sock.close()
                #     rlist.remove()
                #     continue
                wlist.append(sock)
                wdata[sock] = data.upper()
            except Exception:
                sock.close()
                rl.remove(sock)

    for sock in wl:
        data = wdata[sock]
        sock.send(data)
        wlist.remove(sock)
        wdata.pop(sock)  
#客戶端 
import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(("127.0.0.1", 8080))

while True:
    msg = input(">>>:").strip()
    if not msg:
        continue
    client.send(msg.encode("utf-8"))
    data = client.recv(1024)
    print(data.decode("utf-8"))

select監聽fd變化的過程分析:

用戶進程創建socket對象,拷貝監聽的fd到內核空間,每一個fd會對應一張系統文件表,內核空間的fd響應到數據後,
就會發送信號給用戶進程數據已到;
用戶進程再發送系統調用,比如(accept)將內核空間的數據copy到用戶空間,同時作為接受數據端內核空間的數據清除,
這樣重新監聽時fd再有新的數據又可以響應到了(發送端因為基於TCP協議所以需要收到應答後才會清除)。

該模型的優點

相比其他模型,使用select() 的事件驅動模型只用單線程(進程)執行,占用資源少,不消耗太多 CPU,同時能夠為多客戶端提供服務。
如果試圖建立一個簡單的事件驅動的服務器程序,這個模型有一定的參考價值。

該模型的缺點

首先select()接口並不是實現“事件驅動”的最好選擇。因為當需要探測的句柄值較大時,select()接口本身需要消耗大量時間去輪詢各個句柄。
很多操作系統提供了更為高效的接口,如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll,…。
如果需要實現更高效的服務器程序,類似epoll這樣的接口更被推薦。遺憾的是不同的操作系統特供的epoll接口有很大差異,
所以使用類似於epoll的接口實現具有較好跨平臺能力的服務器會比較困難。
其次,該模型將事件探測和事件響應夾雜在一起,一旦事件響應的執行體龐大,則對整個模型是災難性的。

多路復用IO(了解)