Linux NIO 系列(04-4) select、poll、epoll 對比

Linux NIO 系列(04-4) select、poll、epoll 對比web

Netty 系列目錄(http://www.javashuo.com/article/p-hskusway-em.html)編程

既然 select/poll/epoll 都是 I/O 多路複用的具體的實現,之因此如今同時存在,其實他們也是不一樣歷史時期的產物api

  • select 出現是 1984 年在 BSD 裏面實現的
  • 14 年以後也就是 1997 年才實現了 poll,其實拖那麼久也不是效率問題, 而是那個時代的硬件實在太弱,一臺服務器處理1千多個連接簡直就是神同樣的存在了,select 很長段時間已經知足需求
  • 2002, 大神 Davide Libenzi 實現了 epoll

1、API 對比

1.1 select API

int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
int FD_ZERO(int fd, fd_set *fdset);     // 一個 fd_set 類型變量的全部位都設爲 0
int FD_CLR(int fd, fd_set *fdset);      // 清除某個位時可使用
int FD_SET(int fd, fd_set *fd_set);     // 設置變量的某個位置位
int FD_ISSET(int fd, fd_set *fdset);    // 測試某個位是否被置位

select() 的機制中提供一種 fd_set 的數據結構,其實是一個 long 類型的數組,每個數組元素都能與一打開的文件句柄創建聯繫(這種聯繫須要本身完成),當調用 select() 時,由內核根據IO 狀態修改 fd_set 的內容,由此來通知執行了 select() 的進程哪一 Socket 或文件可讀。數組

select 機制的問題服務器

  1. 每次調用 select,都須要把 fd_set 集合從用戶態拷貝到內核態,若是 fd_set 集合很大時,那這個開銷也很大
  2. 同時每次調用 select 都須要在內核遍歷傳遞進來的全部 fd_set,若是 fd_set 集合很大時,那這個開銷也很大
  3. 爲了減小數據拷貝帶來的性能損壞,內核對被監控的 fd_set 集合大小作了限制(默認爲 1024)

1.2 poll API

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
    int fd;         // 文件描述符
    short events;   // 感興趣的事件
    short revents;  // 實際發生的事件
};

poll 的機制與 select 相似,與 select 在本質上沒有多大差異,管理多個描述符也是進行輪詢,根據描述符的狀態進行處理,可是 poll 沒有最大文件描述符數量的限制。也就是說,poll 只解決了上面的問題 3,並無解決問題 1,2 的性能開銷問題。網絡

1.3 epoll API

// 函數建立一個 epoll 句柄,其實是一棵紅黑樹
int epoll_create(int size);
// 函數註冊要監聽的事件類型,op 表示紅黑樹進行增刪改
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 函數等待事件的就緒,成功時返回就緒的事件數目,調用失敗時返回 -1,等待超時返回 0
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

epoll 在 Linux2.6 內核正式提出,是基於事件驅動的 I/O 方式,相對於 select 來講,epoll 沒有描述符個數限制,使用一個文件描述符管理多個描述符,將用戶關心的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的copy只需一次。數據結構

2、總結

I/O 多路複用技術在 I/O 編程過程當中,當須要同時處理多個客戶端接入請求時,能夠利用多線程或者 I/O 多路複用技術進行處理。I/O 多路複用技術經過把多個 I/O 的阻塞複用到同一個 select 的阻塞上,從而使得系統在單線程的狀況下能夠同時處理多個客戶端請求。與傳統的多線程/多進程模型比,I/O 多路複用的最大優點是系統開銷小,系統不須要建立新的額外進程或者線程,也不須要維護這些進程和線程的運行,下降了系統的維護工做量,節省了系統資源,I/O多路複用的主要應用場景以下:多線程

  • 服務器須要同時處理多個處於監聽狀態或者多個鏈接狀態的套接字
  • 服務器須要同時處理多種網絡協議的套接字。

目前支持 I/O 多路複用的系統調用有 select、pselect、poll、epoll,在 Linux 網絡編程過程當中,很長一段時間都使用 select 作輪詢和網絡事件通知,然而 select 的一些固有缺陷致使了它的應用受到了很大的限制,最終 Linux 不得不在新的內核版本中尋找 select 的替代方案,最終選擇了 epoll。epoll 與 select 的原理比較相似,爲了克服 select 的缺點, epoll 做了不少重大改進,現總結以下。併發

2.1 支持一個進程打開的 socket 描述符(FD)不受限制(僅受限於操做系統的最大文件句柄數)

select、poll 和 epoll 底層數據各不相同。select 使用數組;poll 採用鏈表,解決了 fd 數量的限制;epoll 底層使用的是紅黑樹,可以有效的提高效率。

