前言
為什么會(huì)考慮到深入理解多路復(fù)用?采用多路復(fù)用技術(shù)能把多個(gè)信號(hào)組合起來在一條物理信道上進(jìn)行傳輸,在遠(yuǎn)距離傳輸時(shí)可大大節(jié)省電纜的安裝和維護(hù)費(fèi)用。在Http/2,Redis等內(nèi)容中,反復(fù)提到多路復(fù)用帶來的效率提升,也只有了解了基礎(chǔ)概念,才能掌握它們,一步一步來吧。
計(jì)算機(jī)如何接收數(shù)據(jù)?
計(jì)算機(jī)執(zhí)行程序時(shí),會(huì)有優(yōu)先級(jí)的需求。網(wǎng)卡向cpu發(fā)出一個(gè)中斷信號(hào),操作系統(tǒng)便能得知有新數(shù)據(jù)到來,再通過網(wǎng)卡中斷程序去處理數(shù)據(jù)。
這一步,貫穿網(wǎng)卡、中斷、進(jìn)程調(diào)度的知識(shí),敘述阻塞recv()下,內(nèi)核接收數(shù)據(jù)全過程。(不論是客戶還是服務(wù)器應(yīng)用程序都用recv()函數(shù)從TCP連接的另一端接收數(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ì)列中。

select的流程
sock1、sock2和sock3三個(gè)socket,那么在調(diào)用select之后,操作系統(tǒng)把進(jìn)程A分別加入這三個(gè)socket的等待隊(duì)列中。當(dāng)任何一個(gè)socket收到數(shù)據(jù)后,中斷程序?qū)酒疬M(jìn)程。所謂喚起進(jìn)程,就是將進(jìn)程從所有的等待隊(duì)列中移除,加入到工作隊(duì)列里面。經(jīng)由這些步驟,當(dāng)進(jìn)程A被喚醒后,它知道至少有一個(gè)socket接收了數(shù)據(jù)。程序只需遍歷一遍socket列表,就可以得到就緒的socket。這種簡(jiǎn)單方式行之有效,在幾乎所有操作系統(tǒng)都有對(duì)應(yīng)的實(shí)現(xiàn)。
缺點(diǎn):
- 每次調(diào)用select都需要將進(jìn)程加入到所有監(jiān)視socket的等待隊(duì)列,每次喚醒都需要從每個(gè)隊(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ù),還需要遍歷一次。
epoll細(xì)節(jié)
當(dāng)某一進(jìn)程調(diào)用 epoll_create 方法時(shí),Linux內(nèi)核會(huì)創(chuàng)建一個(gè)eventpoll結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體中有兩個(gè)成員與epoll的使用方式密切相關(guān)。eventpoll結(jié)構(gòu)體如下所示:

每一個(gè)epoll對(duì)象都有一個(gè)獨(dú)立的eventpoll結(jié)構(gòu)體,用于存放通過epoll_ctl方法向epoll對(duì)象中添加進(jìn)來的事件。這些事件都會(huì)掛載在紅黑樹中,如此,重復(fù)添加的事件就可以通過紅黑樹而高效的識(shí)別出來(紅黑樹的插入時(shí)間效率是lgn,其中n為樹的高度)。
而所有添加到epoll中的事件都會(huì)與設(shè)備(網(wǎng)卡)驅(qū)動(dòng)程序建立回調(diào)關(guān)系,也就是說,當(dāng)相應(yīng)的事件發(fā)生時(shí)會(huì)調(diào)用這個(gè)回調(diào)方法。這個(gè)回調(diào)方法在內(nèi)核中叫ep_poll_callback,它會(huì)將發(fā)生的事件添加到rdlist雙鏈表中。
在epoll中,對(duì)于每一個(gè)事件,都會(huì)建立一個(gè)epitem結(jié)構(gòu)體,如下所示:

當(dāng)調(diào)用epoll_wait檢查是否有事件發(fā)生時(shí),只需要檢查eventpoll對(duì)象中的rdlist雙鏈表中是否有epitem元素即可。如果rdlist不為空,則把發(fā)生的事件復(fù)制到用戶態(tài),同時(shí)將事件數(shù)量返回給用戶。
當(dāng)我們執(zhí)行epoll_ctl時(shí),除了把socket放到epoll文件系統(tǒng)里file對(duì)象對(duì)應(yīng)的紅黑樹上之外,還會(huì)給內(nèi)核中斷處理程序注冊(cè)一個(gè)回調(diào)函數(shù),告訴內(nèi)核,如果這個(gè)句柄的中斷到了,就把它放到準(zhǔn)備就緒list鏈表里。所以,當(dāng)一個(gè)socket上有數(shù)據(jù)到了,內(nèi)核在把網(wǎng)卡上的數(shù)據(jù)copy到內(nèi)核中后就來把socket插入到準(zhǔn)備就緒鏈表里了。

結(jié)語
多路復(fù)用最重要的知識(shí)點(diǎn)是因?yàn)閮?nèi)部用了一個(gè)紅黑樹記錄添加的socket的數(shù)據(jù)結(jié)構(gòu),用了一個(gè)雙向鏈表接收內(nèi)核觸發(fā)的事件。就是因?yàn)槎嗔诉@個(gè)存儲(chǔ),可以直接拿到就緒socket,而不用像select那樣一個(gè)個(gè)檢查。