前言:
1、什么是IO多路復用:
隨著網(wǎng)絡需求的增大,對于網(wǎng)絡服務性能的要求也越來越高,而這也逐步促進了IO模型的發(fā)展。
最初的IO模型是阻塞式的,就是在數(shù)據(jù)沒有準備好的時候,進程處于阻塞狀態(tài),屁事不干。
然后程序猿就想:我靠,這小子(暫時叫recvfrom吧)這是占著茅坑不拉屎呀,得讓他出去干點其他活。于是就出現(xiàn)了非阻塞式IO。
非阻塞式IO就是在進程之中不斷的詢問fd,看數(shù)據(jù)有沒有準備好,如果沒有準備好,那么先去干其他事,然后過一會再來問數(shù)據(jù)有沒有準備好,這就解決了進程“偷懶”的問題。
但是后來發(fā)現(xiàn)那個進程每天累死累活的,也就只能監(jiān)控一個IO,而且是不管有沒有準備好都去訪問,大家都知道系統(tǒng)調(diào)用很耗時間,這樣每次去詢問,而且還沒有數(shù)據(jù)的情況下很耗cpu資源。有沒有辦法讓他同時監(jiān)控多個IO(老板壓榨程序猿,程序猿壓榨系統(tǒng)),有數(shù)據(jù)準備好才返回?
于是就請了個職業(yè)經(jīng)理人,當然,這種職業(yè)經(jīng)理人也有三種,分別是select(),poll(),epoll(),請誰好后面再說,請來先讓他循環(huán)詢問是否有IO準備好數(shù)據(jù),準備好就立馬通知原來的那個recvfrom,讓他做相應的處理,沒有準備好就進行阻塞。這就是IO的多路復用
IO多路復用好處在于可以不用去創(chuàng)建和維護多個線程/進程,用一個線程/進程來去監(jiān)控多個IO流,減小了系統(tǒng)的開銷
2、select()
select原理:
select會在內(nèi)核中不斷詢問數(shù)據(jù)是否準備好,如果沒有準備好的數(shù)據(jù),就將進程阻塞,直到有一個或者多個IO數(shù)據(jù)準備好后,告訴recvfrom,叫他來對數(shù)據(jù)進行讀寫。具體過程如下

當我們調(diào)用select()時:
1、上下文切換轉(zhuǎn)換為內(nèi)核態(tài)
2、將fd從用戶空間復制到內(nèi)核空間
3、內(nèi)核遍歷所有fd,查看其對應事件是否發(fā)生
4、如果沒發(fā)生,將進程阻塞,當設備驅(qū)動產(chǎn)生中斷或者timeout時間后,將進程喚醒,再次進行遍歷
5、返回遍歷后的fd
6、將fd從內(nèi)核空間復制到用戶空間
但是聰明的你一定會發(fā)現(xiàn)幾個問題:
第一:當那個職業(yè)經(jīng)紀人發(fā)現(xiàn)傳進來需要監(jiān)控的文件描述符(下面用fd表示)有數(shù)據(jù)準備好的,然后就一股腦的把所有fd傳回去,完全不告訴你那個fd準備好了,這樣我recvfrom又得循環(huán)所有fd,查看哪個有數(shù)據(jù)的,再進行讀寫。這樣的話不僅在復制這個fd的時候會消耗大量cpu,而且重復勞動太多了。這就好像是大學時候的坑爹教授在期末的時候跟我們說:考試的內(nèi)容都在這本書里面,這本書就是重點,咱絕對不超綱,你們放心。。。。(每次聽完都有種想揍人的沖動)
第二:監(jiān)控的文件描述符數(shù)量有限,最大是1024個
3、poll
那么,我們聰明的程序員就想改進下select,那么他們最先改進的就是監(jiān)控的文件描述符的限制,原來不是說有1024限制嗎,那么我現(xiàn)在就把他設計成沒有限制,這個時候就出現(xiàn)了poll,poll的基本原理和select差不多,而且poll和select一樣有著第一個缺點,就是fd的數(shù)組在復制進內(nèi)核空間和用戶空間之間,開銷會隨著文件描述符的增大而線性增大。
但是缺點要一點一點改,程序猿想出了個終極版本,就是epoll,徹底改進了原來那種低效的無差別輪詢,而且不限數(shù)量。
4、epoll
epoll和上面兩種模型不同的方式在于
1、從用戶空間到內(nèi)核態(tài),數(shù)據(jù)只拷貝一次:
2、epoll采用基于事件的就緒通知方式,epoll會給當前監(jiān)控的fd每人發(fā)一個回調(diào)函數(shù),當有事件發(fā)生的時候,內(nèi)核會采用類似callback的回調(diào)機制(有點像事件中斷),將文件描述符號放在一個文件描述符表里面,然后每次檢測這個表是不是為空,如果不為空就通知recvfrom進行讀寫數(shù)據(jù),為空就阻塞。具體過程如下:
epoll提供了三個函數(shù),epoll_create,epoll_ctl和epoll_wait,epoll_create是創(chuàng)建一個epoll句柄;epoll_ctl是注冊要監(jiān)聽的事件類型;epoll_wait則是等待事件的產(chǎn)生
epoll_create 創(chuàng)建一個epoll對象,一般epollfd = epoll_create()
epoll_ctl (epoll_add/epoll_del的合體),往epoll對象中增加/刪除某一個流的某一個事件
比如
epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//注冊緩沖區(qū)非空事件,即有數(shù)據(jù)流入
epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//注冊緩沖區(qū)非滿事件,即流可以被寫入
epoll_wait(epollfd,...)等待直到注冊的事件發(fā)生
(注:當對一個非阻塞流的讀寫發(fā)生緩沖區(qū)滿或緩沖區(qū)空,write/read會返回-1,并設置errno=EAGAIN。而epoll只關(guān)心緩沖區(qū)非滿和緩沖區(qū)非空事件)。
總結(jié):
(1)select,poll實現(xiàn)需要自己不斷輪詢所有fd集合,直到設備就緒,期間可能要睡眠和喚醒多次交替。而epoll其實也需要調(diào)用epoll_wait不斷輪詢就緒鏈表,期間也可能多次睡眠和喚醒交替,但是它是設備就緒時,調(diào)用回調(diào)函數(shù),把就緒fd放入就緒鏈表中,并喚醒在epoll_wait中進入睡眠的進程。雖然都要睡眠和交替,但是select和poll在“醒著”的時候要遍歷整個fd集合,而epoll在“醒著”的時候只要判斷一下就緒鏈表是否為空就行了,這節(jié)省了大量的CPU時間。這就是回調(diào)機制帶來的性能提升。
(2)select,poll每次調(diào)用都要把fd集合從用戶態(tài)往內(nèi)核態(tài)拷貝一次,并且要把current往設備等待隊列中掛一次,而epoll只要一次拷貝,而且把current往等待隊列上掛也只掛一次(在epoll_wait的開始,注意這里的等待隊列并不是設備等待隊列,只是一個epoll內(nèi)部定義的等待隊列)。這也能節(jié)省不少的開銷。