epoll是什么呢?,epoll是IO模型中的一種,屬於多路復用IO模型;
到這里你應該想到了,select,的確select也是一種多路復用的IO模型,但是其單個select最多只能同時處理1024個socket,效率實在算不上高,這時候epoll來救場了!
一.程序阻塞過程分析
假設系統目前運行了三個進程 A B C
進程A正在運行一下socket程序
1.系統會創建文件描述符指向一個socket對象 ,其包含了讀寫緩沖區,已經進行等待隊列
2.當執行到accept / recv 時系統會講進程A 從工作隊列中移除
3.將進程A的引用添加到 socket對象的等待隊列中
進程的喚醒
1.當網卡收到數據后會現將數據寫入到緩沖區
2.發送中斷信號給CPU
3.CPU執行中斷程序,將數據從內核copy到socket的緩沖區
4.喚醒進程,即將進程A切換到就緒態,同時從socket的等待隊列中移除這個進程引用
對於select來說,
1.先將所有socket放到一個列表中,
2.遍歷這個列表將進程A 添加到每個socket的等待隊列中 然后阻塞進程
3.當數據到達時,cpu執行中斷程序將數據copy給socket 同時喚醒處於等待隊列中的進程A
為了防止重復添加等待隊列 還需要移除已經存在的進程A
4.進程A喚醒后 由於不清楚那個socket有數據,所以需要遍歷一遍所有socket列表
從上面的過程中不難看出:
1.select,需要遍歷socket列表,頻繁的對等待隊列進行添加移除操作,
2.數據到達后還需要遍歷所有socket才能獲知哪些socket有數據
兩個操作消耗的時間隨着要監控的socket的數量增加而大大增加,
處於效率考慮才規定了最大只能監視1024個socket
二.epoll要解決的問題
1.避免頻繁的對等待隊列進行操作
2.避免遍歷所有socket
對於第一個問題我們先看select的處理方式
每次處理完一次讀寫后,都需要將所有過沖重復一遍,包括移除進程,添加進程,默認就會將進程添加到等待隊列,並阻塞住進程,然而等待隊列的更新操作並不頻繁,
所以對於第一個問題epoll采取的方案是,將對等待隊列的維護和,阻塞進程這兩個操作進行拆分,
import socket,select server = socket.socket() server.bind(("127.0.0.1",1688)) server.listen(5) #創建epoll事件對象,后續要監控的事件添加到其中 epoll = select.epoll() #注冊服務器監聽fd到等待讀事件集合 epoll.register(server.fileno(), select.EPOLLIN) # 等待事件發生 while True: for sock,event in epoll.poll(): pass
在epoll中register 與 unregister函數用於維護等待隊列
epoll.poll則用於阻塞進程
這樣一來就避免了 每次處理都需要重新操作等待隊列的問題
第二個問題是select中進程無法獲知哪些socket是有數據的所以需要遍歷
epol為了解決這個問題,在內核中維護了一個就緒列表,
1.創建epoll對象,epoll也會對應一個文件,由文件系統管理
2.執行register時,將epoll對象 添加到socket的等待隊列中
3.數據到達后,CPU執行中斷程序,將數據copy給socket
4.在epoll中,中斷程序接下來會執行epoll對象中的回調函數,傳入就緒的socket對象
5.將socket,添加到就緒列表中
6.喚醒epoll等待隊列中的進程,
進程喚醒后,由於存在就緒列表,所以不需要再遍歷socket了,直接處理就緒列表即可
解決了這兩個問題后,並發量得到大幅度提升,最大可同時維護上萬級別的socket
三.epoll相關函數及案例:
import select 導入select模塊 epoll = select.epoll() 創建一個epoll對象 epoll.register(文件句柄,事件類型) 注冊要監控的文件句柄和事件 事件類型: select.EPOLLIN 可讀事件 select.EPOLLOUT 可寫事件 select.EPOLLERR 錯誤事件 select.EPOLLHUP 客戶端斷開事件 epoll.unregister(文件句柄) 銷毀文件句柄 epoll.poll(timeout) 當文件句柄發生變化,則會以列表的形式主動報告給用戶進程,timeout 為超時時間,默認為-1,即一直等待直到文件句柄發生變化,如果指定為1 那么epoll每1秒匯報一次當前文件句柄的變化情況,如果無變化則返回空 epoll.fileno() 返回epoll的控制文件描述符(Return the epoll control file descriptor) epoll.modfiy(fineno,event) fineno為文件描述符 event為事件類型 作用是修改文件描述符所對應的事件 epoll.fromfd(fileno) 從1個指定的文件描述符創建1個epoll對象 epoll.close() 關閉epoll對象的控制文件描述符
案例:
#coding:utf-8 #客戶端 #創建客戶端socket對象 import socket clientsocket = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #服務端IP地址和端口號元組 server_address = ('127.0.0.1',1688) #客戶端連接指定的IP地址和端口號 clientsocket.connect(server_address) while True: #輸入數據 data = raw_input('please input:') if data == "q": break if not data: continue #客戶端發送數據 clientsocket.send(data.encode("utf-8")) #客戶端接收數據 server_data = clientsocket.recv(1024) print ('客戶端收到的數據:',server_data) #關閉客戶端socket clientsocket.close()
服務端:
# coding:utf-8 import socket, select server = socket.socket() server.bind(("127.0.0.1", 1688)) server.listen(5) msgs = [] fd_socket = {server.fileno(): server} epoll = select.epoll() # 注冊服務器的 寫就緒 epoll.register(server.fileno(), select.EPOLLIN) while True: for fd, event in epoll.poll(): sock = fd_socket[fd] print(fd, event) # 返回的是文件描述符 需要獲取對應socket if sock == server: # 如果是服務器 就接受請求 client, addr = server.accept() # 注冊客戶端寫就緒 epoll.register(client.fileno(), select.EPOLLIN) # 添加對應關系 fd_socket[client.fileno()] = client # 讀就緒 elif event == select.EPOLLIN: data = sock.recv(2018) if not data: # 注銷事件 epoll.unregister(fd) # 關閉socket sock.close() # 刪除socket對應關系 del fd_socket[fd] print(" somebody fuck out...") continue print(data.decode("utf-8")) # 讀完數據 需要把數據發回去所以接下來更改為寫就緒=事件 epoll.modify(fd, select.EPOLLOUT) #記錄數據 msgs.append((sock,data.upper())) elif event == select.EPOLLOUT: for item in msgs[:]: if item[0] == sock: sock.send(item[1]) msgs.remove(item) # 切換關注事件為寫就緒 epoll.modify(fd,select.EPOLLIN)
上述代碼只能在linux下運行,因為epoll模型是linux內核提供的,上層代碼無法實現!