Epoll的本質(zhì)(內(nèi)部實(shí)現(xiàn)原理)

一辽剧、從網(wǎng)卡接收數(shù)據(jù)說起

下圖是一個(gè)典型的計(jì)算機(jī)結(jié)構(gòu)圖臂寝,計(jì)算機(jī)由CPU章鲤、存儲(chǔ)器(內(nèi)存)、網(wǎng)絡(luò)接口等部件組成咆贬。了解epoll本質(zhì)的第一步败徊,要從硬件的角度看計(jì)算機(jī)怎樣接收網(wǎng)絡(luò)數(shù)據(jù)。

計(jì)算機(jī)結(jié)構(gòu)圖

下圖展示了網(wǎng)卡接收數(shù)據(jù)的過程掏缎。在①階段皱蹦,網(wǎng)卡收到網(wǎng)線傳來的數(shù)據(jù);經(jīng)過②階段的硬件電路的傳輸眷蜈;最終將數(shù)據(jù)寫入到內(nèi)存中的某個(gè)地址上(③階段)沪哺。這個(gè)過程涉及到DMA傳輸、IO通路選擇等硬件有關(guān)的知識(shí)酌儒,但我們只需知道:網(wǎng)卡會(huì)把接收到的數(shù)據(jù)寫入內(nèi)存辜妓。

網(wǎng)卡接收數(shù)據(jù)的過程

通過硬件傳輸,網(wǎng)卡接收的數(shù)據(jù)存放到內(nèi)存中忌怎。操作系統(tǒng)就可以去讀取它們籍滴。

二、如何知道接收了數(shù)據(jù)榴啸?

了解epoll本質(zhì)的第二步孽惰,要從CPU的角度來看數(shù)據(jù)接收。要理解這個(gè)問題插掂,要先了解一個(gè)概念——中斷灰瞻。

計(jì)算機(jī)執(zhí)行程序時(shí)腥例,會(huì)有優(yōu)先級(jí)的需求辅甥。比如,當(dāng)計(jì)算機(jī)收到斷電信號(hào)時(shí)(電容可以保存少許電量燎竖,供CPU運(yùn)行很短的一小段時(shí)間)璃弄,它應(yīng)立即去保存數(shù)據(jù),保存數(shù)據(jù)的程序具有較高的優(yōu)先級(jí)构回。

一般而言夏块,由硬件產(chǎn)生的信號(hào)需要cpu立馬做出回應(yīng)(不然數(shù)據(jù)可能就丟失),所以它的優(yōu)先級(jí)很高纤掸。cpu理應(yīng)中斷掉正在執(zhí)行的程序脐供,去做出響應(yīng);當(dāng)cpu完成對(duì)硬件的響應(yīng)后借跪,再重新執(zhí)行用戶程序政己。中斷的過程如下圖,和函數(shù)調(diào)用差不多掏愁。只不過函數(shù)調(diào)用是事先定好位置歇由,而中斷的位置由“信號(hào)”決定卵牍。

中斷程序調(diào)用

以鍵盤為例,當(dāng)用戶按下鍵盤某個(gè)按鍵時(shí)沦泌,鍵盤會(huì)給cpu的中斷引腳發(fā)出一個(gè)高電平糊昙。cpu能夠捕獲這個(gè)信號(hào),然后執(zhí)行鍵盤中斷程序谢谦。下圖展示了各種硬件通過中斷與cpu交互释牺。

cpu中斷

現(xiàn)在可以回答本節(jié)提出的問題了:當(dāng)網(wǎng)卡把數(shù)據(jù)寫入到內(nèi)存后,網(wǎng)卡向cpu發(fā)出一個(gè)中斷信號(hào)他宛,操作系統(tǒng)便能得知有新數(shù)據(jù)到來船侧,再通過網(wǎng)卡中斷程序去處理數(shù)據(jù)。

三厅各、進(jìn)程阻塞為什么不占用cpu資源镜撩?

