整合營銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          select、poll和epoll的區(qū)別和 IO多路

          select、poll和epoll的區(qū)別和 IO多路復(fù)用模型講解

          elect、poll和epoll的區(qū)別

          在linux沒有實(shí)現(xiàn)epoll事件驅(qū)動(dòng)機(jī)制之前,我們一般選擇用select或者poll等IO多路復(fù)用的方法來實(shí)現(xiàn)并發(fā)服務(wù)程序。在大數(shù)據(jù)、高并發(fā)、集群等一些名詞唱的火熱之年代,select和poll的用武之地越來越有限了,風(fēng)頭已經(jīng)被epoll占盡。

          select()和poll() IO多路復(fù)用模型

          select的缺點(diǎn):

          單個(gè)進(jìn)程能夠監(jiān)視的文件描述符的數(shù)量存在最大限制,通常是1024,當(dāng)然可以更改數(shù)量,但由于select采用輪詢的方式掃描文件描述符,文件描述符數(shù)量越多,性能越差;

          內(nèi)核/用戶空間內(nèi)存拷貝問題,select需要復(fù)制大量的句柄數(shù)據(jù)結(jié)構(gòu),產(chǎn)生巨大的開銷

          select返回的是含有整個(gè)句柄的數(shù)組,應(yīng)用程序需要遍歷整個(gè)數(shù)組才能發(fā)現(xiàn)哪些句柄發(fā)生了事件;

          select的觸發(fā)方式是水平觸發(fā),應(yīng)用程序如果沒有完成對(duì)一個(gè)已經(jīng)就緒的文件描述符進(jìn)行IO,那么之后再次select調(diào)用還是會(huì)將這些文件描述符通知進(jìn)程。

          相比于select模型,poll使用鏈表保存文件描述符,因此沒有了監(jiān)視文件數(shù)量的限制,但其他三個(gè)缺點(diǎn)依然存在。

          拿select模型為例,假設(shè)我們的服務(wù)器需要支持100萬的并發(fā)連接,則在_FD_SETSIZE為1024的情況下,則我們至少需要開辟1k個(gè)進(jìn)程才能實(shí)現(xiàn)100萬的并發(fā)連接。除了進(jìn)程間上下文切換的時(shí)間消耗外,從內(nèi)核/用戶空間大量的無腦內(nèi)存拷貝、數(shù)組輪詢等,是系統(tǒng)難以承受的。因此,基于select模型的服務(wù)器程序,要達(dá)到10萬級(jí)別的并發(fā)訪問,是一個(gè)很難完成的任務(wù)。

          epoll IO多路復(fù)用模型實(shí)現(xiàn)機(jī)制

          由于epoll的實(shí)現(xiàn)機(jī)制與select/poll機(jī)制完全不同,上面所說的select的缺點(diǎn)在epoll上不復(fù)存在。

          設(shè)想一下如下場景:有100萬個(gè)客戶端同時(shí)與一個(gè)服務(wù)器進(jìn)程保持著TCP連接。而每一時(shí)刻,通常只有幾百上千個(gè)TCP連接是活躍的。如何實(shí)現(xiàn)這樣的高并發(fā)?

          在select/poll時(shí)代,服務(wù)器進(jìn)程每次都把這100萬個(gè)連接告訴操作系統(tǒng)(從用戶態(tài)復(fù)制句柄數(shù)據(jù)結(jié)構(gòu)到內(nèi)核態(tài)),讓操作系統(tǒng)內(nèi)核去查詢這些套接字上是否有事件發(fā)生,輪詢完后,再將句柄數(shù)據(jù)復(fù)制到用戶態(tài),讓服務(wù)器應(yīng)用程序輪詢處理已發(fā)生的網(wǎng)絡(luò)事件,這一過程資源消耗較大,因此,select/poll一般只能處理幾千的并發(fā)連接。

          epoll的設(shè)計(jì)和實(shí)現(xiàn)select完全不同。epoll通過在linux內(nèi)核中申請(qǐng)一個(gè)簡易的文件系統(tǒng)(文件系統(tǒng)一般用什么數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)?B+樹)。把原先的select/poll調(diào)用分成了3個(gè)部分:

          1)調(diào)用epoll_create()建立一個(gè)epoll對(duì)象(在epoll文件系統(tǒng)中為這個(gè)句柄對(duì)象分配資源)

          2)調(diào)用epoll_ctl向epoll對(duì)象中添加這100萬個(gè)連接的套接字

          3)調(diào)用epoll_wait收集發(fā)生的事件的連接

          如此一來,要實(shí)現(xiàn)上面說的場景,只需要在進(jìn)程啟動(dòng)時(shí)建立一個(gè)epoll對(duì)象,然后在需要的時(shí)候向這個(gè)epoll對(duì)象中添加或者刪除連接。同時(shí),epoll_wait的效率也非常高,因?yàn)檎{(diào)用epoll_wait時(shí),并沒有一股腦的向操作系統(tǒng)復(fù)制這100萬個(gè)連接的句柄數(shù)據(jù),內(nèi)核也不需要去遍歷全部的連接。

          上面的3個(gè)部分非常清晰,首先要調(diào)用epoll_create創(chuàng)建一個(gè)epoll對(duì)象。然后使用epoll_ctl可以操作上面建立的epoll對(duì)象,例如,將剛建立的socket加入到epoll中讓其監(jiān)控,或者把epoll正在監(jiān)控的某個(gè)socket句柄移出epoll,不再監(jiān)控它等等。

          epoll_wait在調(diào)用時(shí),在給定的timeout時(shí)間內(nèi),當(dāng)在監(jiān)控的所有句柄中有事件發(fā)生時(shí),就返回用戶態(tài)的進(jìn)程。

          從上面的調(diào)用方式就可以看到epoll比select/poll的優(yōu)越之處:因?yàn)楹笳呙看握{(diào)用時(shí)都要傳遞你所要監(jiān)控的所有socket給select/poll系統(tǒng)調(diào)用,這意味著需要將用戶態(tài)的socket列表copy到內(nèi)核態(tài),如果以萬計(jì)的句柄會(huì)導(dǎo)致每次都要copy幾十幾百KB的內(nèi)存到內(nèi)核態(tài),非常低效。而我們調(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)控的句柄列表。

          所以,實(shí)際上在你調(diào)用epoll_create后,內(nèi)核就已經(jīng)在內(nèi)核態(tài)開始準(zhǔn)備幫你存儲(chǔ)要監(jiān)控的句柄了,每次調(diào)用epoll_ctl只是在往內(nèi)核的數(shù)據(jù)結(jié)構(gòu)里塞入新的socket句柄。

          在內(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ì)開辟出epoll自己的內(nèi)核高速cache區(qū),用于安置每一個(gè)我們想監(jiān)控的socket,這些socket會(huì)以紅黑樹的形式保存在內(nèi)核cache里,以支持快速的查找、插入、刪除。這個(gè)內(nèi)核高速cache區(qū),就是建立連續(xù)的物理內(nèi)存頁,然后在之上建立slab層,簡單的說,就是物理上分配好你想要的size的內(nèi)存對(duì)象,每次使用時(shí)都是使用空閑的已分配好的對(duì)象。

          epoll的高效就在于,當(dāng)我們調(diào)用epoll_ctl往里塞入百萬個(gè)句柄時(shí),epoll_wait仍然可以飛快的返回,并有效的將發(fā)生事件的句柄給我們用戶。這是由于我們?cè)谡{(diào)用epoll_create時(shí),內(nèi)核除了幫我們?cè)趀poll文件系統(tǒng)里建了個(gè)file結(jié)點(diǎn),在內(nèi)核cache里建了個(gè)紅黑樹用于存儲(chǔ)以后epoll_ctl傳來的socket外,還會(huì)再建立一個(gè)list鏈表,用于存儲(chǔ)準(zhǔn)備就緒的事件,當(dāng)epoll_wait調(diào)用時(shí),僅僅觀察這個(gè)list鏈表里有沒有數(shù)據(jù)即可。有數(shù)據(jù)就返回,沒有數(shù)據(jù)就sleep,等到timeout時(shí)間到后即使鏈表沒數(shù)據(jù)也返回。所以,epoll_wait非常高效。

          而且,通常情況下即使我們要監(jiān)控百萬計(jì)的句柄,大多一次也只返回很少量的準(zhǔn)備就緒句柄而已,所以,epoll_wait僅需要從內(nèi)核態(tài)copy少量的句柄到用戶態(tài)而已,如何能不高效?!

          那么,這個(gè)準(zhǔn)備就緒list鏈表是怎么維護(hù)的呢?當(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)備就緒鏈表里了。

          如此,一顆紅黑樹,一張準(zhǔn)備就緒句柄鏈表,少量的內(nèi)核cache,就幫我們解決了大并發(fā)下的socket處理問題。執(zhí)行epoll_create時(shí),創(chuàng)建了紅黑樹和就緒鏈表,執(zhí)行epoll_ctl時(shí),如果增加socket句柄,則檢查在紅黑樹中是否存在,存在立即返回,不存在則添加到樹干上,然后向內(nèi)核注冊(cè)回調(diào)函數(shù),用于當(dāng)中斷事件來臨時(shí)向準(zhǔn)備就緒鏈表中插入數(shù)據(jù)。執(zhí)行epoll_wait時(shí)立刻返回準(zhǔn)備就緒鏈表里的數(shù)據(jù)即可。

          最后看看epoll獨(dú)有的兩種模式LT和ET。無論是LT和ET模式,都適用于以上所說的流程。區(qū)別是,LT模式下,只要一個(gè)句柄上的事件一次沒有處理完,會(huì)在以后調(diào)用epoll_wait時(shí)次次返回這個(gè)句柄,而ET模式僅在第一次返回。

          這件事怎么做到的呢?當(dāng)一個(gè)socket句柄上有事件時(shí),內(nèi)核會(huì)把該句柄插入上面所說的準(zhǔn)備就緒list鏈表,這時(shí)我們調(diào)用epoll_wait,會(huì)把準(zhǔn)備就緒的socket拷貝到用戶態(tài)內(nèi)存,然后清空準(zhǔn)備就緒list鏈表,最后,epoll_wait干了件事,就是檢查這些socket,如果不是ET模式(就是LT模式的句柄了),并且這些socket上確實(shí)有未處理的事件時(shí),又把該句柄放回到剛剛清空的準(zhǔn)備就緒鏈表了。所以,非ET的句柄,只要它上面還有事件,epoll_wait每次都會(huì)返回。而ET模式的句柄,除非有新中斷到,即使socket上的事件沒有處理完,也是不會(huì)次次從epoll_wait返回的。

          其中涉及到的數(shù)據(jù)結(jié)構(gòu):

          epoll用kmem_cache_create(slab分配器)分配內(nèi)存用來存放structepitem和structeppoll_entry。

          當(dāng)向系統(tǒng)中添加一個(gè)fd時(shí),就創(chuàng)建一個(gè)epitem結(jié)構(gòu)體,這是內(nèi)核管理epoll的基本數(shù)據(jù)結(jié)構(gòu):

          structepitem{

          structrb_noderbn;//用于主結(jié)構(gòu)管理的紅黑樹

          structlist_headrdllink;//事件就緒隊(duì)列

          structepitem*next;//用于主結(jié)構(gòu)體中的鏈表

          structepoll_filefdffd;//這個(gè)結(jié)構(gòu)體對(duì)應(yīng)的被監(jiān)聽的文件描述符信息

          intnwait;//poll操作中事件的個(gè)數(shù)

          structlist_headpwqlist;//雙向鏈表,保存著被監(jiān)視文件的等待隊(duì)列,功能類似于select/poll中的poll_table

          structeventpoll*ep;//該項(xiàng)屬于哪個(gè)主結(jié)構(gòu)體(多個(gè)epitm從屬于一個(gè)eventpoll)

          structlist_headfllink;//雙向鏈表,用來鏈接被監(jiān)視的文件描述符對(duì)應(yīng)的struct file。因?yàn)閒ile里有f_ep_link,用來保存所有監(jiān)視這個(gè)文件的epoll節(jié)點(diǎn)

          structepoll_eventevent;//注冊(cè)的感興趣的事件,也就是用戶空間的epoll_event

          }

          而每個(gè)epoll fd(epfd)對(duì)應(yīng)的主要數(shù)據(jù)結(jié)構(gòu)為:

          structeventpoll {

          spin_lock_tlock;//對(duì)本數(shù)據(jù)結(jié)構(gòu)的訪問

          structmutex mtx;//防止使用時(shí)被刪除

          wait_queue_head_t wq;//sys_epoll_wait()使用的等待隊(duì)列

          wait_queue_head_tpoll_wait; //file->poll()使用的等待隊(duì)列

          structlist_head rdllist;//事件滿足條件的鏈表 /*雙鏈表中則存放著將要通過epoll_wait返回給用戶的滿足條件的事件*/

          structrb_rootrbr;//用于管理所有fd的紅黑樹(樹根)/*紅黑樹的根節(jié)點(diǎn),這顆樹中存儲(chǔ)著所有添加到epoll中的需要監(jiān)控的事件*/

          structepitem*ovflist;//將事件到達(dá)的fd進(jìn)行鏈接起來發(fā)送至用戶空間

          }

          structeventpoll在epoll_create時(shí)創(chuàng)建。

          這樣說來,內(nèi)核中維護(hù)了一棵紅黑樹,大致的結(jié)構(gòu)如下:

          當(dāng)調(diào)用epoll_wait檢查是否有事件發(fā)生時(shí),只需要檢查eventpoll對(duì)象中的rdlist雙鏈表中是否有epitem元素即可。如果rdlist不為空,則把發(fā)生的事件復(fù)制到用戶態(tài),同時(shí)將事件數(shù)量返回給用戶。

          、概覽

          jQuery官網(wǎng):https://jquery.com/
          jQuery是一個(gè)高效、輕量并且功能豐富的js庫。
          核心在于查詢query。
          jQuery是一個(gè)優(yōu)秀的js函數(shù)庫,是React/Vue/Angular框架之外中大型項(xiàng)目的首選。
          jQuery的主旨是write less, do more。

          1.1 jQuery的功能

          • html元素的選取
          • 操作html元素
          • css操作
          • html事件處理
          • 實(shí)現(xiàn)js動(dòng)畫效果
          • 能夠鏈?zhǔn)秸{(diào)用
          • 容易擴(kuò)展插件
          • 封裝了ajax

          1.2 引入jQuery庫

          引入jQuery的方式有2種,一種是項(xiàng)目中直接引入jQuery的min.js文件,一種是使用服務(wù)器端jQuery文件(使用cdn)腳本標(biāo)簽方式引入。

          1.2.1 本地項(xiàng)目引入

          在官網(wǎng)的:https://jquery.com/download/ 鏈接下可以下載到完整的代碼,放到項(xiàng)目文件的js文件夾下。

          <script src="static/js/jquery-3.7.1.min.js"></script>

          1.2.2 cdn方式引入

          在網(wǎng)站:https://www.bootcdn.cn/ 可以獲得穩(wěn)定、快速、免費(fèi)的cdn加速服務(wù)。

          <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.js"></script>

          1.2.3 版本兼容

          • 1.x 版本兼容老版本的IE,文件比較大
          • 2.x 版本文件比較小,支持IE8+
          • 3.x 版本引入部分新API,提供多個(gè)分包的版本,支持IE9+

          1.2.4 開發(fā)的正確姿勢

          開發(fā)過程中一般使用非min.js文件方便調(diào)試,生產(chǎn)環(huán)境部署上線時(shí)才使用min.js這種壓縮文件。

          二、jQuery源碼淺析

          2.1 匿名函數(shù)調(diào)用

          從源碼中可以看出,jQuery的整體邏輯可以用以下簡單的結(jié)構(gòu)進(jìn)行描述:

          ( function( global, factory ) {
              // 判斷有無window環(huán)境的一堆邏輯代碼
          })( typeof window !=="undefined" ? window : this, function( window, noGlobal ) {
              // 構(gòu)造jQuery的一些邏輯代碼
              return jQuery
          });

          2.2 jQuery是一個(gè)函數(shù)

          從源碼中可以看出,jQuery被定義為一個(gè)函數(shù),函數(shù)中返回了一個(gè)實(shí)例對(duì)象(看new關(guān)鍵字)。

          繼續(xù)跟蹤源碼 new jQuery.fn.init( selector, context),這個(gè)函數(shù)中調(diào)用了makeArray,當(dāng)然在其他if判斷語句中也有返回偽數(shù)組對(duì)象(比如,定義了length字段,還有[0]的操作),這里拿makeArray作為演示。

          查看makeArray函數(shù):

          所以這個(gè)返回實(shí)例對(duì)象,是一個(gè)偽數(shù)組。

          $('#menu-trigger') instanceof Array // false
          $('#menu-trigger') instanceof Object // true

          2.3 jQuery掛載在window上

          從源碼中可以看出,將jQuery函數(shù)和window.$ 以及window.jQuery綁定賦值,所以使用jQuery和$ 標(biāo)識(shí)符就可以直接使用jQuery。通常在項(xiàng)目中直接使用$標(biāo)識(shí)符,快捷簡省。

          2.4 jQuery驗(yàn)證

          所以在引入jQuery的項(xiàng)目中:

          console.log(typeof $); // function
          console.log($===jQuery); // true
          console.log($() instanceof Object); // true

          三、jQuery常見用法

          3.1 函數(shù)形式調(diào)用

          通常形式為:$(param)

          • param為函數(shù):dom加載完成后,執(zhí)行該回調(diào)函數(shù)
          • param為選擇器字符串:查找與該選擇器匹配的所有標(biāo)簽,并封裝成jQuery對(duì)象
          • param為dom對(duì)象:將該dom對(duì)象封裝成jQuery對(duì)象
          • param為標(biāo)簽字符串:創(chuàng)建標(biāo)簽對(duì)象并封裝成jQuery對(duì)象
          $(function() {
              console.log("dom finished and execute this");
          })
          
          $('#btn').click(function () {
              // 這里的this是id為#btn的dom元素
              console.log(this.innerHTML)
              
              console.log($(this).html())
          })
          
          $('<input type="number"></input>').appendTo('div')

          3.2 點(diǎn)語法調(diào)用函數(shù)

          let list=[1, 2, 3]
          $.each(list, function(i, ele) {
              console.log(i, ele)
          })
          
          $.trim(' hello world ')

          3.3 用法淺析

          • jQuery函數(shù)返回的是一個(gè)偽數(shù)組(Object對(duì)象),可以使用length和下標(biāo)。
          // class中名為btn的dom元素有多少
          $('.btn').length
          
          $('.btn')[0]
          
          $('.btn').get(0)
          
          $('.btn').index()
          
          // 設(shè)置名為btn的class對(duì)應(yīng)的dom標(biāo)簽的文本內(nèi)容
          $('.btn').text('自定義文本內(nèi)容')

          通過$(param)傳入的是selector、element、標(biāo)簽情況下,返回的是包含1個(gè)或者多個(gè)dom元素對(duì)象的偽數(shù)組。

          3.4 獲取一組dom元素的常見用法

          // 基礎(chǔ)標(biāo)簽和class
          // 選擇了所有的div和span標(biāo)簽
          $('div, span')
          
          // 選擇所有具有某個(gè)class的標(biāo)簽
          $('div.container')
          
          // 層次選擇器
          $('ul span') // ul標(biāo)簽下的所有span元素
          $('ul>span') // ul標(biāo)簽下的所有子span元素
          $('.container+li') // class為container的元素后的下一個(gè)li元素
          $('ul .item~*') // class為item的元素后面所有兄弟元素
          
          // 過濾選擇器
          $('div:first') // 選擇第一個(gè)div
          $('div:last') // 最后一個(gè)div
          $('div:not(.container)') // class不為container的所有div
          $('div:lt(3):gt(0)') // 所有div元素中的大于0小于3的div元素,表示1和2索引處的dom元素
          $('div:containers("hello world")') // 內(nèi)容為hellow world的div元素
          $('div:hidden') // style中display: none的div元素
          $('div[data]') // 有data屬性的div元素, example: <div data=""></div>
          $('div[data="123"]') // 有data屬性且值為123的div元素, example: <div data="123"></div>
          
          // 示例,使table表格的奇數(shù)行背景樣式設(shè)置
          $('table>tbody>tr:odd')
          
          // form表單中
          $(':text') // 所有單行輸入框
          $(':text:disabled') // 所有disabled的input輸入框
          $(':checkbox') // 所有checkbox
          $(':checkbox:checked') // 所有選中的checkbox
          $('select').val() // select標(biāo)簽選中的option的value值

          3.5 修改css

          直接修改css屬性(如果其dom標(biāo)簽存在這個(gè)css屬性)

          $('#container').css('background', 'red');
          
          $('#container').css({ 'background' : 'red', 'color': 'blue' }) // 一組屬性

          清空某標(biāo)簽下的所有dom:

          $('.carousel-inner').empty();

          給某標(biāo)簽下添加dom標(biāo)簽:

          $('.carousel-inner').append(domStr);

          移除、添加class:

          $('.carousel-indicators li').removeClass('active');
          $('.carousel-indicators li:first').addClass('active');

          3.6 獲取屬性

          獲取dom標(biāo)簽上的屬性:

           $('.about-img-1>img').attr('src');

          設(shè)置標(biāo)簽的屬性:

           $('.about-img-1>img').attr('src', (data && data['image']) ? data['image'] : '');

          3.7 一些dom事件

          點(diǎn)擊:

          $('.category-product-page-ul>li').click(function(e) {
              e.preventDefault();
              console.log('this is:', this); // 打印對(duì)應(yīng)的dom標(biāo)簽
          });

          hover:

          $('#container').hover(  
              function() {  
                  // 當(dāng)鼠標(biāo)進(jìn)入元素時(shí)執(zhí)行的函數(shù)
              },
              function() {  
                  // 當(dāng)鼠標(biāo)離開元素時(shí)執(zhí)行的函數(shù)
              }  
            );

          監(jiān)聽事件:

          $('.bigImage').on("mousemove", function( e ) {
              // do something
          });

          3.8 發(fā)起ajax請(qǐng)求

          const json='/static/js/data/xxx.json';
           $.ajax({
              url: json,  
              dataType: 'json',  
              success: function(data) {
                // do something
              },
              error: function(jqXHR, textStatus, errorThrown) {  
                console.error('Fail to read json:', textStatus, errorThrown, json);
              }  
            });

          post請(qǐng)求:

          /O多路復(fù)用

          I/O多路復(fù)用(multiplexing)的本質(zhì)是通過一種機(jī)制(系統(tǒng)內(nèi)核緩沖I/O數(shù)據(jù)),讓單個(gè)進(jìn)程可以監(jiān)視多個(gè)文件描述符,一旦某個(gè)描述符就緒(一般是讀就緒或?qū)懢途w),能夠通知程序進(jìn)行相應(yīng)的讀寫操作

          select、poll 和 epoll 都是 Linux API 提供的 IO 復(fù)用方式。

          相信大家都了解了Unix五種IO模型,不了解的可以=> 查看這里

          1. blocking IO - 阻塞IO
          2. nonblocking IO - 非阻塞IO
          3. IO multiplexing - IO多路復(fù)用
          4. signal driven IO - 信號(hào)驅(qū)動(dòng)IO
          5. asynchronous IO - 異步IO

          其中前面4種IO都可以歸類為synchronous IO - 同步IO,而select、poll、epoll本質(zhì)上也都是同步I/O,因?yàn)樗麄兌夹枰谧x寫事件就緒后自己負(fù)責(zé)進(jìn)行讀寫,也就是說這個(gè)讀寫過程是阻塞的。

          與多進(jìn)程和多線程技術(shù)相比,I/O多路復(fù)用技術(shù)的最大優(yōu)勢是系統(tǒng)開銷小,系統(tǒng)不必創(chuàng)建進(jìn)程/線程,也不必維護(hù)這些進(jìn)程/線程,從而大大減小了系統(tǒng)的開銷。

          在介紹select、poll、epoll之前,首先介紹一下Linux操作系統(tǒng)中基礎(chǔ)的概念:

          • 用戶空間 / 內(nèi)核空間現(xiàn)在操作系統(tǒng)都是采用虛擬存儲(chǔ)器,那么對(duì)32位操作系統(tǒng)而言,它的尋址空間(虛擬存儲(chǔ)空間)為4G(2的32次方)。操作系統(tǒng)的核心是內(nèi)核,獨(dú)立于普通的應(yīng)用程序,可以訪問受保護(hù)的內(nèi)存空間,也有訪問底層硬件設(shè)備的所有權(quán)限。為了保證用戶進(jìn)程不能直接操作內(nèi)核(kernel),保證內(nèi)核的安全,操作系統(tǒng)將虛擬空間劃分為兩部分,一部分為內(nèi)核空間,一部分為用戶空間。
          • 進(jìn)程切換為了控制進(jìn)程的執(zhí)行,內(nèi)核必須有能力掛起正在CPU上運(yùn)行的進(jìn)程,并恢復(fù)以前掛起的某個(gè)進(jìn)程的執(zhí)行。這種行為被稱為進(jìn)程切換。因此可以說,任何進(jìn)程都是在操作系統(tǒng)內(nèi)核的支持下運(yùn)行的,是與內(nèi)核緊密相關(guān)的,并且進(jìn)程切換是非常耗費(fèi)資源的。
          • 進(jìn)程阻塞正在執(zhí)行的進(jìn)程,由于期待的某些事件未發(fā)生,如請(qǐng)求系統(tǒng)資源失敗、等待某種操作的完成、新數(shù)據(jù)尚未到達(dá)或無新工作做等,則由系統(tǒng)自動(dòng)執(zhí)行阻塞原語(Block),使自己由運(yùn)行狀態(tài)變?yōu)樽枞麪顟B(tài)。可見,進(jìn)程的阻塞是進(jìn)程自身的一種主動(dòng)行為,也因此只有處于運(yùn)行態(tài)的進(jìn)程(獲得了CPU資源),才可能將其轉(zhuǎn)為阻塞狀態(tài)。當(dāng)進(jìn)程進(jìn)入阻塞狀態(tài),是不占用CPU資源的。
          • 文件描述符文件描述符(File descriptor)是計(jì)算機(jī)科學(xué)中的一個(gè)術(shù)語,是一個(gè)用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一個(gè)非負(fù)整數(shù)。實(shí)際上,它是一個(gè)索引值,指向內(nèi)核為每一個(gè)進(jìn)程所維護(hù)的該進(jìn)程打開文件的記錄表。當(dāng)程序打開一個(gè)現(xiàn)有文件或者創(chuàng)建一個(gè)新文件時(shí),內(nèi)核向進(jìn)程返回一個(gè)文件描述符。在程序設(shè)計(jì)中,一些涉及底層的程序編寫往往會(huì)圍繞著文件描述符展開。但是文件描述符這一概念往往只適用于UNIX、Linux這樣的操作系統(tǒng)。
          • 緩存I/O緩存I/O又稱為標(biāo)準(zhǔn)I/O,大多數(shù)文件系統(tǒng)的默認(rèn)I/O操作都是緩存I/O。在Linux的緩存I/O機(jī)制中,操作系統(tǒng)會(huì)將I/O的數(shù)據(jù)緩存在文件系統(tǒng)的頁緩存中,即數(shù)據(jù)會(huì)先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中,然后才會(huì)從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的地址空間。

          Select

          我們先分析一下select函數(shù)

          int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);

          【參數(shù)說明】int maxfdp1 指定待測試的文件描述字個(gè)數(shù),它的值是待測試的最大描述字加1。fd_set *readset , fd_set *writeset , fd_set *exceptsetfd_set可以理解為一個(gè)集合,這個(gè)集合中存放的是文件描述符(file descriptor),即文件句柄。中間的三個(gè)參數(shù)指定我們要讓內(nèi)核測試讀、寫和異常條件的文件描述符集合。如果對(duì)某一個(gè)的條件不感興趣,就可以把它設(shè)為空指針。const struct timeval *timeout timeout告知內(nèi)核等待所指定文件描述符集合中的任何一個(gè)就緒可花多少時(shí)間。其timeval結(jié)構(gòu)用于指定這段時(shí)間的秒數(shù)和微秒數(shù)。

          【返回值】int 若有就緒描述符返回其數(shù)目,若超時(shí)則為0,若出錯(cuò)則為-1

          select運(yùn)行機(jī)制

          select()的機(jī)制中提供一種fd_set的數(shù)據(jù)結(jié)構(gòu),實(shí)際上是一個(gè)long類型的數(shù)組,每一個(gè)數(shù)組元素都能與一打開的文件句柄(不管是Socket句柄,還是其他文件或命名管道或設(shè)備句柄)建立聯(lián)系,建立聯(lián)系的工作由程序員完成,當(dāng)調(diào)用select()時(shí),由內(nèi)核根據(jù)IO狀態(tài)修改fd_set的內(nèi)容,由此來通知執(zhí)行了select()的進(jìn)程哪一Socket或文件可讀。

          從流程上來看,使用select函數(shù)進(jìn)行IO請(qǐng)求和同步阻塞模型沒有太大的區(qū)別,甚至還多了添加監(jiān)視socket,以及調(diào)用select函數(shù)的額外操作,效率更差。但是,使用select以后最大的優(yōu)勢是用戶可以在一個(gè)線程內(nèi)同時(shí)處理多個(gè)socket的IO請(qǐng)求。用戶可以注冊(cè)多個(gè)socket,然后不斷地調(diào)用select讀取被激活的socket,即可達(dá)到在同一個(gè)線程內(nèi)同時(shí)處理多個(gè)IO請(qǐng)求的目的。而在同步阻塞模型中,必須通過多線程的方式才能達(dá)到這個(gè)目的。

          select機(jī)制的問題

          1. 每次調(diào)用select,都需要把fd_set集合從用戶態(tài)拷貝到內(nèi)核態(tài),如果fd_set集合很大時(shí),那這個(gè)開銷也很大
          2. 同時(shí)每次調(diào)用select都需要在內(nèi)核遍歷傳遞進(jìn)來的所有fd_set,如果fd_set集合很大時(shí),那這個(gè)開銷也很大
          3. 為了減少數(shù)據(jù)拷貝帶來的性能損壞,內(nèi)核對(duì)被監(jiān)控的fd_set集合大小做了限制,并且這個(gè)是通過宏控制的,大小不可改變(限制為1024)

          Poll

          poll的機(jī)制與select類似,與select在本質(zhì)上沒有多大差別,管理多個(gè)描述符也是進(jìn)行輪詢,根據(jù)描述符的狀態(tài)進(jìn)行處理,但是poll沒有最大文件描述符數(shù)量的限制。也就是說,poll只解決了上面的問題3,并沒有解決問題1,2的性能開銷問題。

          下面是pll的函數(shù)原型:

          int poll(struct pollfd *fds, nfds_t nfds, int timeout);
          
          typedef struct pollfd {
                  int fd;                         // 需要被檢測或選擇的文件描述符
                  short events;                   // 對(duì)文件描述符fd上感興趣的事件
                  short revents;                  // 文件描述符fd上當(dāng)前實(shí)際發(fā)生的事件
          } pollfd_t;

          poll改變了文件描述符集合的描述方式,使用了pollfd結(jié)構(gòu)而不是select的fd_set結(jié)構(gòu),使得poll支持的文件描述符集合限制遠(yuǎn)大于select的1024

          【參數(shù)說明】struct pollfd *fds fds是一個(gè)struct pollfd類型的數(shù)組,用于存放需要檢測其狀態(tài)的socket描述符,并且調(diào)用poll函數(shù)之后fds數(shù)組不會(huì)被清空;一個(gè)pollfd結(jié)構(gòu)體表示一個(gè)被監(jiān)視的文件描述符,通過傳遞fds指示 poll() 監(jiān)視多個(gè)文件描述符。其中,結(jié)構(gòu)體的events域是監(jiān)視該文件描述符的事件掩碼,由用戶來設(shè)置這個(gè)域,結(jié)構(gòu)體的revents域是文件描述符的操作結(jié)果事件掩碼,內(nèi)核在調(diào)用返回時(shí)設(shè)置這個(gè)域

          nfds_t nfds 記錄數(shù)組fds中描述符的總數(shù)量

          【返回值】int 函數(shù)返回fds集合中就緒的讀、寫,或出錯(cuò)的描述符數(shù)量,返回0表示超時(shí),返回-1表示出錯(cuò);

          Epoll

          epoll在Linux2.6內(nèi)核正式提出,是基于事件驅(qū)動(dòng)的I/O方式,相對(duì)于select來說,epoll沒有描述符個(gè)數(shù)限制,使用一個(gè)文件描述符管理多個(gè)描述符,將用戶關(guān)心的文件描述符的事件存放到內(nèi)核的一個(gè)事件表中,這樣在用戶空間和內(nèi)核空間的copy只需一次。

          Linux中提供的epoll相關(guān)函數(shù)如下:

          int epoll_create(int size);
          int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
          int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
          1. epoll_create 函數(shù)創(chuàng)建一個(gè)epoll句柄,參數(shù)size表明內(nèi)核要監(jiān)聽的描述符數(shù)量。調(diào)用成功時(shí)返回一個(gè)epoll句柄描述符,失敗時(shí)返回-1。
          2. epoll_ctl 函數(shù)注冊(cè)要監(jiān)聽的事件類型。四個(gè)參數(shù)解釋如下:epfd 表示epoll句柄op 表示fd操作類型,有如下3種EPOLL_CTL_ADD 注冊(cè)新的fd到epfd中EPOLL_CTL_MOD 修改已注冊(cè)的fd的監(jiān)聽事件EPOLL_CTL_DEL 從epfd中刪除一個(gè)fdfd 是要監(jiān)聽的描述符event 表示要監(jiān)聽的事件epoll_event 結(jié)構(gòu)體定義如下:
          struct epoll_event {
              __uint32_t events;  /* Epoll events */
              epoll_data_t data;  /* User data variable */
          };
          
          typedef union epoll_data {
              void *ptr;
              int fd;
              __uint32_t u32;
              __uint64_t u64;
          } epoll_data_t;
          1. epoll_wait 函數(shù)等待事件的就緒,成功時(shí)返回就緒的事件數(shù)目,調(diào)用失敗時(shí)返回 -1,等待超時(shí)返回 0。epfd 是epoll句柄events 表示從內(nèi)核得到的就緒事件集合maxevents 告訴內(nèi)核events的大小timeout 表示等待的超時(shí)事件

          epoll是Linux內(nèi)核為處理大批量文件描述符而作了改進(jìn)的poll,是Linux下多路復(fù)用IO接口select/poll的增強(qiáng)版本,它能顯著提高程序在大量并發(fā)連接中只有少量活躍的情況下的系統(tǒng)CPU利用率。原因就是獲取事件的時(shí)候,它無須遍歷整個(gè)被偵聽的描述符集,只要遍歷那些被內(nèi)核IO事件異步喚醒而加入Ready隊(duì)列的描述符集合就行了。

          epoll除了提供select/poll那種IO事件的水平觸發(fā)(Level Triggered)外,還提供了邊緣觸發(fā)(Edge Triggered),這就使得用戶空間程序有可能緩存IO狀態(tài),減少epoll_wait/epoll_pwait的調(diào)用,提高應(yīng)用程序效率。

          • 水平觸發(fā)(LT):默認(rèn)工作模式,即當(dāng)epoll_wait檢測到某描述符事件就緒并通知應(yīng)用程序時(shí),應(yīng)用程序可以不立即處理該事件;下次調(diào)用epoll_wait時(shí),會(huì)再次通知此事件
          • 邊緣觸發(fā)(ET): 當(dāng)epoll_wait檢測到某描述符事件就緒并通知應(yīng)用程序時(shí),應(yīng)用程序必須立即處理該事件。如果不處理,下次調(diào)用epoll_wait時(shí),不會(huì)再次通知此事件。(直到你做了某些操作導(dǎo)致該描述符變成未就緒狀態(tài)了,也就是說邊緣觸發(fā)只在狀態(tài)由未就緒變?yōu)榫途w時(shí)只通知一次)。

          LT和ET原本應(yīng)該是用于脈沖信號(hào)的,可能用它來解釋更加形象。Level和Edge指的就是觸發(fā)點(diǎn),Level為只要處于水平,那么就一直觸發(fā),而Edge則為上升沿和下降沿的時(shí)候觸發(fā)。比如:0->1 就是Edge,1->1 就是Level。

          ET模式很大程度上減少了epoll事件的觸發(fā)次數(shù),因此效率比LT模式下高。

          總結(jié)

          一張圖總結(jié)一下select,poll,epoll的區(qū)別:


          select

          poll

          epoll

          操作方式

          遍歷

          遍歷

          回調(diào)

          底層實(shí)現(xiàn)

          數(shù)組

          鏈表

          哈希表

          IO效率

          每次調(diào)用都進(jìn)行線性遍歷,時(shí)間復(fù)雜度為O(n)

          每次調(diào)用都進(jìn)行線性遍歷,時(shí)間復(fù)雜度為O(n)

          事件通知方式,每當(dāng)fd就緒,系統(tǒng)注冊(cè)的回調(diào)函數(shù)就會(huì)被調(diào)用,將就緒fd放到readyList里面,時(shí)間復(fù)雜度O(1)

          最大連接數(shù)

          1024(x86)或2048(x64)

          無上限

          無上限

          fd拷貝

          每次調(diào)用select,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài)

          每次調(diào)用poll,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài)

          調(diào)用epoll_ctl時(shí)拷貝進(jìn)內(nèi)核并保存,之后每次epoll_wait不拷貝

          epoll是Linux目前大規(guī)模網(wǎng)絡(luò)并發(fā)程序開發(fā)的首選模型。在絕大多數(shù)情況下性能遠(yuǎn)超select和poll。目前流行的高性能web服務(wù)器Nginx正式依賴于epoll提供的高效網(wǎng)絡(luò)套接字輪詢服務(wù)。但是,在并發(fā)連接不高的情況下,多線程+阻塞I/O方式可能性能更好。


          主站蜘蛛池模板: A国产一区二区免费入口| 中文字幕AV一区中文字幕天堂| 鲁丝丝国产一区二区| 人妻无码视频一区二区三区| 视频一区二区中文字幕| 成人毛片一区二区| 精品国产福利在线观看一区| 一区二区三区精密机械| 亚洲日本久久一区二区va| 一区二区三区视频在线观看| 国产在线精品一区在线观看| 国产在线一区二区视频| 国产精品一区二区三区99| 手机福利视频一区二区 | 亚洲一区二区三区AV无码| 国产一区二区三区在线免费观看| 亚洲一区AV无码少妇电影| 精品福利一区二区三区免费视频| 国产成人AV一区二区三区无码 | 日韩中文字幕精品免费一区| 精品一区二区三区在线观看视频| 亚洲国产精品一区二区久久hs| 国产午夜精品一区二区| 精品福利一区二区三区| 国产美女口爆吞精一区二区| 爆乳熟妇一区二区三区霸乳| 亚洲国产精品成人一区| 香蕉免费看一区二区三区| 大屁股熟女一区二区三区| 果冻传媒一区二区天美传媒| 中文字幕乱码一区二区免费| 国产精品无码一区二区三区电影| 丝袜人妻一区二区三区| 亚洲国产成人一区二区三区 | 无码精品国产一区二区三区免费 | 亚洲一区中文字幕久久| 亚洲综合在线成人一区| 国产欧美一区二区精品仙草咪| 国产精品亚洲一区二区无码| 一区二区三区91| 国产精品伦一区二区三级视频|