select、poll、信號驅動、epoll 學習筆記

select

函數&結構

// 返回值表示有多少個 fd 就緒, 同一 fd 若是有多個就緒事件會被統計屢次
int select(int maxfdpl, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
複製代碼

參數說明

  • maxfdpl : 最大文件描述符號+1, 他的值必須設爲比 三個文件描述符集中 所包含的 最大文件描述符大1. 其實是要檢查的文件描述符數量, 數組下標從0開始. 有了這個值, 就不用檢查全部的描述符.
  • readfds : 用來檢測輸入是否就緒的文件描述符集合
  • writers : 用來檢測輸出是否就緒的文件描述符集合
  • exceptfds : 用來檢測異常狀況是否發生的文件描述符集合
  • timeval : 控制 select() 的阻塞, 設置爲 NULL 的時候, select() 會一直阻塞

timeval結構數組

// 兩個值都爲0的話, 此時select()不回阻塞, 會一直輪詢.
// 有一個不爲0的話, 則會給 select() 設定一個等待時間的上限值
struct timeval{
  time_t		tv_sec; // 秒
  suseconds	tv_usec;// 微妙級別的精度
}
複製代碼

fd_set 結構

// 每一個 unsigned long 型能夠表示多少個bit, 是經過 bitmap 的記錄, 一個bit能夠記錄一位數, 好比8位就能夠標記8個fd
#define __NFDBITS (8 * sizeof(unsigned long)) 
// 默認的 FD_SETSIZE 爲 1024, 要修改的話必須修改 glibc 中的頭文件定義, 而後從新編譯, 可是通常連接數量過多, 使用後面的 epoll 性能更佳
#define __FD_SETSIZE 1024 
// 假設一共須要記錄 __FD_SETSIZE(默認1024) 個fd, 須要多少個長整型數
#define __FDSET_LONGS (__FD_SETSIZE/__NFDBITS) 

typedef struct {
  // 使用 long 數組來表示
    unsigned long fds_bits [__FDSET_LONGS];                 
} __kernel_fd_set;
typedef __kernel_fd_set   fd_set;
複製代碼

返回值

  • -1 : 表示 錯誤發生
  • 0 : 表示在任何文件描述符就緒以前 select() 就已經調用超時
  • 正整數 : 表示有一個或多個文件描述符已經達到就緒狀態, 返回值表示有多少個fd就緒. 若是一個文件描述符在3個集合中都被指定, 則這個返回值會 +3, 也就是會被 計算屢次 .

優缺點

優勢

  • 移植性高, 基本上各類主流 os 都支持

缺點

  • 只支持水平觸發markdown

  • 數據須要從用戶空間(程序)複製到內核空間數據結構

  • 每次將3個數據集的數據發送到內核, 都須要進程先重置文件描述符集爲須要監聽的 fd併發

  • fd很是多的時候, 3個描述符集合都須要輪詢, 很是消耗CPU異步

  • select 返回後, 程序並不知道是哪些 fd 準備就緒, 而只知道一共有多少個就緒了, 須要進程本身對傳遞過去的集合進行遍歷和判斷socket

poll

函數&結構

// 調用
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);

// 包含文件描述符的結構
struct pollfd{
  int 	fd;				// 自身的描述符號 fd
  short events;		// 訂閱的事件
  short revents;	// 響應的事件
}
複製代碼

參數說明

調用函數

  • fdarray : pollfd 結構的一個數組
  • nfds : 指定 fdarray 的元素數量
  • timeout : 表示願意等待多長的時間
    • -1 : 永遠等待
    • 0 : 不等待
    • 大於0 : 等待 timeout(毫秒) 時間

包含 fd 的結構性能

  • fd : 對應 fd 號
  • events : 感興趣/訂閱 的事件
  • revents : 表示觸發了哪些事件, 由內核修改

返回值

  • -1 : 失敗
  • 0 : 超時前沒有任何事件發生
  • 大於 0 : 指定的描述符準備好或者timeout到期返回.

優缺點

每一個fd都有屬於自身的 pollfd 結構, 它將 感興趣事件和觸發的事件分紅了 events 和 revents. events 的值告訴內核咱們關心的是描述符的哪些事件 ; 當某個 fd 有事件觸發了以後, 就由 內核修改 revents 的數據, 互不干擾, 因此沒必要像 select 那樣, 每次調用都必須重置 fd 集合.ui

優勢

  • 不須要每次由進程重製 fd 集合spa

  • 數組大小沒有限制

缺點

  • 只支持水平觸發

  • 跟 select() 同樣須要將數據在用戶空間和內核空間來回複製

  • 可移植性較高但沒有 select() 高

  • 不適合 fd 數量多的時候, fd 多了性能不如 epoll

  • 跟 select() 同樣, poll返回後, 程序並不知道是哪些 fd 準備就緒, 而只知道一共有多少個就緒了, 須要進程本身對傳遞過去的集合進行遍歷和判斷

