IO多路複用原理剖析

Panda_XiaoXi發表於2017-11-01

(最近筆試遇到筆試題:select,poll,epoll都是IO多路複用的機制)。
I/O多路複用就通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程式進行相應的讀寫操作。但select,poll,epoll本質上都是同步I/O,因為他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而非同步I/O則無需自己負責進行讀寫,非同步I/O的實現會負責把資料從核心拷貝到使用者空間。關於這三種IO多路複用的用法,前面三篇總結寫的很清楚,並用伺服器回射echo程式進行了測試。

IO多路複用之select總結

IO多路複用之poll總結

IO多路複用之epoll總結

連線如下所示:

對IO的多路複用的整理:

1、select的呼叫過程如下圖:

(1)使用copy_from_user從使用者空間拷貝fd_set到核心空間
(2)註冊回撥函式__pollwait
(3)遍歷所有fd,呼叫其對應的poll方法(對於socket,這個poll方法是sock_poll,sock_poll根據情況會呼叫到tcp_poll,udp_poll或者datagram_poll)
(4)以tcp_poll為例,其核心實現就是__pollwait,也就是上面註冊的回撥函式。
(5)__pollwait的主要工作就是把current(當前程式)掛到裝置的等待佇列中,不同的裝置有不同的等待佇列,對於tcp_poll來說,其等待佇列是sk->sk_sleep(注意把程式掛到等待佇列中並不代表程式已經睡眠了)。在裝置收到一條訊息(網路裝置)或填寫完檔案資料(磁碟裝置)後,會喚醒裝置等待佇列上睡眠的程式,這時current便被喚醒了。
(6)poll方法返回時會返回一個描述讀寫操作是否就緒的mask掩碼,根據這個mask掩碼給fd_set賦值。
(7)如果遍歷完所有的fd,還沒有返回一個可讀寫的mask掩碼,則會呼叫schedule_timeout是呼叫select的程式(也就是current)進入睡眠。當裝置驅動發生自身資源可讀寫後,會喚醒其等待佇列上睡眠的程式。如果超過一定的超時時間(schedule_timeout指定),還是沒人喚醒,則呼叫select的程式會重新被喚醒獲得CPU,進而重新遍歷fd,判斷有沒有就緒的fd。
(8)把fd_set從核心空間拷貝到使用者空間。複製程式碼

select的幾大缺點:

(1)每次呼叫select,都需要把fd集合從使用者態拷貝到核心態,這個開銷在fd很多時會很大
(2)同時每次呼叫select都需要在核心遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大
(3)select支援的檔案描述符數量太小了,預設是1024複製程式碼

2、poll實現

  poll的實現和select非常相似,只是描述fd集合的方式不同,poll使用pollfd結構而不是select的fd_set結構,其他的都差不多。

關於select和poll的實現分析,可以參考下面幾篇博文:

select(poll)系統呼叫實現解析(一)

select(poll)系統呼叫實現解析(二)

select(poll)系統呼叫實現解析(三)

使用事件驅動模型實現高效穩定的網路伺服器程式
幾種網路伺服器模型的介紹與比較

Select函式實現原理分析

3、epoll

  epoll既然是對select和poll的改進,就應該能避免上述的三個缺點。那epoll都是怎麼解決的呢?在此之前,我們先看一下epoll和select和poll的呼叫介面上的不同,select和poll都只提供了一個函式——select或者poll函式。而epoll提供了三個函式,epoll_create,epoll_ctl和epoll_wait,epoll_create是建立一個epoll控制程式碼;epoll_ctl是註冊要監聽的事件型別;epoll_wait則是等待事件的產生。

  對於第一個缺點,epoll的解決方案在epoll_ctl函式中。每次註冊新的事件到epoll控制程式碼中時(在epoll_ctl中指定EPOLL_CTL_ADD),會把所有的fd拷貝進核心,而不是在epoll_wait的時候重複拷貝。epoll保證了每個fd在整個過程中只會拷貝一次。

  對於第二個缺點,epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對應的裝置等待佇列中,而只在epoll_ctl時把current掛一遍(這一遍必不可少)併為每個fd指定一個回撥函式,當裝置就緒,喚醒等待佇列上的等待者時,就會呼叫這個回撥函式,而這個回撥函式會把就緒的fd加入一個就緒連結串列)。epoll_wait的工作實際上就是在這個就緒連結串列中檢視有沒有就緒的fd(利用schedule_timeout()實現睡一會,判斷一會的效果,和select實現中的第7步是類似的)。

  對於第三個缺點,epoll沒有這個限制,它所支援的FD上限是最大可以開啟檔案的數目,這個數字一般遠大於2048,舉個例子,在1GB記憶體的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統記憶體關係很大。

總結:

(1)select,poll實現需要自己不斷輪詢所有fd集合,直到裝置就緒,期間可能要睡眠和喚醒多次交替。而epoll其實也需要呼叫epoll_wait不斷輪詢就緒連結串列,期間也可能多次睡眠和喚醒交替,但是它是裝置就緒時,呼叫回撥函式,把就緒fd放入就緒連結串列中,並喚醒在epoll_wait中進入睡眠的程式。雖然都要睡眠和交替,但是select和poll在“醒著”的時候要遍歷整個fd集合,而epoll在“醒著”的時候只要判斷一下就緒連結串列是否為空就行了,這節省了大量的CPU時間。這就是回撥機制帶來的效能提升。

(2)select,poll每次呼叫都要把fd集合從使用者態往核心態拷貝一次,並且要把current往裝置等待佇列中掛一次,而epoll只要一次拷貝,而且把current往等待佇列上掛也只掛一次(在epoll_wait的開始,注意這裡的等待佇列並不是裝置等待佇列,只是一個epoll內部定義的等待佇列)。這也能節省不少的開銷。

參考資料:

select 原始碼實現分析

select,poll,epoll實現分析 — 結合核心原始碼

select,poll,epoll比較

select、poll、epoll使用小結

使用epoll的一個完整的c語言原始碼例子

相關文章