epoll 原理
本文轉(zhuǎn)載自epoll 原理
導(dǎo)語
以前經(jīng)常被人問道 select、poll、epoll 的區(qū)別,基本都是靠死記硬背的,最近正好復(fù)習(xí) linux 相關(guān)的內(nèi)容,就把這一塊做個(gè)筆記吧,以后也能方便查閱。
epoll 是 linux 2.6 之后新出的一種 I/O 多路復(fù)用方式,與傳統(tǒng)的 select、poll 相比,有著很大的優(yōu)勢。一些開源的軟件如 nginx 也采用了 epoll 的設(shè)計(jì)思路。因此,學(xué)習(xí) epoll 對于我們在 linux 環(huán)境下編程是很有幫助的。
本文是 epoll 的復(fù)習(xí)筆記,主要講一下 epoll 與傳統(tǒng)的 select、poll 區(qū)別,實(shí)現(xiàn)的原理,以及應(yīng)用。
為什么要 I/O 多路復(fù)用
前文說過了,epoll 是一個(gè)優(yōu)秀的 I/O 多路復(fù)用方式。所以,在講解 epoll 之前,我們先來看一下為什么需要 I/O 多路復(fù)用。
阻塞 OR 非阻塞
我們知道,對于 linux 來說,I/O 設(shè)備為特殊的文件,讀寫和文件是差不多的,但是 I/O 設(shè)備因?yàn)樽x寫與內(nèi)存讀寫相比,速度差距非常大。與 cpu 讀寫速度更是沒法比,所以相比于對內(nèi)存的讀寫,I/O 操作總是拖后腿的那個(gè)。網(wǎng)絡(luò) I/O 更是如此,我們很多時(shí)候不知道網(wǎng)絡(luò) I/O 什么時(shí)候到來,就好比我們點(diǎn)了一份外賣,不知道外賣小哥們什么時(shí)候送過來,這個(gè)時(shí)候有兩個(gè)處理辦法:
第一個(gè)是我們可以先去睡覺,外賣小哥送到樓下了自然會(huì)給我們打電話,這個(gè)時(shí)候我們在醒來取外賣就可以了。
第二個(gè)是我們可以每隔一段時(shí)間就給外賣小哥打個(gè)電話,這樣就能實(shí)時(shí)掌握外賣的動(dòng)態(tài)信息了。
第一種方式對應(yīng)的就是阻塞的 I/O 處理方式,進(jìn)程在進(jìn)行 I/O 操作的時(shí)候,進(jìn)入睡眠,如果有 I/O 時(shí)間到達(dá),就喚醒這個(gè)進(jìn)程。第二種方式對應(yīng)的是非阻塞輪詢的方式,進(jìn)程在進(jìn)行 I/O 操作后,每隔一段時(shí)間向內(nèi)核詢問是否有 I/O 事件到達(dá),如果有就立刻處理。
線程池 OR 輪詢
在現(xiàn)實(shí)中,我們當(dāng)然選擇第一種方式,但是在計(jì)算機(jī)中,情況就要復(fù)雜一些。我們知道,在 linux 中,不管是線程還是進(jìn)程都會(huì)占用一定的資源,也就是說,系統(tǒng)總的線程和進(jìn)程數(shù)是一定的。如果有許多的線程或者進(jìn)程被掛起,無疑是白白消耗了系統(tǒng)的資源。而且,線程或者進(jìn)程的切換也是需要一定的成本的,需要上下文切換,如果頻繁的進(jìn)行上下文切換,系統(tǒng)會(huì)損失很大的性能。一個(gè)網(wǎng)絡(luò)服務(wù)器經(jīng)常需要連接成千上萬個(gè)客戶端,而它能創(chuàng)建的線程可能之后幾百個(gè),線程耗光就不能對外提供服務(wù)了。這些都是我們在選擇 I/O 機(jī)制的時(shí)候需要考慮的。這種阻塞的 I/O 模式下,一個(gè)線程只能處理一個(gè)流的 I/O 事件,這是問題的根源。
這個(gè)時(shí)候我們首先想到的是采用線程池的方式限制同時(shí)訪問的線程數(shù),這樣就能夠解決線程不足的問題了。但是這又會(huì)有第二個(gè)問題了,多余的任務(wù)會(huì)通過隊(duì)列的方式存儲(chǔ)在內(nèi)存只能夠,這樣很容易在客戶端過多的情況下出現(xiàn)內(nèi)存不足的情況。
還有一種方式是采用輪詢的方式,我們只要不停的把所有流從頭到尾問一遍,又從頭開始。這樣就可以處理多個(gè)流了。
代理
采用輪詢的方式雖然能夠處理多個(gè) I/O 事件,但是也有一個(gè)明顯的缺點(diǎn),那就是會(huì)導(dǎo)致 CPU 空轉(zhuǎn)。試想一下,如果所有的流中都沒有數(shù)據(jù),那么 CPU 時(shí)間就被白白的浪費(fèi)了。
為了避免CPU空轉(zhuǎn),可以引進(jìn)了一個(gè)代理。這個(gè)代理比較厲害,可以同時(shí)觀察許多流的I/O事件,在空閑的時(shí)候,會(huì)把當(dāng)前線程阻塞掉,當(dāng)有一個(gè)或多個(gè)流有I/O事件時(shí),就從阻塞態(tài)中醒來,于是我們的程序就會(huì)輪詢一遍所有的流。
這就是 select 與 poll 所做的事情,可見,采用 I/O 復(fù)用極大的提高了系統(tǒng)的效率。
為什么需要 epoll
select 與 poll 的缺陷
上文中我們發(fā)現(xiàn),實(shí)現(xiàn)一個(gè)代理來幫助我們處理 I/O 時(shí)間能夠極大的提高工作效率,select 與 poll 就是這樣的代理。
但是它們也不是完美的,從上文中我們可以發(fā)現(xiàn),我們能夠從 select 中知道是只是有 I/O 事件發(fā)生了。但是我們不知道那一個(gè)事件發(fā)生,每一個(gè) I/O 事件發(fā)生的時(shí)候,都需要輪詢所有的流,這樣的時(shí)間復(fù)雜度 O(N)。但是很多情況下,發(fā)生 I/O 時(shí)間的只是少數(shù)的幾個(gè)。通過輪詢所有的找出少數(shù)的幾個(gè)發(fā)生 I/O 的流顯然效率非常低下,因此 select 和 epoll 通常只能處理幾千個(gè)并發(fā)連接。
epoll 的優(yōu)勢
說了這么多,總算引出了我們的主人公 epoll 了。不同于忙輪詢和無差別輪詢,epoll 會(huì)把哪個(gè)流發(fā)生了怎樣的 I/O 事件通知我們。此時(shí)我們對這些流的操作都是有意義的。(復(fù)雜度降低到了O(k),k為產(chǎn)生 I/O 事件的流的個(gè)數(shù)。
epoll 原理
epoll 操作
epoll 在 linux 內(nèi)核中申請了一個(gè)簡易的文件系統(tǒng),把原先的一個(gè) select 或者 poll 調(diào)用分為了三個(gè)部分:調(diào)用 epoll_create 建立一個(gè) epoll 對象(在 epoll 文件系統(tǒng)中給這個(gè)句柄分配資源)、調(diào)用 epoll_ctl 向 epoll 對象中添加連接的套接字、調(diào)用 epoll_wait 收集發(fā)生事件的連接。這樣只需要在進(jìn)程啟動(dòng)的時(shí)候建立一個(gè) epoll 對象,并在需要的時(shí)候向它添加或者刪除連接就可以了,因此,在實(shí)際收集的時(shí)候,epoll_wait 的效率會(huì)非常高,因?yàn)檎{(diào)用的時(shí)候只是傳遞了發(fā)生 IO 事件的連接。
epoll 實(shí)現(xiàn)
我們以 linux 內(nèi)核 2.6 為例,說明一下 epoll 是如何高效的處理事件的。
當(dāng)某一個(gè)進(jìn)程調(diào)用 epoll_create 方法的時(shí)候,Linux 內(nèi)核會(huì)創(chuàng)建一個(gè) eventpoll 結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體中有兩個(gè)重要的成員。
第一個(gè)是 rb_root rbr,這是紅黑樹的根節(jié)點(diǎn),存儲(chǔ)著所有添加到 epoll 中的事件,也就是這個(gè) epoll 監(jiān)控的事件。
第二個(gè)是 list_head rdllist 這是一個(gè)雙向鏈表,保存著將要通過 epoll_wait 返回給用戶的、滿足條件的事件。
每一個(gè) epoll 對象都有一個(gè)獨(dú)立的 eventpoll 結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體會(huì)在內(nèi)核空間中創(chuàng)造獨(dú)立的內(nèi)存,用于存儲(chǔ)使用 epoll_ctl 方法向 epoll 對象中添加進(jìn)來的事件。這些事件都會(huì)掛到 rbr 紅黑樹中,這樣就能夠高效的識(shí)別重復(fù)添加的節(jié)點(diǎn)。
所有添加到 epoll 中的事件都會(huì)與設(shè)備(如網(wǎng)卡等)驅(qū)動(dòng)程序建立回調(diào)關(guān)系,也就是說,相應(yīng)的事件發(fā)生時(shí)會(huì)調(diào)用這里的方法。這個(gè)回調(diào)方法在內(nèi)核中叫做 ep_poll_callback,它把這樣的事件放到 rdllist 雙向鏈表中。在 epoll 中,對于每一個(gè)事件都會(huì)建立一個(gè) epitem 結(jié)構(gòu)體。
當(dāng)調(diào)用 epoll_wait 檢查是否有發(fā)生事件的連接時(shí),只需要檢查 eventpoll 對象中的 rdllist 雙向鏈表中是否有 epitem 元素,如果 rdllist 鏈表不為空,則把這里的事件復(fù)制到用戶態(tài)內(nèi)存中的同時(shí),將事件數(shù)量返回給用戶。通過這種方法,epoll_wait 的效率非常高。epoll-ctl 在向 epoll 對象中添加、修改、刪除事件時(shí),從 rbr 紅黑樹中查找事件也非常快。這樣,epoll 就能夠輕易的處理百萬級的并發(fā)連接。
epoll 工作模式
epoll 有兩種工作模式,LT(水平觸發(fā))模式與 ET(邊緣觸發(fā))模式。默認(rèn)情況下,epoll 采用 LT 模式工作。兩個(gè)的區(qū)別是:
Level_triggered(水平觸發(fā)):當(dāng)被監(jiān)控的文件描述符上有可讀寫事件發(fā)生時(shí),epoll_wait()會(huì)通知處理程序去讀寫。如果這次沒有把數(shù)據(jù)一次性全部讀寫完(如讀寫緩沖區(qū)太小),那么下次調(diào)用 epoll_wait() 時(shí),它還會(huì)通知你在上沒讀寫完的文件描述符上繼續(xù)讀寫,當(dāng)然如果你一直不去讀寫,它會(huì)一直通知你。如果系統(tǒng)中有大量你不需要讀寫的就緒文件描述符,而它們每次都會(huì)返回,這樣會(huì)大大降低處理程序檢索自己關(guān)心的就緒文件描述符的效率。
Edge_triggered(邊緣觸發(fā)):當(dāng)被監(jiān)控的文件描述符上有可讀寫事件發(fā)生時(shí),epoll_wait() 會(huì)通知處理程序去讀寫。如果這次沒有把數(shù)據(jù)全部讀寫完(如讀寫緩沖區(qū)太小),那么下次調(diào)用 epoll_wait()時(shí),它不會(huì)通知你,也就是它只會(huì)通知你一次,直到該文件描述符上出現(xiàn)第二次可讀寫事件才會(huì)通知你。這種模式比水平觸發(fā)效率高,系統(tǒng)不會(huì)充斥大量你不關(guān)心的就緒文件描述符。
當(dāng)然,在 LT 模式下開發(fā)基于 epoll 的應(yīng)用要簡單一些,不太容易出錯(cuò),而在 ET 模式下事件發(fā)生時(shí),如果沒有徹底地將緩沖區(qū)的數(shù)據(jù)處理完,則會(huì)導(dǎo)致緩沖區(qū)的用戶請求得不到響應(yīng)。注意,默認(rèn)情況下 Nginx 采用 ET 模式使用 epoll 的。
參考資料
https://www.cnblogs.com/ajianbeyourself/p/5859989.html
https://www.zhihu.com/question/20122137
總結(jié)
- 上一篇: js常用字符串函数
- 下一篇: curl+个人证书(又叫客户端证书)访问