面試必備:對 select,poll,epoll 的詳細解析

1 前言

select,poll,epoll 都是 IO 多路複用的機制。何爲 IO 多路複用的機制?IO 多路複用的本質是通過一種機制,讓單個進程可以監視多個描述符,當發現某個描述符就緒之後,能夠通知程序進行相應的讀寫操作。

select,poll,epoll 都是同步 IO。所謂同步 IO,便是讀寫是阻塞的,需要在讀寫事件就緒後自己負責讀寫,而異步 IO 會把數據從內核拷貝到用戶空間,並不需要自己負責讀寫。

select、poll 和 epoll 都是 Linux 提供的 IO 複用方式。

2 select

2.1 函數定義

我們來看一下 select 函數的定義:

int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);

2.2 參數

select 函數的參數如下:

  1. int maxfdp1:待測試的文件描述字個數
  2. fd_set *readset , fd_set *writeset , fd_set *exceptset:fd_set 是一個集合,裏面存放的是文件描述符,三個參數分別表示讓內核測試讀、寫和異常條件的文件描述符集合。若對某一條件不感興趣,可以將其設置爲空指針
  3. const struct timeval *timeout :告訴內核,等待所指定文件描述符集合中的任意一個就緒,一共可以花費多少時間

2.3 返回值

返回值類型爲 int ,若有就緒描述符,則返回其數目,若超時則返回0,若出錯則返回-1。

2.4 運行機制

我們上面提到過,傳入的參數有一個 fd_set 集合,其實這是一個 long 類型的數組,數組元素能夠與已經打開的文件句柄(例如 Socket 句柄,又或者其它文件)建立聯繫。

當我們調用 select 函數時,內核會根據 IO 狀態對 fd_set 的內容進行修改,從而通知執行 select 函數的進程哪一個文件或者 Socket 是可讀的。

select 函數與同步阻塞模型並無過多區別,甚至還多出了一部分操作(監視 socket /調用 select 函數),導致更低的效率。

2.5 優勢

用戶可以在一個線程內同時處理多個 socket 的 IO 請求。用戶可以註冊多個 socket,然後調用 select 函數讀取被激活的 socket,從而實現在同一個線程內同時處理多個 IO 請求,在這點上select 函數與同步阻塞模型不同,因爲在同步阻塞模型中需要通過多線程才能達到這個目的。

話說回來,爲啥我們不直接使用多進程/多線程技術,而是要使用 IO 多路複用技術呢?這是因爲,使用 IO 多路複用技術,系統不必創建和維護進程/線程,從而節約了系統的開銷。

2.6 缺點

  1. 調用 select 函數時,需要把 fd_set 集合從用戶態拷貝到內核態,當 fd_set 集合很大時,這個開銷將會非常巨大
  2. 調用 select 函數時,需要在內核遍歷傳遞進來的所有 fd_set,當 fd_set 集合很大時,這個開銷將會非常巨大
  3. 內核對被監控的 fd_set 集合大小做了限制

3 poll

講解了 select 函數之後,相信各位讀者對 poll 的理解便沒有多大難度了。poll 的機制與 select 幾乎相同,會對管理的描述符進行輪詢操作,並根據描述符的狀態進行相應的處理。

poll 將用戶傳入的數組拷貝到內核空間,然後查詢每個描述符對應的設備狀態,如果設備就緒則在設備等待隊列中加入一項並繼續遍歷,如果遍歷完所有描述符後沒有發現就緒設備,則掛起當前進程,直到設備就緒或者主動超時。

3.1 poll 與 select 的區別

select 函數中,內核對 fd_set 集合的大小做出了限制,大小不可變爲1024;而 poll 函數中,並沒有最大文件描述符數量的限制(基於鏈表存儲)。

3.2 函數定義

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

3.3 參數

  1. struct pollfd *fds :存放需要檢測狀態的 socket 描述符,調用 poll 函數後,fds 數組不會被清空。pollfd 結構體表示一個被監視的文件描述符,poll 函數會通過 fds 參數的傳遞來監視多個文件描述符
  2. nfds_t nfds : 記錄 fds 中描述符的總數量

3.4 返回值

返回值類型爲 int ,返回 fds 集合中就緒的讀、寫或出錯的描述符數量,若返回0則表示超時,若返回-1則表示出錯。

4 epoll

epoll 是基於事件驅動的 IO 方式,與 select 相比,epoll 並沒有描述符個數限制。

epoll 使用一個文件描述符管理多個描述符,它將文件描述符的事件放入內核的一個事件表中,從而在用戶空間和內核空間的複製操作只用實行一次即可。

4.1 函數定義

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);

各個函數的作用如下:

  1. epoll_create:創建一個 epoll 句柄,其中 size 表示內核要監聽的描述符數量
  2. epoll_ctl:註冊要監聽的事件類型。在每次註冊新的事件到 epoll 句柄中時,會把所有的描述符拷貝進內核,而不是在 epoll_wait 的時候重複拷貝。epoll 保證了每個描述符在整個過程中只會拷貝一次
  3. epoll_wait:等待事件的就緒,成功時返回就緒的事件數目

