I/O多工
I/O多工是用於提升效率,單個進程可以同時監聽多個網路連接IO
I/O是指Input/Output
I/O多工,通過一種機制,可以監視多個檔案描述符,一旦描述符就緒(讀就緒和寫就緒),能通知程式進行相應的讀寫操作。
I/O多工避免阻塞在io上,原本為多進程或多線程來接收多個串連的訊息變為單進程或單線程儲存多個socket的狀態後輪詢處理. select
select是通過系統調用來監視一組由多個檔案描述符組成的數組,通過調用select()返回結果,數組中就緒的檔案描述符會被核心標記出來,然後進程就可以獲得這些檔案描述符,然後進行相應的讀寫操作
select的實際執行過程如下:
select需要提供要監控的數組,然後由使用者態拷貝到核心態
核心態線性迴圈監控數組,每次都需要遍曆整個數組
核心發現檔案描述符狀態符合操作結果,將其返回
所以對於我們監控的socket都要設定為非阻塞的,只有這樣才能保證不會被阻塞 優點
基本各個平台都支援 缺點
每次調用select,都需要把fd集合由使用者態拷貝到核心態,在fd多的時候開銷會很大
單個進程能夠監控的fd數量存在最大限制,因為其使用的資料結構是數組。
每次select都是線性遍曆整個數組,當fd很大的時候,遍曆的開銷也很大 python使用select
r, w, e = select.select( rlist, wlist, errlist [,timeout] )
rlist,wlist和errlist均是waitable object; 都是檔案描述符,就是一個整數,或者一個擁有返迴文件描述符的函數fileno()的對象。
rlist: 等待讀就緒的檔案描述符數組
wlist: 等待寫就緒的檔案描述符數組
errlist: 等待異常的數組
在linux下這三個列表可以是空列表,但是在windows上不行
當rlist數組中的檔案描述符發生可讀時(調用accept或者read函數),則擷取檔案描述符並添加到r數組中。
當wlist數組中的檔案描述符發生可寫時,則擷取檔案描述符添加到w數組中
當errlist數組中的檔案描述符發生錯誤時,將會將檔案描述符添加到e隊列中
當逾時時間沒有設定時,如果監聽的檔案描述符沒有任何變化,將會一直阻塞到發生變化為止
當逾時時間設定為1時,如果監聽的描述符沒有變化,則select會阻塞1秒,之後返回三個空列表。 如果由變化,則直接執行並返回。
3個list中可接收的參數,可以是Python的file對象,例如sys.stdin,os.open,open返回的對象等等。socket對象將會返回socket.socket(),也可以自訂類,只要由合適的fileno函數即可,前提是真實的檔案名稱描述符
# -*- coding: utf-8 -*-import selectimport socketimport datetimeresponse = b"Hello, World!"sock = socket.socket()# 需要設定socket選項時,需要先將socketlevel設定為SOL_SOCKET SOL=socket option level# SO_REUSEADDR代表重用地址reuse addrsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)sock.bind(("localhost", 10000))sock.listen(5)sock.setblocking(0)inputs = [sock, ]while True: print(datetime.datetime.now()) rlist, wlist, errlist = select.select(inputs, [], [], 10) print(" >>> ", rlist, wlist, errlist) for s in rlist: if s == sock: con, addr = s.accept() # 將新的請求串連加入到監控列表中 inputs.append(con) else: # 對於其他的檔案描述符要接收資訊並返回 try: data = s.recv(1024) if data: s.send(response) finally: s.close() inputs.remove(s)
poll
poll本質上與select基本相同,只不過監控的最大串連數上相較於select沒有了限制,因為poll使用的資料結構是鏈表,而select使用的是數組,數組是要初始化長度大小的,且不能改變
poll原理
將fd列表,由使用者態拷貝到核心態
核心態遍曆,發現fd狀態變為就緒後,返回fd列表
poll狀態
POLLIN 有資料讀取POLLPRT 有資料緊急讀取POLLOUT 準備輸出:輸出不會阻塞POLLERR 某些錯誤情況出現POLLHUP 掛起POLLNVAL 無效請求:描述無法開啟
優點
跨平台使用 缺點
每次調用select,都需要把fd集合由使用者態拷貝到核心態,在fd多的時候開銷會很大
每次select都是線性遍曆整個列表,當fd很大的時候,遍曆的開銷也很大 python使用poll
poll方法
register,將要監控的檔案描述符註冊到poll中,並添加監控的事件類型
unregister,登出檔案描述符監控
modify, 修改檔案描述符監控事件類型
poll([timeout]),輪訓註冊監控的檔案描述符,返回元祖列表,元祖內容是一個檔案描述符及監控類型(
POLLIN,POLLOUT等等),如果設定了timeout,則會阻塞timeout秒,然後返回控列表,如果沒有設定timeout 微秒,則會阻塞到有傳回值為止。
# -*- coding: utf-8 -*-import selectimport socketimport datetimesock = socket.socket()sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)sock.bind(("localhost", 10000))sock.listen(5)# 設定為非阻塞sock.setblocking(0)poll = select.poll()poll.register(sock, select.POLLIN)connections = {}while True: # 遍曆被監控的檔案描述符 print(datetime.datetime.now()) for fd, event in poll.poll(10000): if event == select.POLLIN: if fd == sock.fileno(): # 如果是當前的sock,則接收請求 con, addr = sock.accept() poll.register(con.fileno(), select.POLLIN) connections[con.fileno()] = con else: # 如果是監聽的請求,讀取其內容,並設定其為等待寫監聽 con = connections[fd] data = con.recv(1024) if data: print("%s accept %s" % (fd, data)) poll.modify(fd, select.POLLOUT) else: con = connections[fd] try: con.send(b"Hello, %d" % fd) print("con >>> ", con) finally: poll.unregister(con) connections.pop(fd) con.close()
epoll
epoll相當於是linux核心支援的方法,而epoll主要是解決select,poll的一些缺點
數組長度限制
解決方案:fd上限是最大可以開啟檔案的數目,具體數目可以查看/proc/sys/fs/file-max。一般會和記憶體有關
需要每次輪詢將數組全部拷貝到核心態
解決方案:每次註冊事件的時候,會把fd拷貝到核心態,而不是每次poll的時候拷貝,這樣就保證每個fd只需要拷貝一次。
每次遍曆都需要列表線性遍曆
解決方案:不再採用遍曆的方案,給每個fd指定一個回呼函數,fd就緒時,調用回呼函數,這個回呼函數會把fd加入到就緒的fd列表中,所以epoll只需要遍曆就緒的list即可。
epoll存在的兩種事件模型
水平觸發 level-triggered,epoll對於fd的預設事件模型就是水平觸發,即監控到fd可讀寫時,就會觸發並且返回fd,例如fd可讀時,但是使用recv沒有全部讀取完畢,那下次還會將fd觸發返回,相對而言,這個更安全一些邊緣觸發 edge-triggered, epoll可以對某個fd進行邊緣觸發,邊緣觸發的意思就是每次只要觸發一次我就會給你返回一次,即使你處理完成一半,我也不會給你返回了,除非他下次再次發生一個事件。使用例子:epoll.register(serversocket.fileno(), select.EPOLLIN | select.EPOLLET)
python使用epoll
# -*- coding: utf-8 -*-import selectimport socketimport datetimeEOL1 = b'\n\n'EOL2 = b'\n\r\n'response = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'response += b'Hello, world!'sock = socket.socket()sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)sock.bind(("localhost", 10000))sock.listen(5)sock.setblocking(0)epoll = select.epoll()epoll.register(sock, select.EPOLLIN)# 為了針對長串連的情況,增加請求和響應操作connections = {}requests = {}responses = {}try: while True: print(datetime.datetime.now()) events = epoll.poll(1) print(events) for fd, event in events: if fd == sock.fileno(): # 接收請求 con, addr = sock.accept() con.setblocking(0) epoll.register(con, select.EPOLLIN | select.EPOLLET) connections[con.fileno()] = con requests[con.fileno()] = b'' responses[con.fileno()] = response elif event & select.EPOLLIN: print("ssssssssssssss") con = connections[fd] requests[fd] += con.recv(1024) # 判斷con是否已經完全發送完成 if EOL1 in requests[fd] or EOL2 in requests[fd]: epoll.modify(fd, select.EPOLLOUT) print('-' * 40 + '\n' + requests[fd].decode()[:-2]) elif event & select.EPOLLOUT: # 發送完成,將fd掛起 con = connections[fd] byteswritten = con.send(responses[fd]) # 將已發送內容截取,並判斷是否完全發送完畢,已發送完畢,epoll掛起fd,fdshutdown responses[fd] = responses[fd][byteswritten:] if len(responses[fd]) == 0: epoll.modify(fd, 0) con.shutdown(socket.SHUT_RDWR) elif event & select.EPOLLHUP: # 處理掛起fd, epoll登出fd, 關閉socket, connections移除fd epoll.unregister(fd) connections[fd].close() del connections[fd]finally: epoll.unregister(sock) sock.close()