"一般程序調用這些系統調用(select() * poll() )所檢查的文件描述符集合都是相同的, 可是內核並不會記錄他們."

後面信號驅動I/O 以及 epoll 均可以使內核記錄下進程中感興趣的文件描述符, 經過這種機制消除了 select() 和 poll() 的性能擴展問題.這種方案是 根據發生的 I/O 事件 來延展, 而與被檢查的文件描述符數量無關 , 當須要檢查大量的文件描述符的時, 信號驅動 I/O 和 epoll 能提供更好的性能表現.

信號驅動I/O

後面的這兩種信號驅動I/O 和 epoll 跟前面兩種 select 和 poll 有所不一樣, select 和 poll 沒法讓內核記住進程所感興趣的 fd , 因此每次都要將感興趣的 fd 從用戶空間複製到內核空中, 浪費CPU很是消耗CPU時間, 而且每次調用返回都要檢查全部的fd, 才能知道是哪些fd觸發了事件, 這也是他們不適合大量 fd 操做的緣由.

信號驅動I/O, 進程請求內核: 當文件描述符上有課執行的I/O 操做時,向進程發送一個信號.

具體的步驟在後面

步驟

  • 1.爲內核發送的通知信號安裝一個信號處理例程, 默認狀況下, 這個通知信號時 SIGIO.

  • 2.設定文件描述符的屬主(owner) , 也就是當文件描述符上課執行I/O時會接收到通知信號的進程或進程組. 一般設置調用進程爲屬主. 可經過 fcntl()的 F_SETOWN 操做完成:

    fcntl(fd, F_SETOWN, pid);
    複製代碼
  • 3.設定 O_NONBLOCK 標誌使其能變成非阻塞 I/O

  • 4.經過打開 O_ASYNC 標誌使其能變成信號驅動I/O, 這個和第3步能夠合併成一個操做, 由於它們都須要用到 fcntl()的 F_SETFL

    flags = fcntl(fd, F_GETFL);
    fcntl(fd, F_SETFL, flags | O_ASYNC | O_NONBLOCK);
    複製代碼
  • 5.調用進程執行完這些後就能夠執行其它任務了, 接下來若是有相應的 fd 有事件觸發, 內核會給進程發送一個信號進行通知, 經過設置的信號例程

  • 6.信號驅動I/O提供的是邊緣觸發通知, 這表示一旦進程被通知I/O就緒, 就應該儘量多的執行I/O(儘量的多讀取字節), 若是fd是設置的非阻塞, 表示須要在循環中I/O系統調用直到失敗位置, 此時的錯誤碼爲 EAGAIN 或者 EWOULDBLOCK.

一些特性

在 Linux2.4 或者更早的版本能應用於 套接字、終端、僞終端以及其它特定類型的設備上.Linux2.6 可用於管道和 FIFO.自Linux2.6.25以後, 也能在 inotify 文件描述符上使用.

  • 在啓動信號驅動I/O前安裝信號處理例程 : 因爲接收到 SIGIO 信號默認行爲是終止進程, so 須要在驅動信號I/O前先爲 SIGIO 信號安裝處理例程, 若是先啓動信號驅動/IO, 則在安裝例程以前進程可能先被終止了.

    在其它一些UNIX實現上, 信號 SIGIO 的默認行爲是被忽略.

什麼時候發送 「I/O就緒」 信號

終端和僞終端

產生新的輸入會生成一個信號, 即便以前的輸入沒有被讀取.終端出現文件結尾的狀況, 此時也會發送輸入就緒的信號(僞終端不會).終端沒有輸出就緒,斷開連接也不會有信號.在Linux 中,2.4.19版本後對僞終端的從設備端提供了「輸出就緒」的信號,當僞終端主設備側讀取了輸入後就會產生這個信號.

管道和FIFO

管道和FIFO的讀端 ,信號的產生狀況:

  • 數據寫入到管道中(即便已經有未讀取的輸入存在)
  • 管道的寫端關閉

對於管道或FIFO的寫端 ,信號會在下列狀況中產生

  • 對管道的讀操做增長了管道中的空餘空間大小,所以如今能夠寫入PIPE_BUF個字節而不被阻塞
  • 管道的讀端關閉

套接字

信號驅動I/O適用於UNIX和Internet下的數據報套接字,信號產生狀況:

  • 一個輸入數據報到達套接字(即便已經有未讀取的數據報正等待讀取)
  • 套接字上發生了異步錯誤

