1.select
初識select
系統提供 select 函數來實現多路復用輸入 / 輸出模型 .select 系統調用是用來讓我們的程序監視多個文件描述符的狀態變化的 ;程序會停在 select 這里等待,直到被監視的文件描述符有一個或多個發生了狀態改變 ;
select函數模型
select的函數原型如下: #include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,??????????????????fd_set *exceptfds, struct timeval *timeout);
參數解釋
參數 nfds 是需要監視的最大的文件描述符值 +1 ;rdset,wrset,exset 分別對應于需要檢測的可讀文件描述符的集合,可寫文件描述符的集 合及異常文件描述符的集合;參數 timeout 為結構 timeval ,用來設置 select() 的等待時間
參數timeout取值
NULL :則表示 select ()沒有 timeout , select 將一直被阻塞,直到某個文件描述符上發生了事件 ;0 :僅檢測描述符集合的狀態,然后立即返回,并不等待外部事件的發生。特定的時間值:如果在指定的時間段里沒有事件發生, select 將超時返回。
關于fd_set結構
void FD_CLR(int fd, fd_set *set); // 用來清除描述詞組 set 中相關 fd 的位int FD_ISSET(int fd, fd_set *set); // 用來測試描述詞組 set 中相關 fd 的位是否為真void FD_SET(int fd, fd_set *set); // 用來設置描述詞組 set 中相關 fd 的位void FD_ZERO(fd_set *set); // 用來清除描述詞組 set 的全部位
timeval結構

