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。
引入jQuery的方式有2種,一種是項(xiàng)目中直接引入jQuery的min.js文件,一種是使用服務(wù)器端jQuery文件(使用cdn)腳本標(biāo)簽方式引入。
在官網(wǎng)的:https://jquery.com/download/ 鏈接下可以下載到完整的代碼,放到項(xiàng)目文件的js文件夾下。
<script src="static/js/jquery-3.7.1.min.js"></script>
在網(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>
開發(fā)過程中一般使用非min.js文件方便調(diào)試,生產(chǎn)環(huán)境部署上線時(shí)才使用min.js這種壓縮文件。
從源碼中可以看出,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
});
從源碼中可以看出,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
從源碼中可以看出,將jQuery函數(shù)和window.$ 以及window.jQuery綁定賦值,所以使用jQuery和$ 標(biāo)識(shí)符就可以直接使用jQuery。通常在項(xiàng)目中直接使用$標(biāo)識(shí)符,快捷簡省。
所以在引入jQuery的項(xiàng)目中:
console.log(typeof $); // function
console.log($===jQuery); // true
console.log($() instanceof Object); // true
通常形式為:$(param)
$(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')
let list=[1, 2, 3]
$.each(list, function(i, ele) {
console.log(i, ele)
})
$.trim(' hello world ')
// 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ù)組。
// 基礎(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值
直接修改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');
獲取dom標(biāo)簽上的屬性:
$('.about-img-1>img').attr('src');
設(shè)置標(biāo)簽的屬性:
$('.about-img-1>img').attr('src', (data && data['image']) ? data['image'] : '');
點(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
});
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)求:
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模型,不了解的可以=> 查看這里
其中前面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ǔ)的概念:
我們先分析一下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()的機(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è)目的。
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在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);
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;
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)用程序效率。
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é)一下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方式可能性能更好。
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。