目錄html
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
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 機制的問題服務器
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 的性能開銷問題。網絡
// 函數建立一個 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 做了不少重大改進,現總結以下。併發
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 # 最大上限值
傳統 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 之上了。
不管是 select、poll 仍是 epoll 都須要內核把 FD 消息通知給用戶空間,如何避免沒必要要的內存複製就顯得很是重要,epoll 是經過內核和用戶空間 mmap 同一塊內存來實現的。
包括建立一個 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 的接口實現比較原始。
比較 | 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 方式可能性能更好。
參考:
天天用心記錄一點點。內容也許不重要,但習慣很重要!