函數返回值
執行成功則返回文件描述詞狀態已改變的個數如果返回 0 代表在描述詞狀態改變前已超過 timeout 時間,沒有返回當有錯誤發生時則返回 -1 ,錯誤原因存于 errno ,此時參數 readfds , writefds, exceptfds 和 timeout 的值變成不可預測。錯誤值可能為:EBADF 文件描述詞為無效的或該文件已關閉EINTR 此調用被信號所中斷EINVAL 參數 n 為負值。ENOMEM 核心內存不足
select執行流程
* ( 1 )執行 fd_set set; FD_ZERO(&set); 則 set 用位表示是 0000,0000 。 * ( 2 )若 fd = 5, 執行 FD_SET(fd,&set); 后set 變為 0001,0000( 第 5 位置為 1) * ( 3 )若再加入 fd = 2 , fd=1, 則 set 變為 0001,0011 * ( 4 )執行select(6,&set,0,0,0)阻塞等待 * ( 5 )若 fd=1,fd=2 上都發生可讀事件,則 select 返回,此時 set 變為0000,0011。注意:沒有事件發生的 fd=5 被清空
select就緒條件
讀就緒
socket 內核中 , 接收緩沖區中的字節數 , 大于等于低水位標記 SO_RCVLOWAT. 此時可以無阻塞的讀該文件描述符, 并且返回值大于 0;socket TCP 通信中 , 對端關閉連接 , 此時對該 socket 讀 , 則返回 0;監聽的 socket 上有新的連接請求 ;socket 上有未處理的錯誤 ;
寫就緒
socket 內核中 , 發送緩沖區中的可用字節數 ( 發送緩沖區的空閑位置大小 ), 大于等于低水位標記SO_SNDLOWAT, 此時可以無阻塞的寫 , 并且返回值大于 0;socket 的寫操作被關閉 (close 或者 shutdown). 對一個寫操作被關閉的 socket 進行寫操作 , 會觸發 SIGPIPE信號;socket 使用非阻塞 connect 連接成功或失敗之后 ;socket 上有未讀取的錯誤 ;
異常就緒
socket 上收到帶外數據 . 關于帶外數據 , 和 TCP 緊急模式相關 ( 回憶 TCP 協議頭中 , 有一個緊急指針的字段 )
select的特點
可監控的文件描述符個數取決與 sizeof(fd_set) 的值 . 我這邊服務器上 sizeof(fd_set) = 512 ,每 bit 表示一個文件描述符,則我服務器上支持的最大文件描述符是512*8=4096.將 fd 加入 select 監控集的同時,還要再使用一個數據結構 array 保存放到 select 監控集中的 fd ,一是用于再 select 返回后, array 作為源數據和 fd_set 進行 FD_ISSET 判斷。二是 select 返回后會把以前加入的但并無事件發生的 fd 清空,則每次開始 select 前都要重新從 array 取得fd逐一加入 (FD_ZERO 最先 ) ,掃描 array 的同時取得 fd 最大值 maxfd ,用于 select 的第一個參數。
select的優點
1.可移植性好:select幾乎在所有的平臺上都支持,具有良好的跨平臺兼容性。
2.超時精度高:select對于超時值的精度可以達到微秒級別,比poll的毫秒級別精度更高。
select的缺點
1.每次調用 select, 都需要手動設置 fd 集合 , 從接口使用角度來說也非常不便 .2.每次調用 select ,都需要把 fd 集合從用戶態拷貝到內核態,這個開銷在 fd 很多時會很大3.同時每次調用 select 都需要在內核遍歷傳遞進來的所有 fd ,這個開銷在 fd 很多時也很大4.select 支持的文件描述符數量太小5.代碼編寫難度大
?select的一般編碼格式
1.需要有一個第三方數組,用來保存所以合法的fd
2.while(true)
{
? ? ? ? 1.遍歷數組,更新出最大值
? ? ? ? 2.遍歷數組,添加所有需要關心的fd到fd_set位圖中
? ? ? ? 3.調用select進行事件檢測
? ? ? ? 4.遍歷數組,找到就緒的事件,根據就緒事件,完成對應的動作
}
2.poll
poll函數接口
#include <poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout);// pollfd 結構struct pollfd {????????int fd; /* file descriptor */????????short events; /* requested events */????????short revents; /* returned events */};
參數說明
fds 是一個 poll 函數監聽的結構列表 . 每一個元素中 , 包含了三部分內容 : 文件描述符 , 監聽的事件集合 , 返回的事件集合.nfds 表示 fds 數組的長度 .timeout 表示 poll 函數的超時時間 , 單位是毫秒 (ms)?
events和revents的取值
返回結果
返回值小于 0, 表示出錯 ;返回值等于 0, 表示 poll 函數等待超時 ;返回值大于 0, 表示 poll 由于監聽的文件描述符就緒而返回 .
poll的優點
1.效率高2.輸入輸出參數分離,不需要進行大量的重置3.poll參數級別,沒有可以管理的fd上限
poll的缺點
1.poll依舊需要不少的遍歷,在用戶層檢測事件就緒與內核檢測fd就緒,都是一樣的,用戶還需要維護數組
2.poll需要內核到用戶的拷貝
3.poll的代碼也比較復雜?
3.epoll
按照man手冊的說法: 是為處理大批量句柄而作了改進的poll.
?int epoll_create(int size);
2.epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
它不同于 select() 是在監聽事件時告訴內核要監聽什么類型的事件 , 而是在這里先注冊要監聽的事件類型 .第一個參數是 epoll_create() 的返回值 (epoll 的句柄 ).第二個參數表示動作,用三個宏來表示 .第三個參數是需要監聽的 fd.第四個參數是告訴內核需要監聽什么事
第二個參數的取值
EPOLL_CTL_ADD :注冊新的 fd 到 epfd 中;EPOLL_CTL_MOD :修改已經注冊的 fd 的監聽事件;EPOLL_CTL_DEL :從 epfd 中刪除一個 fd
struct epoll_event結構如下:
EPOLLIN : 表示對應的文件描述符可以讀 ( 包括對端 SOCKET 正常關閉 );EPOLLOUT : 表示對應的文件描述符可以寫 ;EPOLLPRI : 表示對應的文件描述符有緊急的數據可讀 ( 這里應該表示有帶外數據到來 );EPOLLERR : 表示對應的文件描述符發生錯誤 ;EPOLLHUP : 表示對應的文件描述符被掛斷 ;EPOLLET : 將 EPOLL 設為邊緣觸發 (Edge Triggered) 模式 , 這是相對于水平觸發 (Level Triggered) 來說的 .EPOLLONESHOT :只監聽一次事件 , 當監聽完這次事件之后 , 如果還需要繼續監聽這個 socket 的話 , 需要再次把這個socket 加入到 EPOLL 隊列里 .
3.epoll_wait
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);?
參數 events 是分配好的 epoll_event 結構體數組 .epoll 將會把發生的事件賦值到 events 數組中 (events 不可以是空指針,內核只負責把數據復制到這個events數組中,不會去幫助我們在用戶態中分配內存 ).maxevents 告之內核這個 events 有多大,這個 maxevents 的值不能大于創建 epoll_create() 時的 size.參數 timeout 是超時時間 ( 毫秒, 0 會立即返回, -1 是永久阻塞 ).如果函數調用成功,返回對應 I/O 上已準備好的文件描述符數目,如返回 0 表示已超時 , 返回小于 0 表示函數失敗.
epoll的工作原理?
首先,epoll區別于select和poll的點在于,就緒是通過下層主動來的。
?
epoll模型:?
當某一進程調用 epoll_create 方法時, Linux 內核會創建一個 eventpoll 結構體,這個結構體中有兩個成員與epoll 的使用方式密切相關。每一個 epoll 對象都有一個獨立的 eventpoll 結構體,用于存放通過 epoll_ctl 方法向 epoll 對象中添加進來的事件.這些事件都會掛載在紅黑樹中,如此,重復添加的事件就可以通過紅黑樹而高效的識別出來 ( 紅黑樹的插入時間效率是lgn ,其中 n 為樹的高度 ).而所有添加到 epoll 中的事件都會與設備 ( 網卡 ) 驅動程序建立回調關系,也就是說,當響應的事件發生時會調用這個回調方法.這個回調方法在內核中叫 ep_poll_callback, 它會將發生的事件添加到 rdlist 雙鏈表中 .在 epoll 中,對于每一個事件,都會建立一個 epitem 結構體 .
struct eventpoll{..../* 紅黑樹的根節點,這顆樹中存儲著所有添加到 epoll 中的需要監控的事件 */struct rb_root rbr;/* 雙鏈表中則存放著將要通過 epoll_wait 返回給用戶的滿足條件的事件 */struct list_head rdlist;....};struct epitem{struct rb_node rbn;// 紅黑樹節點struct list_head rdllink;// 雙向鏈表節點struct epoll_filefd ffd; // 事件句柄信息struct eventpoll *ep; // 指向其所屬的 eventpoll 對象struct epoll_event event; // 期待發生的事件類型}
當調用 epoll_wait 檢查是否有事件發生時,只需要檢查 eventpoll 對象中的 rdlist 雙鏈表中是否有 epitem元素即可.如果 rdlist 不為空,則把發生的事件復制到用戶態,同時將事件數量返回給用戶 . 這個操作的時間復雜度是 O(1).
epoll的優點
1.接口使用方便 : 雖然拆分成了三個函數 , 但是反而使用起來更方便高效 . 不需要每次循環都設置關注的文件描述符, 也做到了輸入輸出參數分離開2.數據拷貝輕量 : 只在合適的時候調用 EPOLL_CTL_ADD 將文件描述符結構拷貝到內核中 , 這個操作并不頻繁( 而 select/poll 都是每次循環都要進行拷貝 )3.事件回調機制 : 避免使用遍歷 , 而是使用回調函數的方式 , 將就緒的文件描述符結構加入到就緒隊列中 ,epoll_wait 返回直接訪問就緒隊列就知道哪些文件描述符就緒 . 這個操作時間復雜度 O(1). 即使文件描述符數目很多, 效率也不會受到影響 .4.沒有數量限制 : 文件描述符數目無上限
epoll工作方式
epoll有2種工作方式-水平觸發(LT)和邊緣觸發(ET).
epoll 默認狀態下就是 LT 工作模式 .當 epoll 檢測到 socket 上事件就緒的時候 , 可以不立刻進行處理 . 或者只處理一部分 .如上面的例子 , 由于只讀了 1K 數據 , 緩沖區中還剩 1K 數據 , 在第二次調用 epoll_wait 時 , epoll_wait 仍然會立刻返回并通知socket 讀事件就緒 .直到緩沖區上所有的數據都被處理完 , epoll_wait 才不會立刻返回 .支持阻塞讀寫和非阻塞讀寫
如果我們在第 1 步將 socket 添加到 epoll 描述符的時候使用了 EPOLLET 標志 , epoll 進入 ET 工作模式 .當 epoll 檢測到 socket 上事件就緒時 , 必須立刻處理 .如上面的例子 , 雖然只讀了 1K 的數據 , 緩沖區還剩 1K 的數據 , 在第二次調用 epoll_wait 的時候 ,epoll_wait 不會再返回了 .也就是說 , ET 模式下 , 文件描述符上的事件就緒后 , 只有一次處理機會 .ET 的性能比 LT 性能更高 ( epoll_wait 返回的次數少了很多 ). Nginx 默認采用 ET 模式使用 epoll.只支持非阻塞的讀寫