原文鏈接: https://blog.csdn.net/apacat/article/details/51375950
搞Linux 服務(wù)器開發(fā)的人肯定了解 select粱胜、poll另萤、epoll予弧,他們都是基于事件驅(qū)動的IO多路復(fù)用技術(shù)月匣,而他們之間的區(qū)別網(wǎng)上已經(jīng)有很多的文章了历筝,大家可以去詳細的閱讀,我在這里主要想寫寫我對epoll的底層實現(xiàn)的理解傻昙。
首先還是先說說 select闺骚、poll相比與epoll來說他們效率低下的原因吧:
select、poll妆档、epoll是Linux平臺下的IO多路復(fù)用技術(shù)僻爽,適合用來管理大量的文件描述符,但是這些系統(tǒng)調(diào)用本身是阻塞的贾惦,而他們管理的socket描述符其實是可以阻塞胸梆,也可以非阻塞的,但是大部分情況下設(shè)置為非阻塞的要更好一些须板,效率會更高一些碰镜。因此,他們并不是真正的異步IO逼纸。是偽異步的洋措。
1、select
首先杰刽,select的缺點1:是select管理的描述符的數(shù)量在不重新編譯內(nèi)核的情況下是一個固定的值:1024,當(dāng)然王滤,重新編譯了Linux內(nèi)核之后贺嫂,這個數(shù)值可以繼續(xù)增大到用戶的需求,但是這是相對來說比較麻煩的一件事雁乡。
其次第喳。select的缺點2:是select對于socket描述符的管理方式,因為Linux內(nèi)核對select的實現(xiàn)方式為每次返回前都要對所有的描述符進行一遍遍歷踱稍,然后將有事件發(fā)生的socket描述符放到描述符集合里曲饱,然后將這個描述符集合返回悠抹。這種情況對于描述符的數(shù)量不是很大的時候還是可以的,但是當(dāng)描述符達到數(shù)十萬扩淀,甚至上百萬的時候楔敌,select的效率就會急劇的降低,因為這樣的輪詢機制會造成大量的浪費和資源開銷驻谆。因為每一次的輪詢都要將這些所有的socket描述符從用戶態(tài)拷貝到內(nèi)核態(tài)卵凑,在內(nèi)核態(tài),進行輪詢胜臊,查看是否有事件發(fā)生勺卢,這是select的底層需要做的。而這些拷貝完全是可以避免的象对。
2黑忱、poll
poll的實現(xiàn)機制和select是一樣的,也是采用輪詢機制來查看有事件發(fā)生的socket描述符勒魔,所以效率也是很低甫煞,但是poll對select有一項改進就是能夠監(jiān)視的描述符是任意大小的而不是局限在一個較小的數(shù)值上(當(dāng)然這個描述符的大小也是需要操作系統(tǒng)來支持的)。
綜上:在總結(jié)一下沥邻,select與poll的實現(xiàn)機制基本是一樣的危虱,只不過函數(shù)不同,參數(shù)不同唐全,但是基本流程是相同的埃跷;
1、復(fù)制用戶數(shù)據(jù)到內(nèi)核空間
2邮利、估計超時時間
3弥雹、遍歷每個文件并調(diào)用f_op->poll()取得文件狀態(tài)
4、遍歷完成檢查狀態(tài)
如果有就緒的文件(描述符對應(yīng)的還是文件延届,這里就當(dāng)成是描述符就可以)則跳轉(zhuǎn)到5剪勿,
如果有信號產(chǎn)生則重新啟動poll或者select
否則掛起進程并等待超時或喚醒超時或再次遍歷每個文件的狀態(tài)
5、將所有文件的就緒狀態(tài)復(fù)制到用戶空間
6方庭、清理申請的資源
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
3厕吉、epoll
epoll改進了select的兩個缺點,使用了三個數(shù)據(jù)結(jié)構(gòu)從而能夠在管理大量的描述符的情況下械念,對系統(tǒng)資源的使用并沒有急劇的增加头朱,而只是對內(nèi)存的使用有所增加(畢竟存儲大量的描述符的數(shù)據(jù)結(jié)構(gòu)會占用大量內(nèi)存)。
epoll在實現(xiàn)上的三個核心點是:1龄减、mmap项钮,2、紅黑樹,3烁巫、rdlist(就緒描述符鏈表)接下來一一解釋這三個并且解釋為什么會高效署隘;
1、mmap是共享內(nèi)存亚隙,用戶進程和內(nèi)核有一段地址(虛擬存儲器地址)映射到了同一塊物理地址上磁餐,這樣當(dāng)內(nèi)核要對描述符上的事件進行檢查的時候就不用來回的拷貝了。
三恃鞋、共享內(nèi)存
這點實際上涉及到epoll的具體實現(xiàn)了崖媚。內(nèi)核/用戶空間 內(nèi)存拷貝問題,如何讓內(nèi)核把FD消息通知給用戶空間呢恤浪?
在這個問題上select采取了內(nèi)存拷貝方法畅哑。Poll也是么?應(yīng)該是水由,poll和select基本無區(qū)別荠呐?(肯定多少還有點區(qū)別,只是這些缺點是相同的)
既然是內(nèi)存拷貝砂客,因為也要拷貝啊泥张,還是慢!
對于poll來說需要將用戶傳入的 pollfd 數(shù)組拷貝到內(nèi)核空間鞠值,因為拷貝操作和數(shù)組長度相關(guān)媚创,時間上這是一個O(n)操作,當(dāng)事件發(fā)生彤恶,poll返回將獲得的數(shù)據(jù)傳送到用戶空間并執(zhí)行釋放內(nèi)存和剝離等待隊列等善后工作钞钙,向用戶空間拷貝數(shù)據(jù)與剝離等待隊列等操作的的時間復(fù)雜度同樣是O(n)。
而epoll是共享內(nèi)存声离,拷貝都不用,相對來說應(yīng)該會更快芒炼。epoll是通過內(nèi)核與用戶空間mmap同一塊內(nèi)存實現(xiàn)的。
mmap是什么
mmap操作提供了一種機制术徊,讓用戶程序直接訪問設(shè)備內(nèi)存本刽,這種機制,相比較在用戶空間和內(nèi)核空間互相拷貝數(shù)據(jù)赠涮,效率更高子寓。在要求高性能的應(yīng)用中比較常用。mmap映射內(nèi)存必須是頁面大小的整數(shù)倍笋除,面向流的設(shè)備不能進行mmap别瞭,mmap的實現(xiàn)和硬件有關(guān)。
(涉及到內(nèi)核和用戶空間的概念株憾,共享內(nèi)存有沒有安全隱患?)
2、紅黑樹是用來存儲這些描述符的嗤瞎,因為紅黑樹的特性墙歪,就是良好的插入,查找贝奇,刪除性能O(lgN)虹菲。
當(dāng)內(nèi)核初始化epoll的時候(當(dāng)調(diào)用epoll_create的時候內(nèi)核也是個epoll描述符創(chuàng)建了一個文件,畢竟在Linux中一切都是文件掉瞳,而epoll面對的是一個特殊的文件毕源,和普通文件不同),會開辟出一塊內(nèi)核高速cache區(qū)陕习,這塊區(qū)域用來存儲我們要監(jiān)管的所有的socket描述符霎褐,當(dāng)然在這里面存儲一定有一個數(shù)據(jù)結(jié)構(gòu),這就是紅黑樹该镣,由于紅黑樹的接近平衡的查找冻璃,插入,刪除能力损合,在這里顯著的提高了對描述符的管理省艳。
3、rdlist 就緒描述符鏈表這是一個雙鏈表嫁审,epoll_wait()函數(shù)返回的也是這個就緒鏈表跋炕。
當(dāng)內(nèi)核創(chuàng)建了紅黑樹之后,同時也會建立一個雙向鏈表rdlist律适,用于存儲準備就緒的描述符辐烂,當(dāng)調(diào)用epoll_wait的時候在timeout時間內(nèi),只是簡單的去管理這個rdlist中是否有數(shù)據(jù)擦耀,如果沒有則睡眠至超時棉圈,如果有數(shù)據(jù)則立即返回并將鏈表中的數(shù)據(jù)賦值到events數(shù)組中。這樣就能夠高效的管理就緒的描述符眷蜓,而不用去輪詢所有的描述符分瘾。所以當(dāng)管理的描述符很多但是就緒的描述符數(shù)量很少的情況下如果用select來實現(xiàn)的話效率可想而知,很低吁系,但是epoll的話確實是非常適合這個時候使用德召。
對與rdlist的維護:當(dāng)執(zhí)行epoll_ctl時除了把socket描述符放入到紅黑樹中之外,還會給內(nèi)核中斷處理程序注冊一個回調(diào)函數(shù)汽纤,告訴內(nèi)核上岗,當(dāng)這個描述符上有事件到達(或者說中斷了)的時候就調(diào)用這個回調(diào)函數(shù)。這個回調(diào)函數(shù)的作用就是將描述符放入到rdlist中蕴坪,所以當(dāng)一個socket上的數(shù)據(jù)到達的時候內(nèi)核就會把網(wǎng)卡上的數(shù)據(jù)復(fù)制到內(nèi)核肴掷,然后把socket描述符插入就緒鏈表rdlist中敬锐。
補充:epoll的工作模式ET和LT
都知道epoll有兩個工作模式,ET和LT呆瞻,其中ET模式是高速模式台夺,叫做邊緣觸發(fā)模式,LT模式是默認模式痴脾,叫做水平觸發(fā)模式颤介。
這兩種工作模式的區(qū)別在于:
當(dāng)工作在ET模式下,如果一個描述符上有數(shù)據(jù)到達赞赖,然后讀取這個描述符上的數(shù)據(jù)如果沒有將數(shù)據(jù)全部讀完的話滚朵,當(dāng)下次epoll_wait返回的時候這個描述符里的數(shù)據(jù)就再也讀取不到了,因為這個描述符不會再次觸發(fā)返回前域,也就沒法去讀取辕近,所以對于這種模式下對一個描述符的數(shù)據(jù)的正確讀取方式是用一個死循環(huán)一直讀,讀到么有數(shù)據(jù)可讀的情況下才可以認為是讀取結(jié)束话侄。
而工作在LT模式下亏推,這種情況就不會發(fā)生,如果對一個描述符的數(shù)據(jù)沒有讀取完成年堆,那么下次當(dāng)epoll_wait返回的時候會繼續(xù)觸發(fā)吞杭,也就可以繼續(xù)獲取到這個描述符,從而能夠接著讀变丧。
那么這兩種模式的實現(xiàn)方式是什么樣的?
基于以上的數(shù)據(jù)結(jié)構(gòu)是怎么實現(xiàn)這種工作模式的呢芽狗?
實現(xiàn)原理:當(dāng)一個socket描述符的中斷事件發(fā)生,內(nèi)核會將數(shù)據(jù)從網(wǎng)卡復(fù)制到內(nèi)核痒蓬,同時將socket描述符插入到rdlist中童擎,此時如果調(diào)用了epoll_wait會把rdlist中的就緒的socekt描述符復(fù)制到用戶空間,然后清理掉這個rdlist中的數(shù)據(jù)攻晒,最后epoll_wait還會再次檢查這些socket描述符顾复,如果是工作在LT模式下,并且這些socket描述符上還有數(shù)據(jù)沒有讀取完成鲁捏,那么L就會再次把沒有讀完的socket描述符放入到rdlist中芯砸,所以再次調(diào)用epoll_wait的時候是會再次觸發(fā)的,而ET模式是不會這么干的给梅。
ET模式在物理實現(xiàn)上是基于電平的高低變化來工作的假丧,就是從高電平變成低電平,或者從低電平變成高電平的這個上升沿或者下降沿才會觸發(fā)动羽,也就是狀態(tài)變化導(dǎo)致觸發(fā)包帚,而當(dāng)一個描述符上數(shù)據(jù)未讀完的時候這個狀態(tài)是不會發(fā)生變化的,所以觸發(fā)不了运吓,LT模式是在只有出現(xiàn)高電平的時候才會觸發(fā)渴邦。
高電平和低電平:
LT水平觸發(fā):
EPOLLIN的觸發(fā)事件:當(dāng)輸入緩沖區(qū)為空-->低電平疯趟,當(dāng)輸入緩沖區(qū)不為空-->高電平
高電平的時候觸發(fā)EPOLLIN事件,如果沒有把緩沖區(qū)的數(shù)據(jù)讀取完几莽,下次還會觸發(fā)的迅办,因為始終是高電平
EPOLLOUT的觸發(fā)事件:當(dāng)發(fā)送緩沖區(qū)滿-->低電平,當(dāng)發(fā)送緩沖區(qū)不滿-->高電平
高電平的時候觸發(fā)EPOLLOUT事件章蚣,所以在一開始的時候不要關(guān)注EPOLLOUT時間,因為發(fā)送緩沖區(qū)是不滿的所以會導(dǎo)致CPU忙等待姨夹,每次都觸發(fā)纤垂。什么時候關(guān)注EPOLLOUT事件呢? 當(dāng)write的時候沒有寫完全,因為發(fā)送緩沖區(qū)滿了磷账,這個時候才關(guān)注EPOLLOUT事件直到下次把所有數(shù)據(jù)都發(fā)送完畢了峭沦,才取消EPOLLOUT事件
ET邊緣觸發(fā):
EPOLLIN事件發(fā)生的條件:
有數(shù)據(jù)到來(輸入緩沖區(qū)初始為空,為低電平逃糟,有數(shù)據(jù)到來變成了高電平)
EPOLLout事件發(fā)生的條件:
內(nèi)核發(fā)送緩沖區(qū)不滿(當(dāng)發(fā)送緩沖區(qū)出現(xiàn)滿之后為低電平吼鱼,然后內(nèi)核發(fā)送出去了部分數(shù)據(jù)后變成了不滿,也就是高電平)