最近在學習 Java NIO 方面的知識,爲了加深理解。特意去看了 Unix/Linux I/O 方面的知識,並寫了一些代碼進行驗證。在本文接下來的一章中,我將經過舉例的方式向你們介紹五種 I/O 模型。若是你們是第一次瞭解 I/O 模型方面的知識,理解起來會有必定的難度。因此在看文章的同時,我更建議你們動手去實現這些 I/O 模型,感受會不同。好了,下面我們一塊兒進入正題吧。git
本章將向你們介紹五種 I/O 模型,包括阻塞 I/O、非阻塞 I/O、I/O 複用、信號驅動式 I/O 、異步 I/O 等。本文的內容參考了《UNIX網絡編程》,文中所用部分圖片也是來自於本書。關於《UNIX網絡編程》這本書,我想就不用多說了。不少寫網絡編程方面的文章通常都會參考該書,本文也不例外。若是你們想進深刻學習網絡編程,建議去讀讀這本書。github
阻塞 I/O 是最簡單的 I/O 模型,通常表現爲進程或線程等待某個條件,若是條件不知足,則一直等下去。條件知足,則進行下一步操做。相關示意圖以下:編程
上圖中,應用進程經過系統調用 recvfrom 接收數據,但因爲內核還未準備好數據報,應用進程就阻塞住了。直到內核準備好數據報,recvfrom 完成數據報復制工做,應用進程才能結束阻塞狀態。緩存
這裏簡單解釋一下應用進程和內核的關係。內核即操做系統內核,用於控制計算機硬件。同時將用戶態的程序和底層硬件隔離開,以保障整個計算機系統的穩定運轉(若是用戶態的程序能夠控制底層硬件,那麼一些病毒就會針對硬件進行破壞,好比 CIH 病毒)。應用進程即用戶態進程,運行於操做系統之上,經過系統調用與操做系統進行交互。上圖中,內核指的是 TCP/IP 等協議及相關驅動程序。客戶端發送的請求,並非直接送達給應用程序,而是要先通過內核。內核將請求數據緩存在內核空間,應用進程經過 recvfrom 調用,將數據從內核空間拷貝到本身的進程空間內。大體示意圖以下:服務器
阻塞 I/O 理解起來並不難,不過這裏仍是舉個例子類比一下。假設你們平常工做流程設這樣的(其實就是我平常工做的流程?),咱們寫好代碼後,本地測試無誤,經過郵件的方式,告知運維同窗發佈服務。運維同窗經過發佈腳本打包代碼,重啓服務(心疼我司的人肉運維)。通常項目比較大時,重啓一次比較耗時。而運維同窗又有點死腦筋,非要等這個服務重啓好,再去作其餘事。結果一天等待的時間比真正工做的時間還要長,而後就被開了。運維同窗用這個例子告訴咱們,阻塞式 I/O 效率不太好。網絡
與阻塞 I/O 模型相反,在非阻塞 I/O 模型下。應用進程與內核交互,目的未達到時,再也不一味的等着,而是直接返回。而後經過輪詢的方式,不停的去問內核數據準備好沒。示意圖以下:運維
上圖中,應用進程經過 recvfrom 系統調用不停的去和內核交互,直到內核準備好數據報。從上面的流程中能夠看出,應用進程進入輪詢狀態時等同於阻塞,因此非阻塞的 I/O 彷佛並無提升進程工做效率。異步
再用上面的例子進行類比。公司辭退了上一個怠工的運維同窗後,又招了一個運維同窗。這個運維同窗每次重啓服務,隔一分鐘去看一下,而後進入發呆狀態。雖然真正的工做時間增長了,可是沒用啊,等待的時間仍是太長了。被公司發現後,又被辭了。socket
Unix/Linux 環境下的 I/O 複用模型包含三組系統調用,分別是 select、poll 和 epoll(FreeBSD 中則爲 kqueue)。select 出現的時間最先,在 BSD 4.2中被引入。poll 則是在 AT&T System V UNIX 版本中被引入(詳情請參考 UNIX man-page)。epoll 出如今 Linux kernel 2.5.44 版本中,與之對應的 kqueue 調用則出如今 FreeBSD 4.1,早於 epoll。select 和 poll 出現的時間比較早,在當時也是比較先進的 I/O 模型了,知足了當時的需求。不過隨着因特網用戶的增加,C10K 問題出現。select 和 poll 已經不能知足需求了,研發更加高效的 I/O 模型迫在眉睫。到了 2000 年,FreeBSD 率先發布了 select、poll 的改進版 kqueue。Linux 平臺則在 2002 年 2.5.44 中發佈了 epoll。好了,關於三者的一些歷史就說到這裏。本節接下來將以 select 函數爲例,簡述該函數的使用過程。tcp
select 有三個文件描述符集(readfds),分別是可讀文件描述符集(writefds)、可寫文件描述符集和異常文件描述符集(exceptfds)。應用程序可將某個 socket (文件描述符)設置到感興趣的文件描述符集中,並調用 select 等待所感興趣的事件發生。好比某個 socket 處於可讀狀態了,此時應用進程就可調用 recvfrom 函數把數據從內核空間拷貝到進程空間內,無需再等待內核準備數據了。示意圖以下:
通常狀況下,應用進程會將多個 socket 設置到感興趣的文件描述符集中,並調用 select 等待所關注的事件(好比可讀、可寫)處於就緒狀態。當某些 socket 處於就緒狀態後,select 返回處於就緒狀態的 sockct 數量。注意這裏返回的是 socket 的數量,並非具體的 socket。應用程序須要本身去肯定哪些 socket 處於就緒狀態了,肯定以後便可進行後續操做。
I/O 複用自己不是很好理解,因此這裏仍是舉例說明吧。話說公司的運維部連續辭退兩個運維同窗後,運維部的 leader 以爲須要親自監督一下你們工做。因而 leader 在週會上和你們說,從下週開始,全部的發佈郵件都由他接收,並由他轉發給相關運維同窗,同時也由他重啓服務。各位運維同窗須要告訴 leader 各自所負責監控的項目,服務重啓好後,leader 會經過內部溝通工具通知相關運維同窗。至於服務重啓的結果(成功或失敗),leader 不關心,須要運維同窗本身去看。運維同窗看好後,須要把結果回覆給開發同窗。
上面的流程可能有點囉嗦,因此仍是看圖吧。
把上面的流程進行分步,以下:
這種方式爲何能夠提升工做效率呢?緣由在於運維同窗一股腦把他所負責的幾十個項目都告訴了 leader,由 leader 重啓服務,並通知運維同窗。運維同窗這個時候等待 leader 的通知,只要其中一個或幾個服務重啓好了,運維同窗就回接到通知,而後就可去幹活了。而不是像之前同樣,非要等某個服務重啓好再進行後面的工做。
說一下上面例子的角色扮演。開發同窗是客戶端,leader 是內核。開發同窗發的郵件至關於網絡請求,leader 接收郵件,並重啓服務,至關於內核準備數據。運維同窗是服務端應用進程,告訴 leader 本身感興趣的事情,並在最後將事情的處理結果返回給開發同窗。
不知道你們有沒有理解上面的例子,I/O 複用自己可能就不太好理解,因此看不懂也不要氣餒。另外,上面的例子只是爲了說明狀況,現實中並不會是這樣幹,否則 leader 要累死了。若是你們以爲上面的例子不太好,我建議你們去看看權威資料《UNIX網絡編程》。同時,若是能用 select 寫個簡單的 tcp 服務器,有助於加深對 I/O 複用的理解。若是不會寫,也能夠參考我寫的代碼 select_server.c。
信號驅動式 I/O 模型是指,應用進程告訴內核,若是某個 socket 的某個事件發生時,請向我發一個信號。在收到信號後,信號對應的處理函數會進行後續處理。示意圖以下:
再用以前的例子進行說明。某個運維同窗比較聰明,他寫了一個監控系統。重啓服務的過程由監控系統來作,作好後,監控系統會給他發個通知。在此以前,運維同窗能夠去作其餘的事情,不用一直髮呆等着了。運維同窗收到通知後,首先去檢查服務重啓狀況,接着再給開發同窗回覆郵件就好了。
相比以前的工做方式,是否是感受這種方式更合理。從流程上來講,這種方式確實更合理。進程在信號到來以前,能夠去作其餘事情,而不用忙等。但現實中,這種 I/O 模型用的並很少。
異步 I/O 是指應用進程把文件描述符傳給內核後,啥都無論了,徹底由內核去操做這個文件描述符。內核完成相關操做後,會發信號告訴應用進程,某某 I/O 操做我完成了,你如今能夠進行後續操做了。示意圖以下:
上圖經過 aio_read 把文件描述符、數據緩存空間,以及信號告訴內核,當文件描述符處於可讀狀態時,內核會親自將數據從內核空間拷貝到應用進程指定的緩存空間呢。拷貝完在告訴進程 I/O 操做結束,你能夠直接使用數據了。
接着上一節的例子進行類比,運維小哥升級了他的監控系統。此時,監控系統不光能夠監控服務重啓狀態,還能把重啓結果整理好,發送給開發小哥。而運維小哥要作的事情就更簡單了,收收郵件,點點監控系統上的發佈按鈕。而後就能夠悠哉悠哉的繼續睡覺了,一天一天的就這麼過去了。
上面介紹了5種 I/O 模型,也經過舉例的形式對每種模型進行了補充說明,不知道你們看懂沒。拋開上面的 I/O 模型不談,若是某種 I/O 模型能讓進程的工做的時間大於等待的時間,那麼這種模型就是高效的模型。在服務端請求量變大時,經過 I/O 複用模型可讓進程進入繁忙的工做狀態中,減小忙等,進而提升了效率。
I/O 複用模型結果數次改進,目前性能已經很好了,也獲得了普遍應用。像 Nginx,lighttd 等服務器軟件都選用該模型。好了,關於 I/O 模型就說到這裏。
最後附一張幾種 I/O 模型的對比圖:
前面簡述了幾種 I/O 模型,並輔以例子進行說明。關於 I/O 模型的文章,網上有不少。你們也是各開腦洞,用了不一樣的例子進行類比說明,包括但不限於送外賣、送快遞、飛機調度等等。在寫這篇文章前,我也是絞盡腦汁,但願想一個不一樣的例子,否則若是和別人的太像,免不了有抄襲的嫌疑。除此以外,舉的例子還要儘可能是你們都知道的,同時又能說明問題。因此這篇文章想例子想的也是挺累的。另外,限於本人語言水平,文中有些地方可能未能描述清楚。若是給你們形成了困擾,在這裏說聲抱歉。最後聲明一下,本文的例子拿運維同窗舉例,本人並沒有意黑運維同窗。咱們公司運維自動化程度不高,運維同事們仍是很辛苦的,心疼5分鐘。
好了,本文到這裏就結束了,謝謝你們閱讀!
本文在知識共享許可協議 4.0 下發布,轉載需在明顯位置處註明出處
做者:coolblog
本文同步發佈在個人我的博客: http://www.coolblog.xyz
本做品採用知識共享署名-非商業性使用-禁止演繹 4.0 國際許可協議進行許可。