IO多路複用與epoll機制淺析

星見遙發表於2021-01-31

epoll是Linux中用於IO多路複用的機制,在nginx和redis等軟體中都有應用,redis的效能好的原因之一也就是使用了epoll進行IO多路複用,同時epoll也是各大公司面試的熱點問題。

IO多路複用

IO多路複用是一種同步IO模型,使得一個執行緒就可以對多個檔案描述符進行監聽。當有檔案描述符準備就緒時,函式就會返回,從而通知應用進行相應的處理;當沒有描述符就緒時,函式就會阻塞。

IO多路複用對於網路應用來說是非常重要的,在沒有IO多路複用時,應用一般通過同步阻塞(每個socket連線建立一個新執行緒,這將十分耗費系統效能)或者同步非阻塞(對所有socket進行反覆遍歷,當沒有就緒描述符時就會做無用功)來實現,而這些方法的效能都不太好。

在Linux中,IO多路複用主要有三種方法select、poll和epoll。

select

int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

select是通過傳遞檔案描述符陣列fd_set*來實現的。當沒有描述符準備就緒時,函式就會阻塞;當有一個或多個檔案描述符準備就緒時就會返回,之後通過遍歷陣列找到準備就緒的描述符進行處理。select函式一般在所有作業系統中都會實現,因此具有良好的可移植性。

fd_set的大小是固定的,在Linux中一般為1024,本質是一個bitmap,通過FD_SET將描述符加入fd_set,通過對所有檔案描述符依次呼叫FD_ISSET來判斷是否準備就緒。

因此,select就有著以下的缺點:

  • select的檔案描述符最大隻能支援1024個
  • select需要通過遍歷來判斷是否準備就緒,因此時間複雜度為O(n)
  • 當監聽檔案描述符數量增加時,效能會明顯下降
  • select核心態中通過輪詢來判斷檔案描述符是否就緒
  • select每次呼叫都需要將fd_set從使用者地址空間拷貝到核心地址空間中,函式返回時又要拷貝回來

poll

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
    int fd;               // 檔案描述符
    short events;         // 等待的事件
    short revents;        // 發生的事件
};

pollselect的主要改進就是沒有了描述符陣列的大小限制,沒有最大連線數的限制。但是poll仍然需要進行遍歷才能知道哪些檔案描述符準備就緒,因此,select的缺點poll也有。

epoll

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

epoll使用了三個系統呼叫來實現,epoll_create建立一個控制程式碼,epoll_ctl向控制程式碼中新增、刪除或修改檔案描述符,epoll_wait對控制程式碼進行監聽,當有檔案描述符準備就緒後,就會通過events引數返回。返回的引數中僅包含準備就緒的檔案描述符,也就是說不再需要通過遍歷來進行判斷。epoll通過回撥機制來快速將檔案描述符加入就緒連結串列,避免輪詢;同時epoll內部使用紅黑樹來儲存所有監聽的檔案描述符。

epoll有著以下的優點:

  • 沒有最大檔案描述符數量限制
  • 使用mmap,避免了每次wait都要將陣列進行拷貝
  • 直接返回就緒的檔案描述符,避免了遍歷,時間複雜度為O(k),k為就緒檔案描述符
  • 使用回撥機制,當檔案描述符就緒時會觸發回撥函式,將描述符加入到就緒連結串列,避免輪詢
  • 監聽的檔案描述符數量對效能影響不大

但是epoll也不是一定比selectpoll好,當就緒的檔案描述符很多時,即O(k)中的k接近n時,兩者效能就比較接近了;當檔案描述符數量較少時,兩者效能也差不多;epoll的回撥函式註冊也會帶來一定的效能開銷。

觸發方式

epoll有兩種觸發方式,水平觸發(LT, level-triggered)和邊緣觸發(ET, edge-triggered)。通過一個例子來理解兩種方式:

當描述符a中到達2kb資料,呼叫epoll_wait會返回a,之後從描述符中讀取1kb資料,此時該描述符中仍有1kb資料,仍為就緒狀態;第二次呼叫epoll_wait時,如果是LT,那麼返回的描述符中仍包含a,如果為ET,那麼就不包含a。

即ET只會在狀態發生改變時觸發,只返回一次,類似於上升沿觸發;而LT只要處於就緒狀態就會一直返回,類似於電平觸發。

理論上ET的效能會比LT要好,但是ET要保證每次都要把資料全部處理完成,而LT使用起來就更加方便,不易出現bug。在實際當中兩種的效能區別可以忽略,redis使用的就是LT方式。

相關文章