select 最大的缺陷就是單個進程所打開的 FD 是有必定限制的,它由 FD_SETSIZE 設置,默認值是 1024。對於那些須要支持上萬個 TCP 鏈接的大型服務器來講顯然太少了。能夠選擇修改這個宏而後從新編譯內核,不過這會帶來網絡效率的降低。咱們也能夠經過選擇多進程的方案(傳統的 Apache 方案)解決這個問題,不過雖然在 Linux 上建立進程的代價比較小,但仍舊是不可忽視的。另外,進程間的數據交換很是麻煩,對於 Java 來講,因爲沒有共享內存,須要經過 Socket 通訊或者其餘方式進行數據同步,這帶來了額外的性能損耗,増加了程序複雜度,因此也不是一種完美的解決方案。值得慶幸的是, epoll 並無這個限制,它所支持的 FD 上限是操做系統的最大文件句柄數,這個數字遠遠大於 1024。例如,在 1GB 內存的機器上大約是 10 萬個句柄左右,具體的值能夠經過 cat proc/sys/fs/file-max 查看,一般狀況下這個值跟系統的內存關係比較大。

# (全部進程)當前計算機所能打開的最大文件個數。受硬件影響,這個值能夠改(經過limits.conf)
cat /proc/sys/fs/file-max

# (單個進程)查看一個進程能夠打開的socket描述符上限。缺省爲1024
ulimit -a 
# 修改成默認的最大文件個數。【註銷用戶,使其生效】
ulimit -n 2000

# soft軟限制 hard硬限制。所謂軟限制是能夠用命令的方式修改該上限值,但不能大於硬限制
vi /etc/security/limits.conf
* soft nofile 3000      # 設置默認值。可直接使用命令修改
* hard nofile 20000     # 最大上限值

2.2 I/O 效率不會隨着 FD 數目的増加而線性降低

傳統 select/poll 的另外一個致命弱點,就是當你擁有一個很大的 socket 集合時,因爲網絡延時或者鏈路空閒,任一時刻只有少部分的 socket 是「活躍」的,可是 select/poll 每次調用都會線性掃描所有的集合,致使效率呈現線性降低。 epoll 不存在這個問題,它只會對「活躍」的 socket 進行操做一一這是由於在內核實現中, epoll 是根據每一個 fd 上面的 callback 函數實現的。那麼,只有「活躍」的 socket オ會去主動調用 callback 函數,其餘 idle 狀態的 socket 則不會。在這點上, epoll 實現了一個僞 AIO。針對 epoll 和 select 性能對比的 benchmark 測試代表:若是全部的 socket 都處於活躍態 - 例如一個高速 LAN 環境, epoll 並不比 select/poll 效率高太多;相反,若是過多使用 epoll_ctl,效率相比還有稍微地下降可是一旦使用 idle connections 模擬 WAN 環境, epoll 的效率就遠在 select/poll 之上了。

2.3 使用 mmap 加速內核與用戶空間的消息傳遞

不管是 select、poll 仍是 epoll 都須要內核把 FD 消息通知給用戶空間,如何避免沒必要要的內存複製就顯得很是重要,epoll 是經過內核和用戶空間 mmap 同一塊內存來實現的。

2.4 epoll API 更加簡單

包括建立一個 epoll 描述符、添加監聽事件、阻塞等待所監聽的事件發生、關閉 epoll 描述符等。

值得說明的是,用來克服 select/poll 缺點的方法不僅有 epoll, epoll 只是一種 Linux 的實現方案。在 freeBSD 下有 kqueue,而 dev/poll 是最古老的 Solaris 的方案,使用難度依次遞增。 kqueue 是 freeBSD 寵兒,它其實是一個功能至關豐富的 kernel 事件隊列,它不只僅是 select/poll 的升級,並且能夠處理 signal、目錄結構變化、進程等多種事件。 kqueue 是邊緣觸發的。 /dev/poll 是 Solaris 的產物,是這一系列高性能 API 中最先出現的。 Kernel 提供了一個特殊的設備文件 /dev/poll,應用程序打開這個文件獲得操做 fd_set 的句柄,經過寫入 polled 來修改它,一個特殊的 ioctl 調用用來替換 select。不過因爲出現的年代比較早,因此 /dev/poll 的接口實現比較原始。

附表1: select/poll/epoll 區別

比較 select poll epoll
操做方式 遍歷 遍歷 回調
底層實現 數組 鏈表 紅黑樹
IO效率 每次調用都進行線性遍歷,
時間複雜度爲O(n)
每次調用都進行線性遍歷,
時間複雜度爲O(n)

事件通知方式,每當fd就緒,
系統註冊的回調函數就會被調用,
將就緒fd放到readyList裏面,
時間複雜度O(1)
最大鏈接數 | 1024 | 無上限 | 無上限
fd拷貝 | 每次調用select,
都須要把fd集合從用戶態拷貝到內核態 | 每次調用poll,
都須要把fd集合從用戶態拷貝到內核態 | 調用epoll_ctl時拷貝進內核並保存,
以後每次epoll_wait不拷貝

總結:epoll 是 Linux 目前大規模網絡併發程序開發的首選模型。在絕大多數狀況下性能遠超 select 和 poll。目前流行的高性能 web 服務器 Nginx 正式依賴於 epoll 提供的高效網絡套接字輪詢服務。可是,在併發鏈接不高的狀況下,多線程+阻塞 I/O 方式可能性能更好。

參考:

  1. IO多路複用的三種機制Select,Poll,Epoll

天天用心記錄一點點。內容也許不重要,但習慣很重要!

相關文章
相關標籤/搜索