在講解IO多路複用以前,咱們須要預習一下文件以及文件描述符。linux
程序員使用I/O最終都逃不過文件。程序員
由於這篇同屬於高性能、高併發系列,講到高性能、高併發就離不開Linux/Unix,所以這裏就來討論一下Linux世界中的文件。web
實際上對於程序員來講文件是一個很簡單的概念,咱們只須要將其理解爲一個N byte的序列就能夠了:編程
<center>b1, b2, b3, b4, ....... bN</center>服務器
實際上全部的I/O設備都被抽象爲了文件這個概念,一切皆文件,Everything isFile,磁盤、網絡數據、終端,甚至進程間通訊工具管道pipe等都被當作文件對待。網絡
全部的I/O操做也都是經過文件讀寫來實現的,這一很是優雅的抽象可讓程序員使用一套接口就能實現全部I/O操做。多線程
經常使用的I/O操做接口通常有如下幾類:併發
程序員經過這幾個接口幾乎能夠實現全部I/O操做,這就是文件這個概念的強大之處。socket
在本篇第二節I/O過程當中咱們講到,要想讀取好比磁盤數據咱們須要指定一個buff用來裝入數據,是這樣用的:函數
read(buff);
可是這裏咱們忽略了一個問題,那就是雖然咱們執行了往哪裏寫數據,可是咱們該從哪裏讀數據呢?從上一節中咱們知道,經過文件這個概念咱們能實現幾乎全部I/O操做,所以這裏少的一個主角就是文件。
那麼咱們通常都這麼使用文件呢?
若是你週末去比較火的餐廳吃飯應該會有體會,通常週末這樣的餐廳都會排隊,而後服務員會給你一個排隊序號,經過這個序號服務員就能找到你,這裏的好處就是服務員無需記住你是誰、你的名字是什麼、是否是保護環境愛好小動物等等,這裏的關鍵點就是服務員對你一無所知,可是依然能夠經過一個號碼就能找到你。
一樣的,在Linux世界使用文件,咱們也須要藉助一個號碼,根據「弄不懂原則」,這個號碼就被稱爲了文件描述符file descriptors,在Linux世界中鼎鼎大名,其道理和上面那個排隊號碼同樣。
所以,文件描述僅僅就是一個數字而已,可是經過這個數字咱們能夠操做一個打開的文件,這一點要記住。
有了文件描述符,進程對文件一無所知,好比文件在磁盤的什麼位置上、內存是如何管理文件的等等,這些信息屬於操做系統,進程無需關心,操做系統只須要給進程一個文件描述符就足夠了。
所以咱們來完善上述程序:
int fd = open(file_name); read(fd, buff);
怎麼樣,是否是很是簡單。
通過了這麼多的鋪墊,終於到高性能、高併發這一主題了。
從前幾節咱們知道,全部I/O操做均可以經過文件樣的概念來進行,這固然包括網絡通訊。
若是你是一個web服務器,當三次握手成功之後,咱們經過調用accept一樣會獲得一個文件描述符,只不過這個文件描述符是用來進行網絡通訊的,經過讀寫該文件描述符你就能夠同客戶端通訊。在這裏爲了概念上好理解,咱們稱之爲連接描述符,經過這個描述符咱們就能夠讀寫客戶端的數據了。
int conn_fd = accept(...);
server的處理邏輯一般是讀取客戶端請求數據,而後執行某些特定邏輯:
if(read(conn_fd, request_buff) > 0) { do_something(request_buff); }
是否是很是簡單,然而世界終歸是複雜的,也不是這麼簡單的。
接下來就是比較複雜的了。
既然咱們的主題是高併發,那麼server端就不可能只和一個客戶端通訊,而是成千上萬個客戶端。這時你須要處理再也不是一個描述符這麼簡單,而是有可能要處理成千上萬個描述符。
爲了避免讓問題一上來就過於複雜,咱們先簡單化,假設只同時處理兩個客戶端的請求。
有的同窗可能會說,這還不簡單,這樣寫不就好了:
if(read(socket_fd1, buff) > 0) { // 處理第一個 do_something(); } if(read(socket_fd2, buff) > 0) { do_something();
在本篇第二節中咱們討論過這是很是典型的阻塞式I/O,若是讀取第一個請求進程被阻塞而暫停運行,那麼這時咱們就沒法處理第二個請求了,即便第二個請求的數據已經就位,這也就意味着全部其它客戶端必須等待,並且一般狀況下也不會只有兩個客戶端而是成千上萬個,上萬個鏈接也要這樣串行處理嗎。
聰明的你必定會想到使用多線程,爲每一個請求開啓一個線程,這樣一個線程被阻塞不會影響到其它線程了,注意,既然是高併發,那麼咱們要爲成千上萬個請求開啓成千上萬個線程嗎,大量建立銷燬線程會嚴重影響系統性能。
那麼這個問題該怎麼解決呢?
這裏的關鍵點在於在進行I/O時,咱們並非到該文件描述對於的I/O設備是不是可讀的、是不是可寫的,在外設的不可讀或不可寫的狀態下進行I/O只會致使進程阻塞被暫停運行。
所以要優雅的解決這個問題,就要從其它角度來思考這個問題了。
你們生活中確定會接到過推銷電話,並且不止一個,一天下來接上十個八個推銷電話你的身體會被掏空的。
這個場景的關鍵點在於打電話的人並不知道你是否是要買東西,只能來一遍遍問你,所以一種更好的策略是不要讓他們打電話給你,記下他們的電話,有須要的話打給他們。
也就是不要打電話給我,有須要我會打給你。
在這個例子中,你,就比如內核,推銷者就比如應用程序,電話號碼就比如文件描述符,和你用電話溝通就比如I/O。
如今你應該明白了吧,處理多個文件描述符的更好方法其實就存在於推銷電話中。
所以相比上一節中咱們主動經過I/O接口主動問內核這些文件描述符對應的外設是否是已經就緒了,一種更好的方法是,咱們把這些內核一股腦扔給內核,並霸氣的告訴內核:「我這裏有1萬個文件描述符,你替我監視着它們,有能夠讀寫的文件描述符時你就告訴我,我好處理」。而不是弱弱的問內核:「第一個文件描述能夠讀寫了嗎?第二個文件描述符能夠讀寫嗎?第三個文件描述符能夠讀寫了嗎?」
這樣應用程序就從「繁忙」的主動變爲悠閒的被動了,反正哪些設備ok了內核會通知我, 能偷懶我纔不要那麼勤奮。
這是一種不一樣的處理I/O的機制,一樣須要起一個名字,再次祭出「弄不懂原則」,就叫I/O多路複用吧,這就是 I/O multiplexing。
multiplexing一詞其實多用於通訊領域,爲了充分利用通訊線路,但願在一個信道中傳輸多路信號,要想在一個信道中傳輸多路信號就須要把這多路信號結合爲一路,將多路信號組合成一個信號的設備被稱爲multiplexer,顯然接收方接收到這一路組合後的信號後要恢復原先的多路信號,這個設備被稱爲demultiplexer,如圖所示:
回到咱們的主題。
所謂I/O多路複用指的是這樣一個過程:
那麼有哪些函數能夠用來進行I/O多路複用呢?
在Linux世界中有這樣三種機制能夠用來進行I/O多路複用:
接下來咱們就簡單介紹一下牛掰的I/O多路複用三劍客。
本質上select、poll、epoll都是阻塞式I/O,也就是咱們常說的同步I/O。
在select這種I/O多路複用機制下,咱們須要把想監控的文件描述集合經過函數參數的形式告訴select,而後select會將這些文件描述符集合拷貝到內核中,咱們知道數據拷貝是有性能損耗的,所以爲了減小這種數據拷貝帶來的性能損耗,Linux內核對集合的大小作了限制,並規定用戶監控的文件描述集合不能超過1024個,同時當select返回後咱們僅僅能知道有些文件描述符能夠讀寫了,可是咱們不知道是哪個,所以程序員必須再遍歷一邊找到具體是哪一個文件描述符能夠讀寫了。
所以,總結下來select有這樣幾個特色:
所以咱們能夠看到,select機制的特性在高性能網絡服務器動輒幾萬幾十萬併發連接的場景下無疑是低效的。
poll和select是很是類似的,poll相對於select的優化僅僅在於解決了文件描述符不能超過1024個的限制,select和poll都會隨着監控的文件描述增長而出現性能降低,所以不適合高併發場景。
在select面臨的三個問題中,文件描述數量限制已經在poll中解決了,那麼剩下的兩個問題epoll是經過什麼技術巧妙解決的呢?這個問題你能夠關注公衆號「碼農的荒島求生」並回復"epoll"就能獲得答案啦。
基於一切皆文件的設計哲學,I/O也能夠經過文件的形式實現,顯然高併發要與多個文件交互,這就離不開高效的I/O多路複用技術,本文咱們詳細講解了什麼是I/O多路複用以及使用方法,這其中以epoll爲表明的I/O多路複用(基於事件驅動)技術使用很是普遍,實際上你會發現但凡涉及到高併發、高性能都能見到事件驅動的編程方法,這也是下一篇的主題,敬請期待。