epoll模型的探索與實踐


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內核提供的,上層代碼無法實現!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM