在講解IO多路復用之前,我們需要預習一下文件以及文件描述符睬棚。
什么是文件
程序員使用I/O最終都逃不過文件乍惊。
因為這篇同屬于高性能杜秸、高并發(fā)系列,講到高性能润绎、高并發(fā)就離不開Linux/Unix撬碟,因此這里就來討論一下Linux世界中的文件。
實際上對于程序員來說文件是一個很簡單的概念莉撇,我們只需要將其理解為一個N byte的序列就可以了:
<center>b1, b2, b3, b4, ....... bN</center>
實際上所有的I/O設備都被抽象為了文件這個概念呢蛤,一切皆文件,Everything isFile棍郎,磁盤其障、網(wǎng)絡數(shù)據(jù)、終端涂佃,甚至進程間通信工具管道pipe等都被當做文件對待静秆。
所有的I/O操作也都是通過文件讀寫來實現(xiàn)的,這一非常優(yōu)雅的抽象可以讓程序員使用一套接口就能實現(xiàn)所有I/O操作巡李。
常用的I/O操作接口一般有以下幾類:
- 打開文件,open
- 改變讀寫位置扶认,seek
- 文件讀寫侨拦,read、write
- 關閉文件辐宾,close
程序員通過這幾個接口幾乎可以實現(xiàn)所有I/O操作狱从,這就是文件這個概念的強大之處膨蛮。
文件描述符
在本篇第二節(jié)I/O過程中我們講到,要想讀取比如磁盤數(shù)據(jù)我們需要指定一個buff用來裝入數(shù)據(jù)季研,是這樣用的:
read(buff);
但是這里我們忽略了一個問題敞葛,那就是雖然我們執(zhí)行了往哪里寫數(shù)據(jù),但是我們該從哪里讀數(shù)據(jù)呢与涡?從上一節(jié)中我們知道惹谐,通過文件這個概念我們能實現(xiàn)幾乎所有I/O操作,因此這里少的一個主角就是文件驼卖。
那么我們一般都這么使用文件呢氨肌?
如果你周末去比較火的餐廳吃飯應該會有體會,一般周末這樣的餐廳都會排隊酌畜,然后服務員會給你一個排隊序號怎囚,通過這個序號服務員就能找到你,這里的好處就是服務員無需記住你是誰桥胞、你的名字是什么恳守、是不是保護環(huán)境愛好小動物等等,這里的關鍵點就是服務員對你一無所知贩虾,但是依然可以通過一個號碼就能找到你催烘。
同樣的,在Linux世界使用文件整胃,我們也需要借助一個號碼颗圣,根據(jù)“弄不懂原則”,這個號碼就被稱為了文件描述符file descriptors屁使,在Linux世界中鼎鼎大名在岂,其道理和上面那個排隊號碼一樣。
因此蛮寂,文件描述僅僅就是一個數(shù)字而已蔽午,但是通過這個數(shù)字我們可以操作一個打開的文件,這一點要記住酬蹋。
有了文件描述符及老,進程對文件一無所知,比如文件在磁盤的什么位置上范抓、內(nèi)存是如何管理文件的等等骄恶,這些信息屬于操作系統(tǒng),進程無需關心匕垫,操作系統(tǒng)只需要給進程一個文件描述符就足夠了僧鲁。
因此我們來完善上述程序:
int fd = open(file_name);
read(fd, buff);
怎么樣,是不是非常簡單。
文件描述符太多了怎么辦
經(jīng)過了這么多的鋪墊寞秃,終于到高性能斟叼、高并發(fā)這一主題了。
從前幾節(jié)我們知道春寿,所有I/O操作都可以通過文件樣的概念來進行朗涩,這當然包括網(wǎng)絡通信。
如果你是一個web服務器绑改,當三次握手成功以后谢床,我們通過調用accept同樣會得到一個文件描述符,只不過這個文件描述符是用來進行網(wǎng)絡通信的绢淀,通過讀寫該文件描述符你就可以同客戶端通信萤悴。在這里為了概念上好理解,我們稱之為鏈接描述符皆的,通過這個描述符我們就可以讀寫客戶端的數(shù)據(jù)了覆履。
int conn_fd = accept(...);
server的處理邏輯通常是讀取客戶端請求數(shù)據(jù),然后執(zhí)行某些特定邏輯:
if(read(conn_fd, request_buff) > 0) {
do_something(request_buff);
}
是不是非常簡單费薄,然而世界終歸是復雜的硝全,也不是這么簡單的。
接下來就是比較復雜的了楞抡。
既然我們的主題是高并發(fā)伟众,那么server端就不可能只和一個客戶端通信,而是成千上萬個客戶端召廷。這時你需要處理不再是一個描述符這么簡單凳厢,而是有可能要處理成千上萬個描述符。
為了不讓問題一上來就過于復雜竞慢,我們先簡單化先紫,假設只同時處理兩個客戶端的請求。
有的同學可能會說筹煮,這還不簡單遮精,這樣寫不就行了:
if(read(socket_fd1, buff) > 0) { // 處理第一個
do_something();
}
if(read(socket_fd2, buff) > 0) {
do_something();
在本篇第二節(jié)中我們討論過這是非常典型的阻塞式I/O,如果讀取第一個請求進程被阻塞而暫停運行败潦,那么這時我們就無法處理第二個請求了本冲,即使第二個請求的數(shù)據(jù)已經(jīng)就位,這也就意味著所有其它客戶端必須等待劫扒,而且通常情況下也不會只有兩個客戶端而是成千上萬個檬洞,上萬個連接也要這樣串行處理嗎。
聰明的你一定會想到使用多線程沟饥,為每個請求開啟一個線程疮胖,這樣一個線程被阻塞不會影響到其它線程了环戈,注意,既然是高并發(fā)澎灸,那么我們要為成千上萬個請求開啟成千上萬個線程嗎,大量創(chuàng)建銷毀線程會嚴重影響系統(tǒng)性能遮晚。
那么這個問題該怎么解決呢性昭?
這里的關鍵點在于在進行I/O時,我們并不是到該文件描述對于的I/O設備是否是可讀的县遣、是否是可寫的糜颠,在外設的不可讀或不可寫的狀態(tài)下進行I/O只會導致進程阻塞被暫停運行。
因此要優(yōu)雅的解決這個問題萧求,就要從其它角度來思考這個問題了其兴。
不要打電話給我,有需要我會打給你
大家生活中肯定會接到過推銷電話夸政,而且不止一個元旬,一天下來接上十個八個推銷電話你的身體會被掏空的。
這個場景的關鍵點在于打電話的人并不知道你是不是要買東西守问,只能來一遍遍問你匀归,因此一種更好的策略是不要讓他們打電話給你,記下他們的電話耗帕,有需要的話打給他們穆端。
也就是不要打電話給我,有需要我會打給你仿便。
在這個例子中体啰,你,就好比內(nèi)核嗽仪,推銷者就好比應用程序荒勇,電話號碼就好比文件描述符,和你用電話溝通就好比I/O钦幔。
現(xiàn)在你應該明白了吧枕屉,處理多個文件描述符的更好方法其實就存在于推銷電話中。
因此相比上一節(jié)中我們主動通過I/O接口主動問內(nèi)核這些文件描述符對應的外設是不是已經(jīng)就緒了鲤氢,一種更好的方法是搀擂,我們把這些內(nèi)核一股腦扔給內(nèi)核,并霸氣的告訴內(nèi)核:“我這里有1萬個文件描述符卷玉,你替我監(jiān)視著它們哨颂,有可以讀寫的文件描述符時你就告訴我,我好處理”相种。而不是弱弱的問內(nèi)核:“第一個文件描述可以讀寫了嗎威恼?第二個文件描述符可以讀寫嗎?第三個文件描述符可以讀寫了嗎?”
這樣應用程序就從“繁忙”的主動變?yōu)榍彘e的被動了箫措,反正哪些設備ok了內(nèi)核會通知我腹备, 能偷懶我才不要那么勤奮。
這是一種不同的處理I/O的機制斤蔓,同樣需要起一個名字植酥,再次祭出“弄不懂原則”,就叫I/O多路復用吧弦牡,這就是 I/O multiplexing友驮。
I/O多路復用,I/O multiplexing
multiplexing一詞其實多用于通信領域,為了充分利用通信線路,希望在一個信道中傳輸多路信號堤瘤,要想在一個信道中傳輸多路信號就需要把這多路信號結合為一路,將多路信號組合成一個信號的設備被稱為multiplexer耻瑟,顯然接收方接收到這一路組合后的信號后要恢復原先的多路信號,這個設備被稱為demultiplexer捻悯,如圖所示:
回到我們的主題匆赃。
所謂I/O多路復用指的是這樣一個過程:
- 我們拿到了一堆文件描述符(不管是網(wǎng)絡相關的、還是磁盤文件相關等等今缚,任何文件描述符都可以)
- 通過調用某個函數(shù)告訴內(nèi)核:“這個函數(shù)你先不要返回算柳,你替我監(jiān)視著這些描述符,當這堆文件描述符中有可以進行I/O讀寫操作的時候你再返回”
- 當調用的這個函數(shù)返回后我們就能知道哪些文件描述符可以進行I/O操作了姓言。
那么有哪些函數(shù)可以用來進行I/O多路復用呢瞬项?
在Linux世界中有這樣三種機制可以用來進行I/O多路復用:
- select
- poll
- epoll
接下來我們就簡單介紹一下牛掰的I/O多路復用三劍客。
I/O多路復用三劍客
本質上select何荚、poll囱淋、epoll都是阻塞式I/O,也就是我們常說的同步I/O餐塘。
select:初出茅廬
在select這種I/O多路復用機制下妥衣,我們需要把想監(jiān)控的文件描述集合通過函數(shù)參數(shù)的形式告訴select,然后select會將這些文件描述符集合拷貝到內(nèi)核中戒傻,我們知道數(shù)據(jù)拷貝是有性能損耗的税手,因此為了減少這種數(shù)據(jù)拷貝帶來的性能損耗,Linux內(nèi)核對集合的大小做了限制需纳,并規(guī)定用戶監(jiān)控的文件描述集合不能超過1024個芦倒,同時當select返回后我們僅僅能知道有些文件描述符可以讀寫了,但是我們不知道是哪一個不翩,因此程序員必須再遍歷一邊找到具體是哪個文件描述符可以讀寫了兵扬。
因此麻裳,總結下來select有這樣幾個特點:
- 我能照看的文件描述符數(shù)量有限,不能超過1024個
- 用戶給我的文件描述符需要拷貝的內(nèi)核中
- 我只能告訴你有文件描述符滿足要求了器钟,但是我不知道是哪個津坑,你自己一個一個去找吧(遍歷)
因此我們可以看到,select機制的特性在高性能網(wǎng)絡服務器動輒幾萬幾十萬并發(fā)鏈接的場景下無疑是低效的傲霸。
poll:小有所成
poll和select是非常相似的国瓮,poll相對于select的優(yōu)化僅僅在于解決了文件描述符不能超過1024個的限制,select和poll都會隨著監(jiān)控的文件描述增加而出現(xiàn)性能下降狞谱,因此不適合高并發(fā)場景。
epoll:獨步天下
在select面臨的三個問題中禁漓,文件描述數(shù)量限制已經(jīng)在poll中解決了跟衅,那么剩下的兩個問題epoll是通過什么技術巧妙解決的呢?這個問題你可以關注公眾號“碼農(nóng)的荒島求生”并回復"epoll"就能得到答案啦播歼。
總結
基于一切皆文件的設計哲學伶跷,I/O也可以通過文件的形式實現(xiàn),顯然高并發(fā)要與多個文件交互秘狞,這就離不開高效的I/O多路復用技術叭莫,本文我們詳細講解了什么是I/O多路復用以及使用方法,這其中以epoll為代表的I/O多路復用(基于事件驅動)技術使用非常廣泛烁试,實際上你會發(fā)現(xiàn)但凡涉及到高并發(fā)雇初、高性能都能見到事件驅動的編程方法,這也是下一篇的主題减响,敬請期待靖诗。