Select、poll、Epoll、KQueue區別

在早期的文章《unix IO模型》中我們介紹了5種IO模型,如下圖是幾種IO模型的對比

幾種IO模型的對比

幾種IO模型的對比

從上面的圖可以看出,從左到右,越往後,阻塞越少,理論上效率也就越優。

其中Select對應的是第三種IO模型:I/O Multiplexing IO多路複用模型,而epollkqueue其實和Select一樣也屬於I/O Multiplexing IO多路複用模型,只是相比於select來說多了一些高級特性而已,可以看做擁有了第四種模型的某些特性,比如callback的回調機制。

IO多路複用的好處就在於單個process就可以同時處理多個網絡連接的IO。它的基本原理就是select,poll,epoll,kqueue這些個function會不斷的輪詢所負責的所有socket,當某個socket就緒(一般是讀就緒或者寫就緒),就通知用戶進程。

I/O Multiplexing IO多路複用模型

I/O Multiplexing IO多路複用模型

I/O Multiplexing IO多路複用模型

當用戶進程調用了select,那麼整個進程會被block,而同時,kernel會“監視”所有select負責的socket,當任何一個socket中的數據準備好了,select就會返回。這個時候用戶進程再調用read操作,將數據從kernel拷貝到用戶進程。

所以,I/O 多路複用的特點是通過一種機制一個進程能同時等待多個文件描述符,而這些文件描述符(套接字描述符)其中的任意一個進入讀就緒狀態,select()函數就可以返回

更詳細的描述可以查看《/unix-IO模型/#I-O-多路複用(-IO-multiplexing)》

Select

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

select 函數監視的文件描述符分3類,分別是:

  • writefds
  • readfds
  • exceptfds

調用後select函數會阻塞,直到有描述副就緒(有數據 可讀、可寫、或者有except),或者超時(timeout指定等待時間,如果立即返回設爲null即可),函數返回。當select函數返回後,可以 通過遍歷fdset,來找到就緒的描述符。

select的優缺點

優點

  • 跨平臺
    • select目前幾乎在所有的平臺上支持,其良好跨平臺支持也是它的一個優點。

缺點

  • 單個進程打開的文件描述是有一定限制的,它由FD_SETSIZE設置,默認值是1024,採用數組存儲,雖然可以通過編譯內核改變,但相對麻煩。
  • 另外在檢查數組中是否有文件描述需要讀寫時,採用的是線性掃描的方法,即不管這些socket是不是活躍的,我都輪詢一遍,所以效率比較低

poll

1
int poll (struct pollfd *fds, unsigned int nfds, int timeout);

不同與select使用三個位圖來表示三個fdset的方式,poll使用一個 pollfd的指針實現。

1
2
3
4
5
struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
};

pollfd結構包含了要監視的event和發生的event,不再使用select“參數-值”傳遞的方式。

poll的優缺點

主要優點

  • 採樣鏈表的形式存儲,它監聽的描述符數量沒有限制,可以超過select默認限制的1024大小

缺點

  • 類似select,另外在檢查鏈表中是否有文件描述需要讀寫時,採用的是線性掃描的方法,即不管這些socket是不是活躍的,我都輪詢一遍,所以效率比較低

epoll

epoll是在2.6內核中提出的,是之前的select和poll的增強版本。相對於select和poll來說,epoll更加靈活,沒有描述符限制。epoll使用一個文件描述符管理多個描述符,將用戶關係的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的copy只需一次。

1
2
3
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);
  • int epoll_create(int size);

    • 創建一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大,這個參數不同於select()中的第一個參數,給出最大監聽的fd+1的值,參數size並不是限制了epoll所能監聽的描述符最大個數,只是對內核初始分配內部數據結構的一個建議。
    • 當創建好epoll句柄後,它就會佔用一個fd值,在linux下如果查看/proc/進程id/fd/,是能夠看到這個fd的,所以在使用完epoll後,必須調用close()關閉,否則可能導致fd被耗盡。
  • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

    • 函數是對指定描述符fd執行op操作。
      • epfd:是epoll_create()的返回值。
      • op:表示op操作,用三個宏來分別表示對fd添加、刪除和修改監聽事件。
        • EPOLL_CTL_ADD 添加
        • EPOLL_CTL_DEL 刪除
        • EPOLL_CTL_MOD 修改。
      • fd:是需要監聽的fd(文件描述符)
      • epoll_event:是告訴內核需要監聽什麼事,struct epoll_event結構如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
struct epoll_event {
  __uint32_t events;  /* Epoll events */
  epoll_data_t data;  /* User data variable */
};

