Linux NIO 系列(04-4) select、poll、epoll 對比
Netty 系列目錄(https://www.cnblogs.com/binarylei/p/10117436.html)
既然 select/poll/epoll 都是 I/O 多路復用的具體的實現,之所以現在同時存在,其實他們也是不同歷史時期的產物
- select 出現是 1984 年在 BSD 里面實現的
- 14 年之后也就是 1997 年才實現了 poll,其實拖那么久也不是效率問題, 而是那個時代的硬件實在太弱,一台服務器處理1千多個鏈接簡直就是神一樣的存在了,select 很長段時間已經滿足需求
- 2002, 大神 Davide Libenzi 實現了 epoll
一、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 機制的問題
- 每次調用 select,都需要把 fd_set 集合從用戶態拷貝到內核態,如果 fd_set 集合很大時,那這個開銷也很大
- 同時每次調用 select 都需要在內核遍歷傳遞進來的所有 fd_set,如果 fd_set 集合很大時,那這個開銷也很大
- 為了減少數據拷貝帶來的性能損壞,內核對被監控的 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只需一次。
二、總結
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 方式可能性能更好。
參考:
每天用心記錄一點點。內容也許不重要,但習慣很重要!