IO多路復(fù)用技術(shù)及epoll實(shí)現(xiàn)原理
redis 是一個(gè)單線(xiàn)程卻性能非常好的內(nèi)存數(shù)據(jù)庫(kù), 主要用來(lái)作為緩存系統(tǒng)。redis采用網(wǎng)絡(luò)IO多路復(fù)用技術(shù)來(lái)保證在多連接的時(shí)候, 系統(tǒng)的高吞吐量。
一、為什么 Redis 中要使用 I/O 多路復(fù)用這種技術(shù)呢?
首先,Redis 是跑在單線(xiàn)程中的,所有的操作都是按照順序線(xiàn)性執(zhí)行的,但是由于讀寫(xiě)操作等待用戶(hù)輸入或輸出都是阻塞的,所以 I/O 操作在一般情況下往往不能直接返回,這會(huì)導(dǎo)致某一文件的 I/O 阻塞導(dǎo)致整個(gè)進(jìn)程無(wú)法對(duì)其它客戶(hù)提供服務(wù),而 I/O 多路復(fù)用就是為了解決這個(gè)問(wèn)題而出現(xiàn)的。
redis的io模型主要是基于epoll實(shí)現(xiàn)的,不過(guò)它也提供了 select和kqueue的實(shí)現(xiàn),默認(rèn)采用epoll。
epoll是眾多i/o多路復(fù)用技術(shù)當(dāng)中的一種,相比其他io多路復(fù)用技術(shù)(select, poll等等),epoll有諸多優(yōu)點(diǎn):epoll 沒(méi)有最大并發(fā)連接的限制,上限是最大可以打開(kāi)文件的數(shù)目,這個(gè)數(shù)字一般遠(yuǎn)大于 2048, 一般來(lái)說(shuō)這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大? ,具體數(shù)目可以 cat /proc/sys/fs/file-max 察看。
效率提升, Epoll 最大的優(yōu)點(diǎn)就在于它只管你“活躍”的連接 ,而跟連接總數(shù)無(wú)關(guān),因此在實(shí)際的網(wǎng)絡(luò)環(huán)境中, Epoll 的效率就會(huì)遠(yuǎn)遠(yuǎn)高于 select 和 poll 。
內(nèi)存拷貝, Epoll 在這點(diǎn)上使用了“共享內(nèi)存 ”,這個(gè)內(nèi)存拷貝也省略了。
二、epoll與select/poll的區(qū)別
? ? ?select,poll,epoll都是IO多路復(fù)用的機(jī)制。I/O多路復(fù)用就通過(guò)一種機(jī)制,可以監(jiān)視多個(gè)描述符,一旦某個(gè)描述符就緒,能夠通知程序進(jìn)行相應(yīng)的操作。
? ? ?select的本質(zhì)是采用32個(gè)整數(shù)的32位,即32*32= 1024來(lái)標(biāo)識(shí),fd值為1-1024。當(dāng)fd的值超過(guò)1024限制時(shí),就必須修改FD_SETSIZE的大小。這個(gè)時(shí)候就可以表示32*max值范圍的fd。
? ? ?poll與select不同,通過(guò)一個(gè)pollfd數(shù)組向內(nèi)核傳遞需要關(guān)注的事件,故沒(méi)有描述符個(gè)數(shù)的限制,pollfd中的events字段和revents分別用于表示關(guān)注的事件和發(fā)生的事件,故pollfd數(shù)組只需要被初始化一次。
? ? ?epoll還是poll的一種優(yōu)化,返回后不需要對(duì)所有的fd進(jìn)行遍歷,在內(nèi)核中維持了fd的列表。select和poll是將這個(gè)內(nèi)核列表維持在用戶(hù)態(tài),然后傳遞到內(nèi)核中。與poll/select不同,epoll不再是一個(gè)單獨(dú)的系統(tǒng)調(diào)用,而是由epoll_create/epoll_ctl/epoll_wait三個(gè)系統(tǒng)調(diào)用組成,后面將會(huì)看到這樣做的好處。epoll在2.6以后的內(nèi)核才支持。
2.1 select/poll的幾大缺點(diǎn):
每次調(diào)用select/poll,都需要把fd集合從用戶(hù)態(tài)拷貝到內(nèi)核態(tài),這個(gè)開(kāi)銷(xiāo)在fd很多時(shí)會(huì)很大
同時(shí)每次調(diào)用select/poll都需要在內(nèi)核遍歷傳遞進(jìn)來(lái)的所有fd,這個(gè)開(kāi)銷(xiāo)在fd很多時(shí)也很大
針對(duì)select支持的文件描述符數(shù)量太小了,默認(rèn)是1024
select返回的是含有整個(gè)句柄的數(shù)組,應(yīng)用程序需要遍歷整個(gè)數(shù)組才能發(fā)現(xiàn)哪些句柄發(fā)生了事件;
select的觸發(fā)方式是水平觸發(fā),應(yīng)用程序如果沒(méi)有完成對(duì)一個(gè)已經(jīng)就緒的文件描述符進(jìn)行IO操作,那么之后每次select調(diào)用還是會(huì)將這些文件描述符通知進(jìn)程。
三、epoll IO多路復(fù)用模型實(shí)現(xiàn)機(jī)制
由于epoll的實(shí)現(xiàn)機(jī)制與select/poll機(jī)制完全不同,上面所說(shuō)的 select的缺點(diǎn)在epoll上不復(fù)存在。
epoll沒(méi)有這個(gè)限制,它所支持的FD上限是最大可以打開(kāi)文件的數(shù)目,這個(gè)數(shù)字一般遠(yuǎn)大于2048,舉個(gè)例子,在1GB內(nèi)存的機(jī)器上大約是10萬(wàn)左右,設(shè)想一下如下場(chǎng)景:有100萬(wàn)個(gè)客戶(hù)端同時(shí)與一個(gè)服務(wù)器進(jìn)程保持著TCP連接。而每一時(shí)刻,通常只有幾百上千個(gè)TCP連接是活躍的(事實(shí)上大部分場(chǎng)景都是這種情況)。如何實(shí)現(xiàn)這樣的高并發(fā)?在select/poll時(shí)代,服務(wù)器進(jìn)程每次都把這100萬(wàn)個(gè)連接告訴操作系統(tǒng)(從用戶(hù)態(tài)復(fù)制句柄數(shù)據(jù)結(jié)構(gòu)到內(nèi)核態(tài)),讓操作系統(tǒng)內(nèi)核去查詢(xún)這些套接字上是否有事件發(fā)生,輪詢(xún)完后,再將句柄數(shù)據(jù)復(fù)制到用戶(hù)態(tài),讓服務(wù)器應(yīng)用程序輪詢(xún)處理已發(fā)生的網(wǎng)絡(luò)事件,這一過(guò)程資源消耗較大,因此,select/poll一般只能處理幾千的并發(fā)連接。如果沒(méi)有I/O事件產(chǎn)生,我們的程序就會(huì)阻塞在select處。但是依然有個(gè)問(wèn)題,我們從select那里僅僅知道了,有I/O事件發(fā)生了,但卻并不知道是那幾個(gè)流(可能有一個(gè),多個(gè),甚至全部),我們只能無(wú)差別輪詢(xún)所有流,找出能讀出數(shù)據(jù),或者寫(xiě)入數(shù)據(jù)的流,對(duì)他們進(jìn)行操作。但是使用select,我們有O(n)的無(wú)差別輪詢(xún)復(fù)雜度,同時(shí)處理的流越多,每一次無(wú)差別輪詢(xún)時(shí)間就越長(zhǎng)
epoll的設(shè)計(jì)和實(shí)現(xiàn)與select完全不同。epoll通過(guò)在Linux內(nèi)核中申請(qǐng)一個(gè)簡(jiǎn)易的文件系統(tǒng)(文件系統(tǒng)一般用什么數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)?B+樹(shù))。把原先的select/poll調(diào)用分成了3個(gè)部分:
- 調(diào)用epoll_create()建立一個(gè)epoll對(duì)象(在epoll文件系統(tǒng)中為這個(gè)句柄對(duì)象分配資源)
調(diào)用epoll_ctl向epoll對(duì)象中添加這100萬(wàn)個(gè)連接的套接字
- 調(diào)用epoll_wait收集發(fā)生的事件的連接
如此一來(lái),要實(shí)現(xiàn)上面說(shuō)是的場(chǎng)景,只需要在進(jìn)程啟動(dòng)時(shí)建立一個(gè)epoll對(duì)象,然后在需要的時(shí)候向這個(gè)epoll對(duì)象中添加或者刪除連接。同時(shí),epoll_wait的效率也非常高,因?yàn)檎{(diào)用epoll_wait時(shí),并沒(méi)有一股腦的向操作系統(tǒng)復(fù)制這100萬(wàn)個(gè)連接的句柄數(shù)據(jù),內(nèi)核也不需要去遍歷全部的連接。
四、epoll IO底層實(shí)現(xiàn)
當(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)體如下所示:
而所有添加到epoll中的事件都會(huì)與設(shè)備(網(wǎng)卡)驅(qū)動(dòng)程序建立回調(diào)關(guān)系,也就是說(shuō),當(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)體,如下所示:

優(yōu)勢(shì):
1). 不用重復(fù)傳遞。
我們調(diào)用epoll_wait時(shí)就相當(dāng)于以往調(diào)用select/poll,但是這時(shí)卻不用傳遞socket句柄給內(nèi)核,因?yàn)閮?nèi)核已經(jīng)在epoll_ctl中拿到了要監(jiān)控的句柄列表。
?2). 在內(nèi)核里,一切皆文件
epoll向內(nèi)核注冊(cè)了一個(gè)文件系統(tǒng),用于存儲(chǔ)上述的被監(jiān)控socket。當(dāng)你調(diào)用epoll_create時(shí),就會(huì)在這個(gè)虛擬的epoll文件系統(tǒng)里創(chuàng)建一個(gè)file結(jié)點(diǎn)。當(dāng)然這個(gè)file不是普通文件,它只服務(wù)于epoll。
epoll在被內(nèi)核初始化時(shí)(操作系統(tǒng)啟動(dòng)),同時(shí)會(huì)開(kāi)辟出epoll自己的內(nèi)核高速cache區(qū),用于安置每一個(gè)我們想監(jiān)控的socket,這些socket會(huì)以紅黑樹(shù)的形式保存在內(nèi)核cache里,以支持快速的查找、插入、刪除。這個(gè)內(nèi)核高速cache區(qū),就是建立連續(xù)的物理內(nèi)存頁(yè),然后在之上建立slab層,簡(jiǎn)單的說(shuō),就是物理上分配好你想要的size的內(nèi)存對(duì)象,每次使用時(shí)都是使用空閑的已分配好的對(duì)象。
?3). 極其高效的原因:
這是由于我們?cè)谡{(diào)用epoll_create時(shí),內(nèi)核除了幫我們?cè)趀poll文件系統(tǒng)里建了個(gè)file結(jié)點(diǎn),在內(nèi)核cache里建了個(gè)紅黑樹(shù)用于存儲(chǔ)以后epoll_ctl傳來(lái)的socket外,還會(huì)再建立一個(gè)list鏈表,用于存儲(chǔ)準(zhǔn)備就緒的事件,當(dāng)epoll_wait調(diào)用時(shí),僅僅觀察這個(gè)list鏈表里有沒(méi)有數(shù)據(jù)即可。有數(shù)據(jù)就返回,沒(méi)有數(shù)據(jù)就sleep,等到timeout時(shí)間到后即使鏈表沒(méi)數(shù)據(jù)也返回。所以,epoll_wait非常高效。
當(dāng)我們執(zhí)行epoll_ctl時(shí),除了把socket放到epoll文件系統(tǒng)里file對(duì)象對(duì)應(yīng)的紅黑樹(shù)上之外,還會(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)核中后就來(lái)把socket插入到準(zhǔn)備就緒鏈表里了。從上面這句可以看出,epoll的基礎(chǔ)就是回調(diào)!?一顆紅黑樹(shù),一張準(zhǔn)備就緒句柄鏈表,少量的內(nèi)核cache,就幫我們解決了大并發(fā)下的socket處理問(wèn)題。執(zhí)行epoll_create時(shí),創(chuàng)建了紅黑樹(shù)和就緒鏈表,執(zhí)行epoll_ctl時(shí),如果增加socket句柄,則檢查在紅黑樹(shù)中是否存在,存在立即返回,不存在則添加到樹(shù)干上,然后向內(nèi)核注冊(cè)回調(diào)函數(shù),用于當(dāng)中斷事件來(lái)臨時(shí)向準(zhǔn)備就緒鏈表中插入數(shù)據(jù)。執(zhí)行epoll_wait時(shí)立刻返回準(zhǔn)備就緒鏈表里的數(shù)據(jù)即可。