了解epoll本質(zhì)的第三步,要從操作系統(tǒng)進(jìn)程調(diào)度的角度來看數(shù)據(jù)接收队塘。阻塞是進(jìn)程調(diào)度的關(guān)鍵一環(huán)袁梗,指的是進(jìn)程在等待某事件(如接收到網(wǎng)絡(luò)數(shù)據(jù))發(fā)生之前的等待狀態(tài),recv憔古、select和epoll都是阻塞方法遮怜。了解“進(jìn)程阻塞為什么不占用cpu資源?”鸿市,也就能夠了解這一步锯梁。

為簡(jiǎn)單起見,我們從普通的recv接收開始分析焰情,先看看下面代碼:

//創(chuàng)建socket
int s = socket(AF_INET, SOCK_STREAM, 0);   
//綁定
bind(s, ...)
//監(jiān)聽
listen(s, ...)
//接受客戶端連接
int c = accept(s, ...)
//接收客戶端數(shù)據(jù)
recv(c, ...);
//將數(shù)據(jù)打印出來
printf(...)

這是一段最基礎(chǔ)的網(wǎng)絡(luò)編程代碼陌凳,先新建socket對(duì)象,依次調(diào)用bind内舟、listen合敦、accept,最后調(diào)用recv接收數(shù)據(jù)验游。recv是個(gè)阻塞方法充岛,當(dāng)程序運(yùn)行到recv時(shí),它會(huì)一直等待耕蝉,直到接收到數(shù)據(jù)才往下執(zhí)行崔梗。

插入:如果您還不太熟悉網(wǎng)絡(luò)編程,歡迎閱讀我編寫的《Unity3D網(wǎng)絡(luò)游戲?qū)崙?zhàn)(第2版)》垒在,會(huì)有詳細(xì)的介紹蒜魄。

那么阻塞的原理是什么?

工作隊(duì)列

操作系統(tǒng)為了支持多任務(wù),實(shí)現(xiàn)了進(jìn)程調(diào)度的功能权悟,會(huì)把進(jìn)程分為“運(yùn)行”和“等待”等幾種狀態(tài)砸王。運(yùn)行狀態(tài)是進(jìn)程獲得cpu使用權(quán),正在執(zhí)行代碼的狀態(tài)峦阁;等待狀態(tài)是阻塞狀態(tài)谦铃,比如上述程序運(yùn)行到recv時(shí),程序會(huì)從運(yùn)行狀態(tài)變?yōu)榈却隣顟B(tài)榔昔,接收到數(shù)據(jù)后又變回運(yùn)行狀態(tài)驹闰。操作系統(tǒng)會(huì)分時(shí)執(zhí)行各個(gè)運(yùn)行狀態(tài)的進(jìn)程,由于速度很快撒会,看上去就像是同時(shí)執(zhí)行多個(gè)任務(wù)嘹朗。

下圖中的計(jì)算機(jī)中運(yùn)行著A、B诵肛、C三個(gè)進(jìn)程屹培,其中進(jìn)程A執(zhí)行著上述基礎(chǔ)網(wǎng)絡(luò)程序,一開始怔檩,這3個(gè)進(jìn)程都被操作系統(tǒng)的工作隊(duì)列所引用褪秀,處于運(yùn)行狀態(tài),會(huì)分時(shí)執(zhí)行薛训。

工作隊(duì)列中有A媒吗、B和C三個(gè)進(jìn)程

等待隊(duì)列

當(dāng)進(jìn)程A執(zhí)行到創(chuàng)建socket的語句時(shí),操作系統(tǒng)會(huì)創(chuàng)建一個(gè)由文件系統(tǒng)管理的socket對(duì)象(如下圖)乙埃。這個(gè)socket對(duì)象包含了發(fā)送緩沖區(qū)闸英、接收緩沖區(qū)、等待隊(duì)列等成員介袜。等待隊(duì)列是個(gè)非常重要的結(jié)構(gòu)甫何,它指向所有需要等待該socket事件的進(jìn)程。

創(chuàng)建socket

當(dāng)程序執(zhí)行到recv時(shí)米酬,操作系統(tǒng)會(huì)將進(jìn)程A從工作隊(duì)列移動(dòng)到該socket的等待隊(duì)列中(如下圖)沛豌。由于工作隊(duì)列只剩下了進(jìn)程B和C趋箩,依據(jù)進(jìn)程調(diào)度赃额,cpu會(huì)輪流執(zhí)行這兩個(gè)進(jìn)程的程序,不會(huì)執(zhí)行進(jìn)程A的程序叫确。所以進(jìn)程A被阻塞跳芳,不會(huì)往下執(zhí)行代
碼,也不會(huì)占用cpu資源
竹勉。

