NIO详解(三):IO多路复用模型之select、poll、epoll
1. 前言
最近在研究基于Java的高性能異步非阻塞I/O框架Netty,因為最近做的RDMAChannel要用到其中的思想。Netty底層是通過大量的NIO實現的,通過分析底層NIO源碼,發現NIO底層調用的是poll系統調用。所以本博客就來細談select、poll、epoll系統調用。
2. NIO Selector底層源碼分析
protected int doSelect(long timeout) throws IOException {if (channelArray == null)throw new ClosedSelectorException();this.timeout = timeout; // set selector timeoutprocessDeregisterQueue();if (interruptTriggered) {resetWakeupSocket();return 0;}// Calculate number of helper threads needed for poll. If necessary// threads are created here and start waiting on startLockadjustThreadsCount();finishLock.reset(); // reset finishLock// Wakeup helper threads, waiting on startLock, so they start polling.// Redundant threads will exit here after wakeup.startLock.startThreads();// do polling in the main thread. Main thread is responsible for// first MAX_SELECTABLE_FDS entries in pollArray.try {begin();try {subSelector.poll();} catch (IOException e) {finishLock.setException(e); // Save this exception}// Main thread is out of poll(). Wakeup others and wait for themif (threads.size() > 0)finishLock.waitForHelperThreads();} finally {end();}// Done with poll(). Set wakeupSocket to nonsignaled for the next run.finishLock.checkForException();processDeregisterQueue();int updated = updateSelectedKeys();// Done with poll(). Set wakeupSocket to nonsignaled for the next run.resetWakeupSocket();return updated;}其中 subSelector.poll() 是select的核心,由native函數poll0實現,readFds、writeFds 和exceptFds數組用來保存底層select的結果,數組的第一個位置都是存放發生事件的socket的總數,其余位置存放發生事件的socket句柄fd。
在早期的JDK1.4和1.5 update10版本之前,Selector基于select/poll模型實現,是基于IO復用技術的非阻塞IO,不是異步IO。在JDK1.5 update10和linux core2.6以上版本,sun優化了Selctor的實現,底層使用epoll替換了select/poll。
2. I/O多路復用模型(I/O Multiplexing)
解決問題:如果一個I/O流進來,我們就開啟一個進程處理這個I/O流。那么假設現在有一百萬個I/O流進來,那我們就需要開啟一百萬個進程一一對應處理這些I/O流(——這就是傳統意義下的多進程并發處理)。思考一下,一百萬個進程,你的CPU占有率會多高,這個實現方式及其的不合理。所以人們提出了I/O多路復用這個模型,一個線程,通過記錄I/O流的狀態來同時管理多個I/O,可以提高服務器的吞吐能力。
3. select、poll、epoll簡介
select,poll,epoll都是IO多路復用的機制。I/O多路復用就通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。但select,poll,epoll本質上都是同步I/O,因為他們都需要在讀寫事件就緒后自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。
3.1 select實現
在這種模式中,首先不是進行read系統調動,而是進行select系統調用。當然,這里有一個前提,需要將目標網絡連接,提前注冊到select的可查詢socket列表中。然后,才可以開啟整個的IO多路復用模型的讀流程。(1)進行select系統調用,查詢可以讀的連接。kernel會查詢所有select的可查詢socket列表,當任何一個socket中的數據準備好了,select就會返回。當用戶進程調用了select,那么整個線程會被block(阻塞掉)。(2)用戶線程獲得了目標連接后,發起read系統調用,用戶線程阻塞。內核開始復制數據。它就會將數據從kernel內核緩沖區,拷貝到用戶緩沖區(用戶內存),然后kernel返回結果。(3)用戶線程才解除block的狀態,用戶線程終于真正讀取到數據,繼續執行。
select的調用過程如下所示:
__pollwait的主要工作就是把current(當前進程)掛到設備的等待隊列中,不同的設備有不同的等待隊列,對于tcp_poll來說,其等待隊列是sk->sk_sleep(注意把進程掛到等待隊列中并不代表進程已經睡眠了)。一旦設備收到一條消息(網絡設備)或填寫完文件數據(磁盤設備)后,會喚醒設備等待隊列上睡眠的進程,這時current便被喚醒了。用戶線程可以通過相應的fd讀取到相應的可讀socket中的數據了。
poll方法返回時會返回一個描述讀寫操作是否就緒的mask掩碼,根據這個mask掩碼給fd_set賦值。如果遍歷完所有的fd,還沒有返回一個可讀寫的mask掩碼,則會調用schedule_timeout是調用select的進程(也就是current)進入睡眠。當設備驅動發生自身資源可讀寫后,會喚醒其等待隊列上睡眠的進程,然后進程就會通過相應的fd讀取到相應的可讀的socket中的數據了。如果超過一定的超時時間(schedule_timeout指定),還是沒人喚醒,則調用select的進程會重新被喚醒獲得CPU,進而重新遍歷fd,判斷有沒有就緒的fd。
總結:
select的幾大缺點:
- 每次調用select,都需要把fd集合從用戶態拷貝到內核態,這個開銷在fd很多時會很大
- 同時每次調用select都需要在內核遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大
- select支持的文件描述符數量太小了,默認是1024
3.2 poll實現
poll的實現和select非常相似,只是描述fd集合的方式不同,poll使用pollfd結構而不是select的fd_set結構,其他的都差不多。但是它沒有最大連接數的限制,原因是它是基于鏈表來存儲的.
3.3 epoll實現
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實現中的是類似的)
對于第三個缺點,epoll沒有這個限制,它所支持的FD上限是最大可以打開文件的數目,這個數字一般遠大于2048,舉個例子,在1GB內存的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統內存關系很大。
總結:
(1)epoll其實也需要調用epoll_wait不斷輪詢就緒鏈表,期間也可能多次睡眠和喚醒交替,但是它是設備就緒時,調用回調函數,把就緒fd放入就緒鏈表中,并喚醒在epoll_wait中進入睡眠的進程。雖然都要睡眠和交替,但是select和poll在“醒著”的時候要遍歷整個fd集合,而epoll在“醒著”的時候只要判斷一下就緒鏈表是否為空就行了,這節省了大量的CPU時間。這就是回調機制(Callback)帶來的性能提升。
(2)select,poll每次調用都要把fd集合從用戶態往內核態拷貝一次,并且要把current往設備等待隊列中掛一次,而epoll只要一次拷貝,而且把current往等待隊列上掛也只掛一次(在epoll_wait的開始,注意這里的等待隊列并不是設備等待隊列,只是一個epoll內部定義的等待隊列)。這也能節省不少的開銷。
總結
以上是生活随笔為你收集整理的NIO详解(三):IO多路复用模型之select、poll、epoll的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 计算机网络:浅谈HTTPS和加密
- 下一篇: Redis:主从复制原理