信號驅動I/O適用於UNIX和Internet下的流式套接字,產生狀況:

  • 監聽套接字接收到新的鏈接
  • TCP connect() 請求完成, 也就是TCP的主動端進入 ESTABLISHED 狀態, 對於UNIX 域套接字,相似狀況是不會發出信號.
  • 套接字上接收到了新的輸入(即便已經有未讀取的輸入存在)
  • 套接字對端使用 shutdown() 關閉了寫連接(半關閉), 或者經過 close() 徹底關閉
  • 套接字上輸出就緒(例如發送緩衝區有了空間,則會觸發寫就緒事件)
  • 套接字發生了異步錯誤

inotify 文件描述符

當 notify 文件描述符成爲可讀狀態時會產生一個信號, 也就是由 inotify 文件描述符監視的其中一個文件上有事件發生時.

問題

信號隊列溢出的處理

能夠排隊的實時信號數量是有限的, 當達到了數量限制以後, 通知會恢復爲默認的SIGIO信號, 出現這種現象表示信號隊列溢出了. 出現這種狀況, 會失去有關fd上發生的I/O事件的信息, 由於 SIGIO 信號不會排隊, SIGIO信號處理例程不接受 siginfo_t 結構體參數, so 信號處理例程不能肯定是哪個fd上產生了信號.

上面的問題的一種解決方式是增長事實信號數量的限制來減少信號隊列溢出的可能性, 可是並不能徹底排除.

採用 F_SETSIG 來創建實時信號做爲 「I/O就緒」 通知的程序必須爲 SIGIO 安裝處理例程.若是發送了SIGIO信號, 程序能夠先經過 sigwaitinfo() 先將隊列中的實時信號所有獲取, 臨時切換到 select() 或 poll(), 經過它們獲取剩餘的發生 I/O 事件的文件描述符列表.

優缺點

優勢:

當有事件觸發的時候, 由內核經過發送信號的方式主動通知進程

不須要由用戶進程複製fd數組到內核

缺點:

只支持邊緣觸發

信號處理很差可能會致使進程出問題.

epoll

epoll 同I/O多路複用和信號驅動同樣, Linux的epoll(event poll) 能夠檢查多個文件描述符上的I/O就緒狀態.

流程&函數&結構

epoll 是 Linux 系統中獨有的, 在2.6版本後新增, epoll API 的核心數據結構稱做 epoll 實例,它和一個打開的文件描述符相關聯, 這個文件描述符不是用來作I/O操做的, 它是內核數據結構的句柄, 這些內核數據結構實現了兩個目的:

  • 記錄了在進程中聲明過的感興趣的文件描述符列表 -- interest list(興趣列表)
  • 維護了處於I/O就緒狀態的文件描述符列表 -- ready list(就緒列表)

(ready list 是 interest list 的子集)

使用流程

  • 系統調用 epoll_create() 建立 epoll 實例, 返回表明該實例的文件描述符(對於每個打開的fd建立一個與之對應的 epoll 實例表示該fd)

    // size 僅僅是指明內部數據結構的初始大小劃分,2.6.8以後這個參數被忽略使用
    int epoll_create(int size);
    複製代碼

    在這個fd再也不使用, 經過close()關閉, 當全部的 epoll 實例相關的文件描述符都被關閉的時候, 實例被銷燬, 相關的資源都返還給系統(多個fd可能引用到了相同的 epoll 實例, 這是因爲調用了 fork() 或者 dup() 這樣的函數)

  • 系統調用 epoll_ctl() 操做同 epoll 實例相關聯的興趣列表, 經過 epoll_ctl() 能夠增長新的描述符到列表中、將已有的文件描述符從該列表刪除, 以及修改表明文件描述符上事件類型的位掩碼.(新增、刪除感興趣列表、感興趣事件)

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);
    複製代碼

    參數fd代表要修改的fd, 能夠是管道、FIFO、套接字、POSIX消息隊列、inotify實例、終端、設備,甚至是另外一個 epoll 實例的fd.

    event 事件的結構體

    struct epoll_event{
      uint32_t events;		// epoll events(bit mask), 位操做,多個事件進行 邏輯& 操做
      epoll_data_t data;	// User data
    }
    
    typedef union epoll_data{
      void 		*ptr;	// 
      int 		 fd;
      uint32_t u32;
      uint64_t u64;
    }epoll_data_t;
    複製代碼
  • 系統調用 epoll_wait() 返回與 epoll 實例相關聯的就緒列表中的成員.( 獲取在感興趣列表中已經就緒的)

    int epoll_wait(int epfd, struct epoll_event * evlist, int maxevents, int timeout);
    複製代碼
    • 參數 evlist 所指向的結構體數組中返回的是有關 就緒態文件描述符的信息

    • events 字段返回了 在該描述符上已經發生的事件掩碼

    • 參數 timeout 用來肯定 epoll_wait()的阻塞行爲

      • -1 : 一直阻塞
      • 0 : 執行一次非阻塞的檢查
      • 大於0 : 阻塞至多 timeout 毫秒, 直到有事件發生或者捕捉到一個信號爲止

      調用成功後,epoll_wait()返回數組 evlist 中的元素個數。若是在 timeout 超時間隔內沒有任何文件描述符處於就緒態的話,返回 0。出錯時返回−1,並在 errno 中設定錯誤碼以表示錯誤緣由.