socket的等待隊(duì)列

ps:操作系統(tǒng)添加等待隊(duì)列只是添加了對(duì)這個(gè)“等待中”進(jìn)程的引用飞盆,以便在接收到數(shù)據(jù)時(shí)獲取進(jìn)程對(duì)象、將其喚醒,而非直接將進(jìn)程管理納入自己之下吓歇。上圖為了方便說明孽水,直接將進(jìn)程掛到等待隊(duì)列之下。

喚醒進(jìn)程

當(dāng)socket接收到數(shù)據(jù)后城看,操作系統(tǒng)將該socket等待隊(duì)列上的進(jìn)程重新放回到工作隊(duì)列女气,該進(jìn)程變成運(yùn)行狀態(tài),繼續(xù)執(zhí)行代碼测柠。也由于socket的接收緩沖區(qū)已經(jīng)有了數(shù)據(jù)炼鞠,recv可以返回接收到的數(shù)據(jù)。

四轰胁、內(nèi)核接收網(wǎng)絡(luò)數(shù)據(jù)全過程

這一步谒主,貫穿網(wǎng)卡、中斷赃阀、進(jìn)程調(diào)度的知識(shí)霎肯,敘述阻塞recv下,內(nèi)核接收數(shù)據(jù)全過程榛斯。
進(jìn)程在recv阻塞期間姿现,計(jì)算機(jī)收到了對(duì)端傳送的數(shù)據(jù)(步驟①)。數(shù)據(jù)經(jīng)由網(wǎng)卡傳送到內(nèi)存(步驟②)肖抱,然后網(wǎng)卡通過中斷信號(hào)通知cpu有數(shù)據(jù)到達(dá)备典,cpu執(zhí)行中斷程序(步驟③)。此處的中斷程序主要有兩項(xiàng)功能意述,先將網(wǎng)絡(luò)數(shù)據(jù)寫入到對(duì)應(yīng)socket的接收緩沖區(qū)里面(步驟④)提佣,再喚醒進(jìn)程A(步驟⑤),重新將進(jìn)程A放入工作隊(duì)列中荤崇。
喚醒進(jìn)程
以上是內(nèi)核接收數(shù)據(jù)全過程

五拌屏、同時(shí)監(jiān)視多個(gè)socket的簡(jiǎn)單方法

服務(wù)端需要管理多個(gè)客戶端連接,而recv只能監(jiān)視單個(gè)socket术荤,這種矛盾下倚喂,人們開始尋找監(jiān)視多個(gè)socket的方法。epoll的要義是高效的監(jiān)視多個(gè)socket瓣戚。從歷史發(fā)展角度看端圈,必然先出現(xiàn)一種不太高效的方法,人們?cè)偌右愿倪M(jìn)子库。只有先理解了不太高效的方法舱权,才能夠理解epoll的本質(zhì)。

假如能夠預(yù)先傳入一個(gè)socket列表仑嗅,如果列表中的socket都沒有數(shù)據(jù)宴倍,掛起進(jìn)程张症,直到有一個(gè)socket收到數(shù)據(jù),喚醒進(jìn)程鸵贬。這種方法很直接俗他,也是select的設(shè)計(jì)思想。

為方便理解阔逼,我們先復(fù)習(xí)select的用法拯辙。在如下的代碼中,先準(zhǔn)備一個(gè)數(shù)組(下面代碼中的fds)颜价,讓fds存放著所有需要監(jiān)視的socket涯保。然后調(diào)用select,如果fds中的所有socket都沒有數(shù)據(jù)周伦,select會(huì)阻塞夕春,直到有一個(gè)(也可以是多個(gè))socket接收到數(shù)據(jù),select返回专挪,喚醒進(jìn)程及志。用戶可以遍歷fds,通過FD_ISSET判斷具體哪個(gè)socket收到數(shù)據(jù)寨腔,然后做出處理速侈。

