【面試】I/O 複用

南望山下磚瓦工發表於2019-03-31

面試考察的是如何把書本上的知識轉化為自己的理解

前前後後也面試了大小快二十場面試,總的來看感覺面試的過程其實沒有想象的那麼難,很多知識是自己平時遇到的問題,要麼是沒有用心去理解,要麼是理解了,心裡知道是什麼意思,但是無法表達清楚,抓不住關鍵點。

所以打算以後學習一個知識,先仔細閱讀理解,然後不看書本,用自己的話寫下來總結成一篇部落格。

知識回顧

  1. 套接字

套接字就是IP:埠號,是用於 TCP 連線的端點

  1. I/O 模型

一個輸入操作大概就是分兩步:應用程式請求並等待資料到達;從核心向程式複製資料。而對於套接字上的輸入操作,第一步類似,就是等待資料從網路中到達,資料到達以後為了防止資料丟失,會先複製到核心中的某個緩衝區;第二步就是將核心緩衝區中的資料複製到應用程式緩衝區。

Unix 下有五種 I/O 模型:

  • 阻塞式 I/O
  • 非阻塞式 I/O
  • I/O 複用(select poll)
  • 訊號驅動式 I/O(SIGIO)
  • 非同步 I/O(AIO)

阻塞式 I/O

【面試】I/O 複用

具體過程就是首先應用程式發起系統呼叫,會進入等待,一直等到資料包就緒,這時候資料還是在核心緩衝區中,需要將資料包返回給應用程式緩衝區。

需要注意的是,阻塞式 I/O 不是意味著系統進入阻塞,而僅僅是當前應用程式阻塞,其他應用程式還是可以繼續執行的,因此不消耗 CPU 時間,執行效率較高

非阻塞式 I/O

【面試】I/O 複用

應用程式執行系統呼叫以後,不同於阻塞式程式直接進入阻塞,而是如果資料沒有準備好,就會返回一個錯誤碼,應用程式可以繼續執行,但是需要不斷進行系統呼叫來獲得 I/O 是否完成,這種方式稱為輪詢(polling)

我這裡的理解就是其實非阻塞式 I/O 和阻塞式 I/O 實際上是相同的,都需要等待資料,只不過一個是被動進入阻塞,另一個是主動請求看資料有沒有好,就比如是高中學習,一個是在想學習新知識過程中等待老師上課講,在老師沒有上課講之前就一直等待,而另外一個同學是不停去辦公室問,老師上不上課,什麼時候上課。

I/O 複用

【面試】I/O 複用

I/O 複用其實流程和阻塞式有很大相同之處,只不過 I/O 複用會先呼叫 select,這個時候系統會監聽所有 select 負責的資料包,一旦有某個資料準備就緒,就會將其返回,然後進行 recvfrom 系統呼叫,執行同阻塞式 I/O 相同的處理。對比阻塞式 I/O,這裡需要呼叫兩個系統呼叫,所以效率肯定不如前者,但是最大的特點就是可以同時處理多個 connection。

多說一句。所以,如果處理的連線數不是很高的話,使用 select/epoll 的 web server 不一定比使用 multi-threading + blocking IO 的 web server 效能更好,可能延遲還更大。select/epoll 的優勢並不是對於單個連線能處理得更快,而是在於能處理更多的連線。

訊號驅動式 I/O

【面試】I/O 複用

不常用,基本不會涉及到

非同步 I/O

【面試】I/O 複用

進行 aio_read 系統呼叫會立即返回, 應用程式繼續執行, 不會被阻塞, 核心會在所有操作完成之後嚮應用程式傳送訊號。

非同步 I/O 與訊號驅動 I/O 的區別在於, 非同步 I/O 的訊號是通知應用程式 I/O 完成,而訊號驅動 I/O 的訊號是通知應用程式可以開始 I/O。

簡單說就是,當使用者應用程式請求資料時,不會進入阻塞,而是繼續執行其他任務,等到該應用程式資料處理完畢,那麼相應的系統會給使用者程式傳回一個訊號,表示應用程式已經執行完畢,這是和訊號驅動最大的不同,這個返回的訊號是通知程式已經執行完畢,而訊號驅動返回訊號是通知程式可以開始執行。

比較

【面試】I/O 複用

阻塞式和非阻塞式區別?同步非同步區別?