事件

對於 epoll 檢查的每個文件描述符, 能夠指定位掩碼來表示感興趣的事件, 這些掩碼和poll() 所使用的位掩碼有緊密聯繫..

優勢

主要優勢:

  • 檢查大量文件描述符的時, epoll 的性能比 select() 和 poll() 高不少.

  • epoll 既支持水平觸發, 也支持邊緣觸發. 與之相反, select() 和 poll() 只支持水平觸發, 信號驅動I/O 只支持邊緣觸發.

性能表現上, epoll 跟信號驅動I/O差很少, 可是epoll有一些優勢賽過信號驅動I/O:

  • 能夠避免複雜的信號處理流程(好比信號隊列溢出時的處理)

  • 靈活性高, 能夠指定咱們但願檢查的事件類型(例如檢查 socket 的讀就緒事件、寫就緒事件、或者二者都檢查)

總結

關於水平和邊緣觸發

水平觸發

當緩衝區中有數據可讀/有空間可寫的時候, 就會響應事件.

一個問題

使用水平觸發的時候, 在socket緩衝區可寫的時候, 會一直觸發寫事件, 因此一種處理方法就是, 在要寫數據的時候纔去註冊寫事件, 寫完數據後取消寫事件.

若是數據區一直有數據, 可是進程還沒處理完數據, 會一直觸發事件消耗性能, 這也是水平觸發的一個缺點.

邊緣觸發

邊緣觸發的話, 是靠着新事件的產生纔會觸發的.

你的事件已經反饋給進程了, 可是若是進程對此次事件的數據沒讀取完, 那剩餘的數據就會在緩衝區裏面, 不會再觸發事件, 除非有新的數據到來(跟水平觸發不一樣,水平對只要有數據,就會觸發事件), 這樣你就能夠讀取到以前的數據了, 因此這也是邊緣觸發的一個問題, 當有事件發生的時候, 儘量的多讀取數據, 防止數據停留在了緩衝區, 而進程讀取不到完整的數據沒法對請求進行處理. 客戶端極可能就會請求超時.

若是開發團隊的實力足夠強的話,使用邊緣觸發進行開發.不然使用水平觸發的性能已經足夠.

在鏈接數多、併發高的狀況下,使用邊緣觸發能更好的體現性能優點

特色

select() 和 poll() 相對於 信號驅動和epoll() 在不一樣os之間的可移植性更高, 可是當fd過多的時候, 效率也遠低於後二者.

poll 和 select 只支持 水平觸發

select() 和 poll() 的操做相似, 每次檢查都是進程主動將數組 拷貝 到內核

select 傳遞的是3個事件(讀、寫、異常)數組,而且每次傳遞到內核前都須要清空原數據

poll 每次是將進程感興趣的fd複製到內核中, 而且使用了 event 和 revent 將感興趣事件和觸發的事件分開了, 這樣就不須要每次都初始化數組的數據.

select 和 poll 都是主動的進行檢查, 由於這兩種調用內核並不記錄進程感興趣的fd和事件.後面這兩種信號驅動I/O 和 epoll, 都能使內核記住他們感興趣的fd和事件, 因此不須要每次都進行數組的拷貝.

信號驅動只支持邊緣觸發

如上面所說, 信號驅動和epoll對於接收來講是相似的, 都是內核通知進程, 而不是進程每隔一段時間去檢查對應的文件描述符上是否有事件發生. 當文件描述符數量過多的時候, 性能優點很是明顯, 由於不須要將 fd 複製到內核中, 而且 都是由內核主動通知進程 .

epoll支持水平觸發和邊緣觸發

epoll 支持水平觸發和邊緣觸發. epoll 的機制相似於信號驅動, 都是進程告訴內核對哪些 fd 感興趣, 而後對應的fd上有事件的時候, 由內核主動通知進程, 進程再進行相應的處理.

總的對比

select、poll 和 信號驅動、epoll 的區別在於 主動 or 被動接受事件, select 和 poll 是主動去檢查對應fd上是否有事件發生, 每次都須要複製數組到內核中, 而後由內核修改參數返回, 而 信號驅動和 epoll 在進程添加感興趣fd和對應事件後, 每次通知都由內核主動的通知進程, 由於內核知道須要通知哪些, 不須要進程主動去詢問和將數組從用戶進程複製到內核, 因此信號驅動和epoll的性能顯著的高於 select 和 poll.

相關文章
相關標籤/搜索