int s = socket(AF_INET, SOCK_STREAM, 0);  
bind(s, ...)
listen(s, ...) 
int fds[] =  存放需要監(jiān)聽的socket 
while(1){
    int n = select(..., fds, ...)
    for(int i=0; i < fds.count; i++){
        if(FD_ISSET(fds[i], ...)){
            //fds[i]的數(shù)據(jù)處理
        }    
    }
}

select的流程
select的實(shí)現(xiàn)思路很直接。假如程序同時(shí)監(jiān)視sock1迫卢、sock2和sock3三個(gè)socket倚搬,那么在調(diào)用select之后,操作系統(tǒng)把進(jìn)程A(包括了select邏輯)分別加入這三個(gè)socket的等待隊(duì)列中(前文說過乾蛤,放的其實(shí)是進(jìn)程A的引用)每界。
操作系統(tǒng)把進(jìn)程A分別加入這三個(gè)socket的等待隊(duì)列中
當(dāng)任何一個(gè)socket收到數(shù)據(jù)后,中斷程序?qū)酒疬M(jìn)程家卖。下圖展示了sock2接收到了數(shù)據(jù)的處理流程眨层。

ps:recv和select的中斷回調(diào)可以設(shè)置成不同的內(nèi)容。

sock2接收到了數(shù)據(jù)上荡,中斷程序喚起進(jìn)程A

所謂喚起進(jìn)程趴樱,就是將進(jìn)程從所有的等待隊(duì)列中移除,加入到工作隊(duì)列里面酪捡。

經(jīng)由這些步驟叁征,當(dāng)進(jìn)程被喚醒后,它知道至少有一個(gè)socket接收了數(shù)據(jù)沛善。程序只需遍歷一遍socket列表航揉,就可以得到就緒的socket塞祈。

這種簡(jiǎn)單方式行之有效金刁,在幾乎所有操作系統(tǒng)都有對(duì)應(yīng)的實(shí)現(xiàn)帅涂。

select的缺點(diǎn):

但是簡(jiǎn)單的方法往往有缺點(diǎn),主要是:

其一尤蛮,每次調(diào)用select都需要將進(jìn)程加入到所有監(jiān)視socket的等待隊(duì)列媳友,每次喚醒都需要從每個(gè)隊(duì)列中移除。這里涉及了兩次遍歷(遍歷進(jìn)程A關(guān)心的所有socket产捞,需要注意的是添加從等待隊(duì)列頭部添加醇锚,刪除通過回調(diào)直接實(shí)現(xiàn),所以每個(gè)socket的等待隊(duì)列不用遍歷)坯临,而且每次都要將整個(gè)fds列表傳遞給內(nèi)核焊唬,有一定的開銷。正是因?yàn)楸闅v操作開銷大看靠,出于效率的考量赶促,才會(huì)規(guī)定select的最大監(jiān)視數(shù)量,默認(rèn)只能監(jiān)視1024個(gè)socket挟炬。

其二鸥滨,進(jìn)程被喚醒后,程序并不知道哪些socket收到數(shù)據(jù)谤祖,還需要遍歷一次(這一次遍歷是在應(yīng)用層)婿滓。

那么,有沒有減少遍歷的方法粥喜?有沒有保存就緒socket的方法凸主?這兩個(gè)問題便是epoll技術(shù)要解決的。

當(dāng)程序調(diào)用select時(shí)额湘,內(nèi)核會(huì)先遍歷一遍socket秕铛,如果有一個(gè)以上的socket接收緩沖區(qū)有數(shù)據(jù),那么select直接返回缩挑,不會(huì)阻塞但两。這也是為什么select的返回值有可能大于1的原因之一。如果沒有socket有數(shù)據(jù)供置,進(jìn)程才會(huì)阻塞

六谨湘、epoll的設(shè)計(jì)思路

epoll是在select出現(xiàn)N多年后才被發(fā)明的,是select和poll的增強(qiáng)版本芥丧。epoll通過以下一些措施來改進(jìn)效率紧阔。

措施一:功能分離