//events可以是以下幾個宏的集合:
EPOLLIN :表示對應的文件描述符可以讀(包括對端SOCKET正常關閉);
EPOLLOUT:表示對應的文件描述符可以寫;
EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(這裏應該表示有帶外數據到來);
EPOLLERR:表示對應的文件描述符發生錯誤;
EPOLLHUP:表示對應的文件描述符被掛斷;
EPOLLET: 將EPOLL設爲邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的。
EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列裏
  • int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

    • 等待epfd上的io事件,最多返回maxevents個事件。
      參數events用來從內核得到事件的集合,maxevents告之內核這個events有多大,這個maxevents的值不能大於創建epoll_create()時的size,參數timeout是超時時間(毫秒,0會立即返回,-1將不確定,也有說法說是永久阻塞)。該函數返回需要處理的事件數目,如返回0表示已超時。

工作模式

epoll對文件描述符的操作有兩種模式:LT(level trigger)和ET(edge trigger)。LT模式是默認模式,LT模式與ET模式的區別如下:

  • LT模式:當epoll_wait檢測到描述符事件發生並將此事件通知應用程序,應用程序可以不立即處理該事件。下次調用epoll_wait時,會再次響應應用程序並通知此事件。

    • LT(level triggered)是缺省的工作方式,並且同時支持block和no-block socket.在這種做法中,內核告訴你一個文件描述符是否就緒了,然後你可以對這個就緒的fd進行IO操作。如果你不作任何操作,內核還是會繼續通知你的。
  • ET模式:當epoll_wait檢測到描述符事件發生並將此事件通知應用程序,應用程序必須立即處理該事件。如果不處理,下次調用epoll_wait時,不會再次響應應用程序並通知此事件。

    • ET(edge-triggered)是高速工作方式,只支持no-block socket。在這種模式下,當描述符從未就緒變爲就緒時,內核通過epoll告訴你。然後它會假設你知道文件描述符已經就緒,並且不會再爲那個文件描述符發送更多的就緒通知,直到你做了某些操作導致那個文件描述符不再爲就緒狀態了(比如,你在發送,接收或者接收請求,或者發送接收的數據少於一定量時導致了一個EWOULDBLOCK 錯誤)。但是請注意,如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),內核不會發送更多的通知(only once)

ET模式在很大程度上減少了epoll事件被重複觸發的次數,因此效率要比LT模式高。epoll工作在ET模式的時候,必須使用非阻塞套接口,以避免由於一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務餓死。

在 select/poll中,進程只有在調用一定的方法後,內核纔對所有監視的文件描述符進行掃描。

而epoll事先通過epoll_ctl()來註冊一個文件描述符,一旦基於某個文件描述符就緒時,內核會採用類似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait()時便得到通知。(此處去掉了遍歷文件描述符,而是通過監聽回調的的機制。這正是epoll的魅力所在。)

epoll優點

  • 監視的描述符數量不受限制,它所支持的FD上限是最大可以打開文件的數目,這個數字一般遠大於2048,舉個例子,在1GB內存的機器上大約是10萬左 右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統內存關係很大。select的最大缺點就是進程打開的fd是有數量限制的。這對 於連接數量比較大的服務器來說根本不能滿足。雖然也可以選擇多進程的解決方案( Apache就是這樣實現的),不過雖然linux上面創建進程的代價比較小,但仍舊是不可忽視的,加上進程間數據同步遠比不上線程間同步的高效,所以也不是一種完美的方案。

  • IO的效率不會隨着監視fd的數量的增長而下降。epoll不同於select和poll輪詢的方式,而是通過每個fd定義的回調函數來實現的。只有就緒的fd纔會執行回調函數。

如果沒有大量的idle -connection或者dead-connection,epoll的效率並不會比select/poll高很多,但是當遇到大量的idle- connection,就會發現epoll的效率大大高於select/poll。

kqueue

kqueue與epoll非常相似,最初是2000年Jonathan Lemon在FreeBSD系統上開發的一個高性能的事件通知接口。註冊一批socket描述符到 kqueue 以後,當其中的描述符狀態發生變化時,kqueue 將一次性通知應用程序哪些描述符可讀、可寫或出錯了。

其他

  • 只有IOCP是asynchronous I/O,其他機制或多或少都會有一點阻塞。
  • select低效是因爲每次它都需要輪詢。但低效也是相對的,視情況而定,也可通過良好的設計改善
  • epoll, kqueue是Reacor模式,IOCP是Proactor模式。
  • java nio包是select模型
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章