先說簡單的,阻塞式和非阻塞式最大的區別就是阻塞式在等待資料階段會進入阻塞,而非阻塞式不會,但是對於非阻塞式,在獲得資料存在核心緩衝區後,將核心緩衝區中資料複製到應用程式緩衝區這個階段是阻塞的。

在說同步非同步區別之前先要了解什麼叫同步,什麼叫非同步?區別就是在進行 I/O 操作時候會將程式阻塞,根據這個定義就知道,阻塞式、非阻塞式、訊號驅動式、I/O 複用式都屬於同步,為什麼非阻塞式也是呢?這就涉及到前面說的,雖然在開始是沒有阻塞,但是後面將資料從核心到應用程式是阻塞的。

引用博主中很好的一個例子來理解:

最後,再舉幾個不是很恰當的例子來說明這四個IO Model: 有A,B,C,D四個人在釣魚:
A用的是最老式的魚竿,所以呢,得一直守著,等到魚上鉤了再拉桿;
B的魚竿有個功能,能夠顯示是否有魚上鉤,所以呢,B就和旁邊的MM聊天,隔會再看看有沒有魚上鉤,有的話就迅速拉桿;
C用的魚竿和B差不多,但他想了一個好辦法,就是同時放好幾根魚竿,然後守在旁邊,一旦有顯示說魚上鉤了,它就將對應的魚竿拉起來;
D是個有錢人,乾脆僱了一個人幫他釣魚,一旦那個人把魚釣上來了,就給D發個簡訊

面試必問

select/poll/epoll 都是 I/O 多路複用的具體實現, select 出現的最早, 之後是 poll, 再是 epoll。

select

有三種型別的描述符型別: readset、 writeset、 exceptset, 分別對應讀、 寫、 異常 條件的描述符集合。 fd_set 使用陣列實現, 陣列大小使用 FD_SETSIZE 定義。

timeout 為超時引數, 呼叫 select 會一直阻塞直到有描述符的事件到達或者等待的 時間超過 timeout。

成功呼叫返回結果大於 0, 出錯返回結果為 -1, 超時返回結果為 0。

關鍵程式碼如下:

fd_set fd_in, fd_out;
struct timeval tv;
// Reset the sets
FD_ZERO( &fd_in );
FD_ZERO( &fd_out );
// Monitor sock1 for input events
FD_SET( sock1, &fd_in );
// Monitor sock2 for output events
FD_SET( sock2, &fd_out );
// Find out which socket has the largest numeric value as select requires it
int largest_sock = sock1 > sock2 ? sock1 : sock2;
// Wait up to 10 seconds
tv.tv_sec = 10;
tv.tv_usec = 0;
// Call the select
int ret = select( largest_sock + 1, &fd_in, &fd_out, NULL, &tv ); 
// Check if select actually succeed
if ( ret == -1 )
    // report error and abort
else if ( ret == 0 )
    // timeout; no event detected
else
{
    if ( FD_ISSET( sock1, &fd_in ) )
        // input event on sock1
    if ( FD_ISSET( sock2, &fd_out ) )
        // output event on sock2
}
複製程式碼

select 詳細過程:

當使用者 process 呼叫 select 的時候,select 會將需要監控的 readfds 集合拷貝到核心空間(假設監控的僅僅是 socket 可讀),然後遍歷自己監控的 socket sk,挨個呼叫 sk 的 poll 邏輯以便檢查該 sk 是否有可讀事件,遍歷完所有的 sk 後,如果沒有任何一個 sk 可讀,那 select 會呼叫 schedule_timeout 進入 schedule 迴圈,使得 process 進入睡眠。如果在 timeout 時間內某個 sk 上有資料可讀了,或者等待 timeout 了,則呼叫 select 的 process 會被喚醒,接下來 select 就是遍歷監控的 sk 集合,挨個收集可讀事件並返回給使用者了

到這裡,我們有三個問題需要解決:

(1)被監控的fds集合限制為1024,1024太小了,我們希望能夠有個比較大的可監控fds集合

(2)fds集合需要從使用者空間拷貝到核心空間的問題,我們希望不需要拷貝

(3)當被監控的fds中某些有資料可讀的時候,我們希望通知更加精細一點,就是我們希望能夠從通知中得到有可讀事件的fds列表,而不是需要遍歷整個fds來收集。

poll