select低效的原因之一是將“維護(hù)等待隊(duì)列”和“阻塞進(jìn)程”兩個(gè)步驟合二為一。如下圖所示续担,每次調(diào)用select都需要這兩步操作擅耽,然而大多數(shù)應(yīng)用場(chǎng)景中,需要監(jiān)視的socket相對(duì)固定物遇,并不需要每次都修改乖仇。epoll將這兩個(gè)操作分開憾儒,先用epoll_ctl維護(hù)等待隊(duì)列,再調(diào)用epoll_wait阻塞進(jìn)程(解耦)乃沙。顯而易見的起趾,效率就能得到提升。

相比select警儒,epoll拆分了功能

為方便理解后續(xù)的內(nèi)容训裆,我們先復(fù)習(xí)下epoll的用法。如下的代碼中蜀铲,先用epoll_create創(chuàng)建一個(gè)epoll對(duì)象epfd边琉,再通過epoll_ctl將需要監(jiān)視的socket添加到epfd中,最后調(diào)用epoll_wait等待數(shù)據(jù)记劝。

int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...)
listen(s, ...) 
int epfd = epoll_create(...);
epoll_ctl(epfd, ...); //將所有需要監(jiān)聽的socket添加到epfd中 
while(1){
    int n = epoll_wait(...)
    for(接收到數(shù)據(jù)的socket){ 
       //處理    
    }
}

功能分離艺骂,使得epoll有了優(yōu)化的可能。

措施二:就緒列表

select低效的另一個(gè)原因在于程序不知道哪些socket收到數(shù)據(jù)隆夯,只能一個(gè)個(gè)遍歷钳恕。如果內(nèi)核維護(hù)一個(gè)“就緒列表”,引用收到數(shù)據(jù)的socket蹄衷,就能避免遍歷忧额。如下圖所示,計(jì)算機(jī)共有三個(gè)socket愧口,收到數(shù)據(jù)的sock2和sock3被rdlist(就緒列表)所引用睦番。當(dāng)進(jìn)程被喚醒后,只要獲取rdlist的內(nèi)容耍属,就能夠知道哪些socket收到數(shù)據(jù)托嚣。(這里引出了另一個(gè)問題:

什么時(shí)候select優(yōu)于epoll?

一般認(rèn)為如果在并發(fā)量低厚骗,socket都比較活躍的情況下示启,select效率更高,也就是說活躍socket數(shù)目與監(jiān)控的總的socket數(shù)目之比越大领舰,select效率越高夫嗓,因?yàn)閟elect反正都會(huì)遍歷所有的socket,如果比例大冲秽,就沒有白白遍歷舍咖。加之于select本身實(shí)現(xiàn)比較簡(jiǎn)單,導(dǎo)致總體現(xiàn)象比epoll好)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末锉桑,一起剝皮案震驚了整個(gè)濱河市排霉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌民轴,老刑警劉巖攻柠,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件球订,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡辙诞,警方通過查閱死者的電腦和手機(jī)辙售,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門轻抱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來飞涂,“玉大人,你說我怎么就攤上這事祈搜〗系辏” “怎么了?”我有些...
    開封第一講書人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵容燕,是天一觀的道長梁呈。 經(jīng)常有香客問我,道長蘸秘,這世上最難降的妖魔是什么官卡? 我笑而不...
    開封第一講書人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮醋虏,結(jié)果婚禮上寻咒,老公的妹妹穿的比我還像新娘。我一直安慰自己颈嚼,他們只是感情好毛秘,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著阻课,像睡著了一般叫挟。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上限煞,一...
    開封第一講書人閱讀 51,190評(píng)論 1 299
  • 那天抹恳,我揣著相機(jī)與錄音,去河邊找鬼署驻。 笑死适秩,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的硕舆。 我是一名探鬼主播秽荞,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼抚官!你這毒婦竟也來了扬跋?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤凌节,失蹤者是張志新(化名)和其女友劉穎钦听,沒想到半個(gè)月后洒试,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡朴上,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年垒棋,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片痪宰。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡叼架,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出衣撬,到底是詐尸還是另有隱情乖订,我是刑警寧澤,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布具练,位于F島的核電站乍构,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏扛点。R本人自食惡果不足惜哥遮,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望陵究。 院中可真熱鬧眠饮,春花似錦、人聲如沸畔乙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽牲距。三九已至返咱,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間牍鞠,已是汗流浹背咖摹。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留难述,地道東北人萤晴。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像胁后,于是被迫代替她去往敵國和親店读。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容