4.2 特點

epoll 是 poll 的增強版,在獲取事件時,epoll 無需遍歷整個被監聽的描述符集,而是隻需遍歷被內核 IO 事件異步喚醒而加入 Ready 隊列的描述符集合即可。因此,epoll 能顯著提高程序在大量併發連接中只有少量活躍的情況下的系統 CPU 利用率。

epoll 會在 epoll_ctl 時爲每個描述符指定一個回調函數,當設備就緒,喚醒等待隊列上的等待者時,就會調用這個回調函數,而這個回調函數會把就緒的描述符加入一個就緒鏈表。epoll_wait 實際上就是去就緒鏈表中查看有沒有就緒的描述符。

4.3 優勢

  1. 沒有最大併發連接的限制
  2. 不採取輪詢的方式,效率高,只會處理活躍的連接,與連接總數無關

4.4 兩種模式

epoll 提供了兩種模式,一種是水平觸發,一種是邊緣觸發。邊緣觸發與水平觸發相比較,可以使用戶空間程序可能緩存 IO 狀態,並減少 epoll_wait 的調用,從而提高應用程序的效率。

  1. 水平觸發(LT):默認工作模式,當 epoll_wait 檢測到某描述符事件就緒並通知應用程序時,應用程序可以不立即處理該事件;等到下次調用 epoll_wait 時,會再次通知此事件
  2. 邊緣觸發(ET):當 epoll_wait 檢測到某描述符事件就緒並通知應用程序時,應用程序必須立即處理該事件。如果不處理,下次調用 epoll_wait 時,不會再次通知此事件

ET 模式減少了 epoll 事件的觸發次數,其效率比 LT 模式下高。爲什麼呢?

如果我們使用 LT 模式的話,系統中一旦有大量不需要讀寫的就緒文件描述符,每次調用 epoll_wait 都會返回,大大降低處理程序檢索自己關心的就緒文件描述符的效率。如果使用的是 ET 模式,當被監控的文件描述符上有可讀寫事件發生時,epoll_wait 會通知處理程序去讀寫,如果這次沒有把數據全部讀寫完,下次調用 epoll_wait 不會通知你,即它只會通知你一次,直到該文件描述符上出現第二次可讀寫事件纔會通知你。在這種模式下,系統不會充斥大量你不關心的就緒文件描述符,故其效率較高。

5 總結

select,poll 需要自己不斷輪詢所有描述符集合,直到設備就緒,期間可能要睡眠和喚醒多次交替。epoll 其實也需要調用 epoll_wait 不斷輪詢就緒鏈表,期間也可能多次睡眠和喚醒交替,但是它是設備就緒時,調用回調函數,把就緒描述符放入就緒鏈表中,並喚醒在 epoll_wait 中進入睡眠的進程。雖然都要睡眠和交替,但是 select 和 poll 在醒着的時候要遍歷整個描述符集合,而epoll在醒着的時候只要判斷一下就緒鏈表是否爲空即可,這就是回調機制帶來的性能提升,節省了大量的 CPU 時間。

select,poll 每次調用都要將描述符集合從用戶態往內核態拷貝一次,而 epoll 只需要一次拷貝即可。

6 select,poll,epoll 之間的對比

  1. IO 效率:select 只知道有 IO 事件發生,卻不知道是哪幾個流,只能採取輪詢所有流的方式,故其具有 O(n) 的無差別輪詢複雜度,處理的流越多,無差別輪詢時間就越長;poll 與 select 並無區別,它的時間複雜度也是 O(n);epoll 會將哪個流發生了怎樣的 IO 事件通知我們(當描述符就緒時,系統註冊的回調函數會被調用,將就緒描述符放到 readyList 裏面),它是事件驅動的,其時間複雜度爲 O(1)
  2. 操作方式:select 和 poll 都是採取遍歷的方式,而 epoll 則是採取了回調的方式
  3. 底層實現:select 的底層實現爲數組,poll 的底層實現爲鏈表,而 epoll 的底層實現爲紅黑樹
  4. 最大連接數:select 的最大連接數爲 1024 或 2048,而 poll 和 epoll 是無上限的
  5. 對描述符的拷貝:select 和 poll 每次被調用時都會把描述符集合從用戶態拷貝到內核態,而 epoll 在調用 epoll_ctl 時會拷貝進內核並保存,之後每次 epoll_wait 時不會拷貝
  6. 性能:epoll 在絕大多數情況下性能遠超 select 和 poll,但在連接數少並且連接都十分活躍的情況下,select 和 poll 的性能可能比 epoll 好,因爲 epoll 的通知機制需要很多函數回調

參考:IO多路複用的三種機制Select,Poll,Epoll
select、poll、epoll之間的區別(搜狗面試)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章