select / poll / epoll 結合源碼分析

基於對網絡編程和基本I/O模型瞭解的基礎上,進一步分析I/O複用的系統調用函數。

1 select/poll實現

(1) int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds: 所有描述符的總數,受限於FD_SETSIZE
fd_set: 按bit位標記句柄的隊列,如fd_set *readfds這個集合中包括文件描述符,監視這些文件描述符的讀變化。如果這個集合中有一個文件可讀,select就會返回一個大於0的值,表示有文件可讀,如果沒有可讀的文件,則根據timeout參數再判斷是否超時。可以傳入NULL值,表示不關心任何文件的讀變化。  

(2) int poll(struct pollfd *fds, nfds_t nfds, int timeout) 構造一個pollfd結構體數組,每個數組元素指定一個描述符標號及其所關心的條件
struct pollfd {
int fd; /* file descriptor */
short events; /* 由用戶來設置,告訴內核我們關注的是什麼 */
short revents; /* revents域是返回時內核設置的 */
}

大致流程

a 依次調用fd對應的struct file.f_op->poll()方法,檢查每個提供待檢測IO的fd是否已經有IO事件就緒
b 如果已經有IO事件就緒,則直接所收集到的IO事件返回,本次調用結束
c 如果暫時沒有IO事件就緒,則根據所給定的超時參數,選擇性地進入等待
d 如果超時參數指示不等待,則本次調用結束,無IO事件返回
e 如果超時參數指示等待(等待一段時間或持續等待),則將當前select/poll/epoll的調用任務掛起
f 當所檢測的fd任何一個有新的IO事件發生時,會將上述的處於等待的任務喚醒。任務被喚醒之後,重新執行1中的IO事件收集過程,將此時收集到的IO事件返回,本次的調用過程結束。

2 epoll實現

epoll的API

(1). int epoll_create(int size)
    創建一個epoll句柄
    param size: 監聽fd的數目
    return: 創建epoll實例的文件描述符
(2). int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
    註冊要監聽的事件類型
    param epfd:create的返回值
    param op: 動作(註冊,修改,刪除fd監聽事件)
    param fd: 監聽fd
    param event: 需要監聽的事件
(3). int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
    等待事件的產生
    return :準備好的文件fd
    檢測到事件,就將所有就緒的事件從內核事件表中複製到events指向的數組中

epoll_create的創建過程做了什麼

  • 文件系統裏建file結點(佔用一個fd值)
  • 內核cache裏建紅黑樹,以存儲epoll_ctl傳來的socket(fd)
  • 建立一個雙向鏈表,以存儲準備就緒的事件

epol_ctl新事件如何註冊(紅黑樹和鏈表有什麼用)

  • 增加socket句柄時,檢查在紅黑樹中是否存在,不存在則添加,然後向內核註冊回調函數;回調函數的作用是在中斷事件來臨時,向就緒鏈表中插入數據
  • 其中,紅黑樹以其自平衡的特點,降低fd增刪改查的複雜度(lgn)
  • 此處雙向鏈表效果相當於隊列,就緒fd先入先出

可以偷懶的epol_wait

  • 執行epoll_wait時,只需要判斷就緒鏈表是否爲空,將準備就緒的socket拷貝到用戶態內存,清空就緒鏈表
  • LT模式下還需加個班,如果該節點確實有事件未處理,那麼就會把該節點重新放入到剛剛刪除掉的且剛準備好的就緒鏈表

epoll的兩種工作模式
在這裏插入圖片描述

  • 水平觸發(LT):在1處,不做任何操作,內核會不斷通知進程文件描述符準備就緒;因此可以不立即處理
  • 邊緣觸發(ET):只有在0 – 1處時,內核纔會通知進程文件描述符準備就緒。之後如果不在發生文件描述符狀態變化,內核就不會再通知進程文件描述符已準備就緒。只支持no-block socket(重複read,直到EAGAIN)

3 select/poll與epoll有什麼區別(epoll高效的原因)

從二者的實現看,區別已經很明顯了,可以再結合源碼看一下

區別一 epoll, select 支持的併發數

  • selct:一個進程所打開的fd(文件描述符)是有限制的,由FD_SETSIZE設置,默認值是1024/2048
  • epoll:fd上限是最大可以打開文件的數目

區別二 epoll, select/poll喚醒過程

epoll 執行回調將就緒的fd加入就緒鏈表,使epol_wait可以直接獲取
static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
    list_add_tail(&epi->rdllink, &ep->rdllist);
    // list_add_tail,先加入的節點左移,先入先出
}

select 直接喚醒(do_select實現成本增加,通過三層循環找到可操作的fd)
int default_wake_function(wait_queue_t *curr, unsigned mode, int sync,
              void *key)
{
    return try_to_wake_up(curr->private, mode, sync);
}
此過程,epoll的實現有明顯優勢

區別三 維護套接字集合的方式

select/poll: 
服務器每次循環調用select()時首先需要使用FD_ZERO宏來初始化fd_set對象;
然後調用FD_SET將我們維護的這個套接字集合中的套接字加入fd_set這個集合中;
即每次調用select,都需要把fd集合從用戶態拷貝到內核態,這個開銷在fd很多時會很大
epoll,只需在註冊時進行一次拷貝,後續的epol_ctl只是進行簡單的維護
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章