Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
elect、poll和epoll的區別
在linux沒有實現epoll事件驅動機制之前,我們一般選擇用select或者poll等IO多路復用的方法來實現并發服務程序。在大數據、高并發、集群等一些名詞唱的火熱之年代,select和poll的用武之地越來越有限了,風頭已經被epoll占盡。
select()和poll() IO多路復用模型
select的缺點:
單個進程能夠監視的文件描述符的數量存在最大限制,通常是1024,當然可以更改數量,但由于select采用輪詢的方式掃描文件描述符,文件描述符數量越多,性能越差;
內核/用戶空間內存拷貝問題,select需要復制大量的句柄數據結構,產生巨大的開銷
select返回的是含有整個句柄的數組,應用程序需要遍歷整個數組才能發現哪些句柄發生了事件;
select的觸發方式是水平觸發,應用程序如果沒有完成對一個已經就緒的文件描述符進行IO,那么之后再次select調用還是會將這些文件描述符通知進程。
相比于select模型,poll使用鏈表保存文件描述符,因此沒有了監視文件數量的限制,但其他三個缺點依然存在。
拿select模型為例,假設我們的服務器需要支持100萬的并發連接,則在_FD_SETSIZE為1024的情況下,則我們至少需要開辟1k個進程才能實現100萬的并發連接。除了進程間上下文切換的時間消耗外,從內核/用戶空間大量的無腦內存拷貝、數組輪詢等,是系統難以承受的。因此,基于select模型的服務器程序,要達到10萬級別的并發訪問,是一個很難完成的任務。
epoll IO多路復用模型實現機制
由于epoll的實現機制與select/poll機制完全不同,上面所說的select的缺點在epoll上不復存在。
設想一下如下場景:有100萬個客戶端同時與一個服務器進程保持著TCP連接。而每一時刻,通常只有幾百上千個TCP連接是活躍的。如何實現這樣的高并發?
在select/poll時代,服務器進程每次都把這100萬個連接告訴操作系統(從用戶態復制句柄數據結構到內核態),讓操作系統內核去查詢這些套接字上是否有事件發生,輪詢完后,再將句柄數據復制到用戶態,讓服務器應用程序輪詢處理已發生的網絡事件,這一過程資源消耗較大,因此,select/poll一般只能處理幾千的并發連接。
epoll的設計和實現select完全不同。epoll通過在linux內核中申請一個簡易的文件系統(文件系統一般用什么數據結構實現?B+樹)。把原先的select/poll調用分成了3個部分:
1)調用epoll_create()建立一個epoll對象(在epoll文件系統中為這個句柄對象分配資源)
2)調用epoll_ctl向epoll對象中添加這100萬個連接的套接字
3)調用epoll_wait收集發生的事件的連接
如此一來,要實現上面說的場景,只需要在進程啟動時建立一個epoll對象,然后在需要的時候向這個epoll對象中添加或者刪除連接。同時,epoll_wait的效率也非常高,因為調用epoll_wait時,并沒有一股腦的向操作系統復制這100萬個連接的句柄數據,內核也不需要去遍歷全部的連接。
上面的3個部分非常清晰,首先要調用epoll_create創建一個epoll對象。然后使用epoll_ctl可以操作上面建立的epoll對象,例如,將剛建立的socket加入到epoll中讓其監控,或者把epoll正在監控的某個socket句柄移出epoll,不再監控它等等。
epoll_wait在調用時,在給定的timeout時間內,當在監控的所有句柄中有事件發生時,就返回用戶態的進程。
從上面的調用方式就可以看到epoll比select/poll的優越之處:因為后者每次調用時都要傳遞你所要監控的所有socket給select/poll系統調用,這意味著需要將用戶態的socket列表copy到內核態,如果以萬計的句柄會導致每次都要copy幾十幾百KB的內存到內核態,非常低效。而我們調用epoll_wait時就相當于以往調用select/poll,但是這時卻不用傳遞socket句柄給內核,因為內核已經在epoll_ctl中拿到了要監控的句柄列表。
所以,實際上在你調用epoll_create后,內核就已經在內核態開始準備幫你存儲要監控的句柄了,每次調用epoll_ctl只是在往內核的數據結構里塞入新的socket句柄。
在內核里,一切皆文件。所以,epoll向內核注冊了一個文件系統,用于存儲上述的被監控socket。當你調用epoll_create時,就會在這個虛擬的epoll文件系統里創建一個file結點。當然這個file不是普通文件,它只服務于epoll。
epoll在被內核初始化時(操作系統啟動),同時會開辟出epoll自己的內核高速cache區,用于安置每一個我們想監控的socket,這些socket會以紅黑樹的形式保存在內核cache里,以支持快速的查找、插入、刪除。這個內核高速cache區,就是建立連續的物理內存頁,然后在之上建立slab層,簡單的說,就是物理上分配好你想要的size的內存對象,每次使用時都是使用空閑的已分配好的對象。
epoll的高效就在于,當我們調用epoll_ctl往里塞入百萬個句柄時,epoll_wait仍然可以飛快的返回,并有效的將發生事件的句柄給我們用戶。這是由于我們在調用epoll_create時,內核除了幫我們在epoll文件系統里建了個file結點,在內核cache里建了個紅黑樹用于存儲以后epoll_ctl傳來的socket外,還會再建立一個list鏈表,用于存儲準備就緒的事件,當epoll_wait調用時,僅僅觀察這個list鏈表里有沒有數據即可。有數據就返回,沒有數據就sleep,等到timeout時間到后即使鏈表沒數據也返回。所以,epoll_wait非常高效。
而且,通常情況下即使我們要監控百萬計的句柄,大多一次也只返回很少量的準備就緒句柄而已,所以,epoll_wait僅需要從內核態copy少量的句柄到用戶態而已,如何能不高效?!
那么,這個準備就緒list鏈表是怎么維護的呢?當我們執行epoll_ctl時,除了把socket放到epoll文件系統里file對象對應的紅黑樹上之外,還會給內核中斷處理程序注冊一個回調函數,告訴內核,如果這個句柄的中斷到了,就把它放到準備就緒list鏈表里。所以,當一個socket上有數據到了,內核在把網卡上的數據copy到內核中后就來把socket插入到準備就緒鏈表里了。
如此,一顆紅黑樹,一張準備就緒句柄鏈表,少量的內核cache,就幫我們解決了大并發下的socket處理問題。執行epoll_create時,創建了紅黑樹和就緒鏈表,執行epoll_ctl時,如果增加socket句柄,則檢查在紅黑樹中是否存在,存在立即返回,不存在則添加到樹干上,然后向內核注冊回調函數,用于當中斷事件來臨時向準備就緒鏈表中插入數據。執行epoll_wait時立刻返回準備就緒鏈表里的數據即可。
最后看看epoll獨有的兩種模式LT和ET。無論是LT和ET模式,都適用于以上所說的流程。區別是,LT模式下,只要一個句柄上的事件一次沒有處理完,會在以后調用epoll_wait時次次返回這個句柄,而ET模式僅在第一次返回。
這件事怎么做到的呢?當一個socket句柄上有事件時,內核會把該句柄插入上面所說的準備就緒list鏈表,這時我們調用epoll_wait,會把準備就緒的socket拷貝到用戶態內存,然后清空準備就緒list鏈表,最后,epoll_wait干了件事,就是檢查這些socket,如果不是ET模式(就是LT模式的句柄了),并且這些socket上確實有未處理的事件時,又把該句柄放回到剛剛清空的準備就緒鏈表了。所以,非ET的句柄,只要它上面還有事件,epoll_wait每次都會返回。而ET模式的句柄,除非有新中斷到,即使socket上的事件沒有處理完,也是不會次次從epoll_wait返回的。
其中涉及到的數據結構:
epoll用kmem_cache_create(slab分配器)分配內存用來存放structepitem和structeppoll_entry。
當向系統中添加一個fd時,就創建一個epitem結構體,這是內核管理epoll的基本數據結構:
structepitem{
structrb_noderbn;//用于主結構管理的紅黑樹
structlist_headrdllink;//事件就緒隊列
structepitem*next;//用于主結構體中的鏈表
structepoll_filefdffd;//這個結構體對應的被監聽的文件描述符信息
intnwait;//poll操作中事件的個數
structlist_headpwqlist;//雙向鏈表,保存著被監視文件的等待隊列,功能類似于select/poll中的poll_table
structeventpoll*ep;//該項屬于哪個主結構體(多個epitm從屬于一個eventpoll)
structlist_headfllink;//雙向鏈表,用來鏈接被監視的文件描述符對應的struct file。因為file里有f_ep_link,用來保存所有監視這個文件的epoll節點
structepoll_eventevent;//注冊的感興趣的事件,也就是用戶空間的epoll_event
}
而每個epoll fd(epfd)對應的主要數據結構為:
structeventpoll {
spin_lock_tlock;//對本數據結構的訪問
structmutex mtx;//防止使用時被刪除
wait_queue_head_t wq;//sys_epoll_wait()使用的等待隊列
wait_queue_head_tpoll_wait; //file->poll()使用的等待隊列
structlist_head rdllist;//事件滿足條件的鏈表 /*雙鏈表中則存放著將要通過epoll_wait返回給用戶的滿足條件的事件*/
structrb_rootrbr;//用于管理所有fd的紅黑樹(樹根)/*紅黑樹的根節點,這顆樹中存儲著所有添加到epoll中的需要監控的事件*/
structepitem*ovflist;//將事件到達的fd進行鏈接起來發送至用戶空間
}
structeventpoll在epoll_create時創建。
這樣說來,內核中維護了一棵紅黑樹,大致的結構如下:
當調用epoll_wait檢查是否有事件發生時,只需要檢查eventpoll對象中的rdlist雙鏈表中是否有epitem元素即可。如果rdlist不為空,則把發生的事件復制到用戶態,同時將事件數量返回給用戶。
jQuery官網:https://jquery.com/
jQuery是一個高效、輕量并且功能豐富的js庫。
核心在于查詢query。
jQuery是一個優秀的js函數庫,是React/Vue/Angular框架之外中大型項目的首選。
jQuery的主旨是write less, do more。
引入jQuery的方式有2種,一種是項目中直接引入jQuery的min.js文件,一種是使用服務器端jQuery文件(使用cdn)腳本標簽方式引入。
在官網的:https://jquery.com/download/ 鏈接下可以下載到完整的代碼,放到項目文件的js文件夾下。
<script src="static/js/jquery-3.7.1.min.js"></script>
在網站:https://www.bootcdn.cn/ 可以獲得穩定、快速、免費的cdn加速服務。
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.js"></script>
開發過程中一般使用非min.js文件方便調試,生產環境部署上線時才使用min.js這種壓縮文件。
從源碼中可以看出,jQuery的整體邏輯可以用以下簡單的結構進行描述:
( function( global, factory ) {
// 判斷有無window環境的一堆邏輯代碼
})( typeof window !=="undefined" ? window : this, function( window, noGlobal ) {
// 構造jQuery的一些邏輯代碼
return jQuery
});
從源碼中可以看出,jQuery被定義為一個函數,函數中返回了一個實例對象(看new關鍵字)。
繼續跟蹤源碼 new jQuery.fn.init( selector, context),這個函數中調用了makeArray,當然在其他if判斷語句中也有返回偽數組對象(比如,定義了length字段,還有[0]的操作),這里拿makeArray作為演示。
查看makeArray函數:
所以這個返回實例對象,是一個偽數組。
$('#menu-trigger') instanceof Array // false
$('#menu-trigger') instanceof Object // true
從源碼中可以看出,將jQuery函數和window.$ 以及window.jQuery綁定賦值,所以使用jQuery和$ 標識符就可以直接使用jQuery。通常在項目中直接使用$標識符,快捷簡省。
所以在引入jQuery的項目中:
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()
// 設置名為btn的class對應的dom標簽的文本內容
$('.btn').text('自定義文本內容')
通過$(param)傳入的是selector、element、標簽情況下,返回的是包含1個或者多個dom元素對象的偽數組。
// 基礎標簽和class
// 選擇了所有的div和span標簽
$('div, span')
// 選擇所有具有某個class的標簽
$('div.container')
// 層次選擇器
$('ul span') // ul標簽下的所有span元素
$('ul>span') // ul標簽下的所有子span元素
$('.container+li') // class為container的元素后的下一個li元素
$('ul .item~*') // class為item的元素后面所有兄弟元素
// 過濾選擇器
$('div:first') // 選擇第一個div
$('div:last') // 最后一個div
$('div:not(.container)') // class不為container的所有div
$('div:lt(3):gt(0)') // 所有div元素中的大于0小于3的div元素,表示1和2索引處的dom元素
$('div:containers("hello world")') // 內容為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表格的奇數行背景樣式設置
$('table>tbody>tr:odd')
// form表單中
$(':text') // 所有單行輸入框
$(':text:disabled') // 所有disabled的input輸入框
$(':checkbox') // 所有checkbox
$(':checkbox:checked') // 所有選中的checkbox
$('select').val() // select標簽選中的option的value值
直接修改css屬性(如果其dom標簽存在這個css屬性)
$('#container').css('background', 'red');
$('#container').css({ 'background' : 'red', 'color': 'blue' }) // 一組屬性
清空某標簽下的所有dom:
$('.carousel-inner').empty();
給某標簽下添加dom標簽:
$('.carousel-inner').append(domStr);
移除、添加class:
$('.carousel-indicators li').removeClass('active');
$('.carousel-indicators li:first').addClass('active');
獲取dom標簽上的屬性:
$('.about-img-1>img').attr('src');
設置標簽的屬性:
$('.about-img-1>img').attr('src', (data && data['image']) ? data['image'] : '');
點擊:
$('.category-product-page-ul>li').click(function(e) {
e.preventDefault();
console.log('this is:', this); // 打印對應的dom標簽
});
hover:
$('#container').hover(
function() {
// 當鼠標進入元素時執行的函數
},
function() {
// 當鼠標離開元素時執行的函數
}
);
監聽事件:
$('.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請求:
I/O多路復用(multiplexing)的本質是通過一種機制(系統內核緩沖I/O數據),讓單個進程可以監視多個文件描述符,一旦某個描述符就緒(一般是讀就緒或寫就緒),能夠通知程序進行相應的讀寫操作
select、poll 和 epoll 都是 Linux API 提供的 IO 復用方式。
相信大家都了解了Unix五種IO模型,不了解的可以=> 查看這里
其中前面4種IO都可以歸類為synchronous IO - 同步IO,而select、poll、epoll本質上也都是同步I/O,因為他們都需要在讀寫事件就緒后自己負責進行讀寫,也就是說這個讀寫過程是阻塞的。
與多進程和多線程技術相比,I/O多路復用技術的最大優勢是系統開銷小,系統不必創建進程/線程,也不必維護這些進程/線程,從而大大減小了系統的開銷。
在介紹select、poll、epoll之前,首先介紹一下Linux操作系統中基礎的概念:
我們先分析一下select函數
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);
【參數說明】int maxfdp1 指定待測試的文件描述字個數,它的值是待測試的最大描述字加1。fd_set *readset , fd_set *writeset , fd_set *exceptsetfd_set可以理解為一個集合,這個集合中存放的是文件描述符(file descriptor),即文件句柄。中間的三個參數指定我們要讓內核測試讀、寫和異常條件的文件描述符集合。如果對某一個的條件不感興趣,就可以把它設為空指針。const struct timeval *timeout timeout告知內核等待所指定文件描述符集合中的任何一個就緒可花多少時間。其timeval結構用于指定這段時間的秒數和微秒數。
【返回值】int 若有就緒描述符返回其數目,若超時則為0,若出錯則為-1
select()的機制中提供一種fd_set的數據結構,實際上是一個long類型的數組,每一個數組元素都能與一打開的文件句柄(不管是Socket句柄,還是其他文件或命名管道或設備句柄)建立聯系,建立聯系的工作由程序員完成,當調用select()時,由內核根據IO狀態修改fd_set的內容,由此來通知執行了select()的進程哪一Socket或文件可讀。
從流程上來看,使用select函數進行IO請求和同步阻塞模型沒有太大的區別,甚至還多了添加監視socket,以及調用select函數的額外操作,效率更差。但是,使用select以后最大的優勢是用戶可以在一個線程內同時處理多個socket的IO請求。用戶可以注冊多個socket,然后不斷地調用select讀取被激活的socket,即可達到在同一個線程內同時處理多個IO請求的目的。而在同步阻塞模型中,必須通過多線程的方式才能達到這個目的。
poll的機制與select類似,與select在本質上沒有多大差別,管理多個描述符也是進行輪詢,根據描述符的狀態進行處理,但是poll沒有最大文件描述符數量的限制。也就是說,poll只解決了上面的問題3,并沒有解決問題1,2的性能開銷問題。
下面是pll的函數原型:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
typedef struct pollfd {
int fd; // 需要被檢測或選擇的文件描述符
short events; // 對文件描述符fd上感興趣的事件
short revents; // 文件描述符fd上當前實際發生的事件
} pollfd_t;
poll改變了文件描述符集合的描述方式,使用了pollfd結構而不是select的fd_set結構,使得poll支持的文件描述符集合限制遠大于select的1024
【參數說明】struct pollfd *fds fds是一個struct pollfd類型的數組,用于存放需要檢測其狀態的socket描述符,并且調用poll函數之后fds數組不會被清空;一個pollfd結構體表示一個被監視的文件描述符,通過傳遞fds指示 poll() 監視多個文件描述符。其中,結構體的events域是監視該文件描述符的事件掩碼,由用戶來設置這個域,結構體的revents域是文件描述符的操作結果事件掩碼,內核在調用返回時設置這個域
nfds_t nfds 記錄數組fds中描述符的總數量
【返回值】int 函數返回fds集合中就緒的讀、寫,或出錯的描述符數量,返回0表示超時,返回-1表示出錯;
epoll在Linux2.6內核正式提出,是基于事件驅動的I/O方式,相對于select來說,epoll沒有描述符個數限制,使用一個文件描述符管理多個描述符,將用戶關心的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的copy只需一次。
Linux中提供的epoll相關函數如下:
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內核為處理大批量文件描述符而作了改進的poll,是Linux下多路復用IO接口select/poll的增強版本,它能顯著提高程序在大量并發連接中只有少量活躍的情況下的系統CPU利用率。原因就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被內核IO事件異步喚醒而加入Ready隊列的描述符集合就行了。
epoll除了提供select/poll那種IO事件的水平觸發(Level Triggered)外,還提供了邊緣觸發(Edge Triggered),這就使得用戶空間程序有可能緩存IO狀態,減少epoll_wait/epoll_pwait的調用,提高應用程序效率。
LT和ET原本應該是用于脈沖信號的,可能用它來解釋更加形象。Level和Edge指的就是觸發點,Level為只要處于水平,那么就一直觸發,而Edge則為上升沿和下降沿的時候觸發。比如:0->1 就是Edge,1->1 就是Level。
ET模式很大程度上減少了epoll事件的觸發次數,因此效率比LT模式下高。
一張圖總結一下select,poll,epoll的區別:
select | poll | epoll | |
操作方式 | 遍歷 | 遍歷 | 回調 |
底層實現 | 數組 | 鏈表 | 哈希表 |
IO效率 | 每次調用都進行線性遍歷,時間復雜度為O(n) | 每次調用都進行線性遍歷,時間復雜度為O(n) | 事件通知方式,每當fd就緒,系統注冊的回調函數就會被調用,將就緒fd放到readyList里面,時間復雜度O(1) |
最大連接數 | 1024(x86)或2048(x64) | 無上限 | 無上限 |
fd拷貝 | 每次調用select,都需要把fd集合從用戶態拷貝到內核態 | 每次調用poll,都需要把fd集合從用戶態拷貝到內核態 | 調用epoll_ctl時拷貝進內核并保存,之后每次epoll_wait不拷貝 |
epoll是Linux目前大規模網絡并發程序開發的首選模型。在絕大多數情況下性能遠超select和poll。目前流行的高性能web服務器Nginx正式依賴于epoll提供的高效網絡套接字輪詢服務。但是,在并發連接不高的情況下,多線程+阻塞I/O方式可能性能更好。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。