poll 的機制與 select 類似,與 select 在本質上沒有多大差別,管理多個描述符也是進行輪詢,根據描述符的狀態進行處理,但是 poll 沒有最大檔案描述符數量的限制。poll改變了fds集合的描述方式,使用了pollfd結構而不是select的fd_set結構,使得poll支援的fds集合限制遠大於select的1024。其中 pollfd 使用連結串列實現

int poll(struct pollfd *fds, unsigned int nfds, int timeout);
複製程式碼

select poll 比較

  1. 功能

實現大致相同,但是一些細節還是存在不同:

  • select 的描述符型別使用陣列實現,FD_SETSIZE 預設大小為 1024,不過這個值可以改變,如果需要修改的話要重新編譯;而 poll 使用連結串列實現,沒有描述符大小的限制
  • poll 提供更多的事件型別,並且對描述符的重複利用比 selec 高
  • 如果一個執行緒對某個描述符呼叫了 selec 或者 poll,另一個執行緒關閉了該描述符,會導致呼叫結果不確定。
  1. 速度

速度都很慢

  • 共同的就是在呼叫時都需要將全部描述符從應用程式緩衝區複製到核心緩衝區
  • 兩者返回結果中沒有宣告哪些描述符已經準備好,所以如果返回值大於 0 時,應用程式都需要使用輪詢的方式來找到 I/O 完成的描述符
  1. 可移植性

select 出現比較早,所以基本上所有的系統都支援,而只有比較新的系統才支援 poll

epoll(太強了)

epoll_ctl() 用於向核心註冊新的描述符或者是改變某個檔案描述符的狀態。 已註冊的描述符在核心中會被維護在一棵紅黑樹上, 通過回撥函式核心會將 I/O 準備好的描述符加入到一個連結串列中管理, 程式呼叫 epoll_wait() 便可以得到事件完成的描述符。

從上面的描述可以看出, epoll 只需要將描述符從程式緩衝區向核心緩衝區拷貝一次, 並且程式不需要通過輪詢來獲得事件完成的描述符。

epoll 僅適用於 Linux OS。

epoll 比 select 和 poll 更加靈活而且沒有描述符數量限制。

epoll 對多執行緒程式設計更有友好, 一個執行緒呼叫了 epoll_wait() 另一個執行緒關閉了同一個描述符也不會產生像 select 和 poll 的不確定情況。

epoll 工作模式

epoll 的描述符事件有兩種觸發模式: LT( level trigger) 和 ET( edge trigger)

  1. LT 模式

當 epoll_wait() 檢測到描述符事件到達時,將此時間通知程式,程式可以不立即處理該事件,下次呼叫 epoll_wait() 時會再次通知程式,這是預設一種模式,並且同時支援阻塞和非阻塞

  1. ET 模式

和 LT 模式不同的是,通知之後必須立即處理事件,下次再呼叫 epoll_wait() 不會再得到時間到達的通知。

減少了 epoll 事件被重複觸發的次數,因此效率比 LT 高,只支援非阻塞式,以避免由於一個檔案控制程式碼的阻塞讀/阻塞寫操作把處理多個檔案描述符的任務餓死。

應用場景

通過上面的對比,很容易理解是既然 epoll 這麼強大,那麼都使用 epoll 不就夠了?實際上不是這樣的,其實都各自有各自的使用場景

  1. select 使用場景
  • select 的 timeout 精度為 1ns,而其他兩種為 1ms,所以 select 更適用於實時要求很高的場景,比如核反應堆的控制
  • select 可移植性好,幾乎被所有主流平臺支援
  1. poll 使用場景
  • poll 與 select 相比沒有最大描述符數量的限制,並且如果平臺對實時性要求不是很高,一般使用poll
  • 需要同時監控小於 1000 個描述符,就沒必要使用 epoll,因為這個應用場景下並不能體現 epoll 的優勢
  • 需要監控的描述符狀態變化多,而且都是非常短暫的,也沒有必要使用 epoll,因為 epoll 中的所有描述符都是儲存在核心中,造成每次對描述符狀態的改變都需要通過系統呼叫,頻繁系統呼叫肯定會降低效率,並且 epoll 的描述符儲存在核心中不容易除錯
  1. epoll 使用場景

只需要執行在 Linux 平臺,並且有非常大量的描述符需要同時輪詢,而且這些連線最好是長連線

參考

CS-Notes

IO - 同步,非同步,阻塞,非阻塞 (亡羊補牢篇)

大話 Select、Poll、Epoll

Linux下I/O多路複用系統呼叫(select, poll, epoll)介紹

相關文章