詳解IO複用模型select,poll,epoll機制
在Java中,主要有三種IO模型,分別是阻塞IO(BIO)、非阻塞IO(NIO)和 非同步IO(AIO)。
Java中提供的IO有關的API,在檔案處理的時候,其實依賴作業系統層面的IO操作實現的。比如在Linux 2.6以後,Java中NIO和AIO都是通過epoll來實現的,而在Windows上,AIO是通過IOCP來實現的,可以把Java中的BIO、NIO和AIO理解為是Java語言對作業系統的各種IO模型的封裝。
select,poll,epoll都是IO多路複用的機制。I/O多路複用就通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程式進行相應的讀寫操作。但select,poll,epoll本質上都是同步I/O,因為他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的。
1、select實現
select的呼叫過程如下所示:

image.png
- 使用copy_from_user從使用者空間拷貝fd_set到核心空間
- 註冊回撥函式__pollwait
- 遍歷所有fd,呼叫其對應的poll方法(對於socket,這個poll方法是sock_poll,sock_poll根據情況會呼叫到tcp_poll,udp_poll或者datagram_poll)
- 以tcp_poll為例,其核心實現就是__pollwait,也就是上面註冊的回撥函式。
- __pollwait的主要工作就是把current(當前程序)掛到裝置的等待佇列中,不同的裝置有不同的等待佇列,對於tcp_poll來說,其等待佇列是sk->sk_sleep(注意把程序掛到等待佇列中並不代表程序已經睡眠了)。在裝置收到一條訊息(網路裝置)或填寫完檔案資料(磁碟裝置)後,會喚醒裝置等待佇列上睡眠的程序,這時current便被喚醒了。
- poll方法返回時會返回一個描述讀寫操作是否就緒的mask掩碼,根據這個mask掩碼給fd_set賦值。
- 如果遍歷完所有的fd,還沒有返回一個可讀寫的mask掩碼,則會呼叫schedule_timeout是呼叫select的程序(也就是current)進入睡眠。當裝置驅動發生自身資源可讀寫後,會喚醒其等待佇列上睡眠的程序。如果超過一定的超時時間(schedule_timeout指定),還是沒人喚醒,則呼叫select的程序會重新被喚醒獲得CPU,進而重新遍歷fd,判斷有沒有就緒的fd。
- 把fd_set從核心空間拷貝到使用者空間。
總結:
select的幾大缺點:
(1)每次呼叫select,都需要把fd集合從使用者態拷貝到核心態,這個開銷在fd很多時會很大
(2)同時每次呼叫select都需要在核心遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大
(3)select支援的檔案描述符數量太小,預設是1024
2、poll實現
poll的實現和select非常相似,只是描述fd集合的方式不同,poll使用pollfd結構而不是select的fd_set結構,其他的都差不多。
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察看,一般來說這個數目和系統記憶體關係很大。
epoll兩種工作方式
ET:Edge Triggered,邊緣觸發。僅當狀態發生變化時才會通知,epoll_wait返回。換句話,就是對於一個事件,只通知一次。且只支援非阻塞的socket。
LT:Level Triggered,電平觸發(預設工作方式)。類似select/poll,只要還有沒有處理的事件就會一直通知,以LT方式呼叫epoll介面的時候,它就相當於一個速度比較快的poll.支援阻塞和不阻塞的socket。
總結:
(1)select,poll實現需要自己不斷輪詢所有fd集合,直到裝置就緒,期間可能要睡眠和喚醒多次交替。而epoll其實也需要呼叫epoll_wait不斷輪詢就緒連結串列,期間也可能多次睡眠和喚醒交替,但是它是裝置就緒時,呼叫回撥函式,把就緒fd放入就緒連結串列中,並喚醒在epoll_wait中進入睡眠的程序。雖然都要睡眠和交替,但是select和poll在“醒著”的時候要遍歷整個fd集合,而epoll在“醒著”的時候只要判斷一下就緒連結串列是否為空就行了,
epoll只會對"活躍"的socket進行操作---這是因為在核心實現中epoll是根據每個fd上面的callback函式實現的。只有"活躍"的socket才會主動的去呼叫 callback函式(把這個控制代碼加入佇列),其他idle狀態控制代碼則不會,在這點上,epoll實現了一個"偽"AIO。但是如果絕大部分的I/O都是“活躍的”,每個I/O埠使用率很高的話,epoll效率不一定比select高(可能是要維護佇列複雜),這節省了大量的CPU時間,這就是回撥機制帶來的效能提升。
(2)select,poll每次呼叫都要把fd集合從使用者態往核心態拷貝一次,並且要把current往裝置等待佇列中掛一次,而epoll只要一次拷貝(epoll是通過核心於使用者空間mmap同一塊記憶體實現加速核心與使用者空間的訊息傳遞),而且把current往等待佇列上掛也只掛一次(在epoll_wait的開始,注意這裡的等待佇列並不是裝置等待佇列,只是一個epoll內部定義的等待佇列),這也能節省不少的開銷。
參考: ofollow,noindex">https://blog.csdn.net/hsy12342611/article/details/51086532
技術討論 & 疑問建議 & 個人部落格
版權宣告: 本部落格所有文章除特別宣告外,均採用 CC BY-NC-SA 3.0 許可協議,轉載請註明出處!