epoll和select
相比于select,epoll最大的好處在于它不會(huì)隨著監(jiān)聽(tīng)fd數(shù)目的增長(zhǎng)而降低效率。因?yàn)樵趦?nèi)核中的select實(shí)現(xiàn)中,它是采用輪詢來(lái)處理的,輪詢的fd數(shù)目越多,自然耗時(shí)越多。
并且,在linux/posix_types.h頭文件有這樣的聲明:
#define __FD_SETSIZE 1024
表示select最多同時(shí)監(jiān)聽(tīng)1024個(gè)fd,當(dāng)然,可以通過(guò)修改頭文件再重編譯內(nèi)核來(lái)擴(kuò)大這個(gè)數(shù)目,但這似乎并不治本。
一、IO多路復(fù)用的select
IO多路復(fù)用相對(duì)于阻塞式和非阻塞式的好處就是它可以監(jiān)聽(tīng)多個(gè) socket ,并且不會(huì)消耗過(guò)多資源。當(dāng)用戶進(jìn)程調(diào)用 select 時(shí),它會(huì)監(jiān)聽(tīng)其中所有 socket 直到有一個(gè)或多個(gè) socket 數(shù)據(jù)已經(jīng)準(zhǔn)備好,否則就一直處于阻塞狀態(tài)。select的缺點(diǎn)在于單個(gè)進(jìn)程能夠監(jiān)視的文件描述符的數(shù)量存在最大限制,select()所維護(hù)的存儲(chǔ)大量文件描述符的數(shù)據(jù)結(jié)構(gòu),隨著文件描述符數(shù)量的增大,其復(fù)制的的開(kāi)銷也線性增長(zhǎng)。同時(shí),由于網(wǎng)絡(luò)響應(yīng)時(shí)間的延遲使得大量的tcp鏈接處于非?;钴S狀態(tài),但調(diào)用select()會(huì)對(duì)所有的socket進(jìn)行一次線性掃描,所以這也浪費(fèi)了一定的開(kāi)銷。不過(guò)它的好處還有就是它的跨平臺(tái)特性。
二、 epoll
epoll的ET是必須對(duì)非阻塞的socket才能工作,LT對(duì)于阻塞的socket也可以
所有I/O多路復(fù)用操作都是同步的,涵蓋select/poll。
阻塞/非阻塞是相對(duì)于同步I/O來(lái)說(shuō)的,與異步I/O無(wú)關(guān)。
select/poll/epoll本身是同步的,可以阻塞也可以不阻塞。
(阻塞和非阻塞 與同步不同步不同;阻塞與否 是自身,異步與否是與外部協(xié)作的關(guān)系)
skater:
無(wú)論是阻塞 I/O、非阻塞 I/O,還是基于非阻塞 I/O 的多路復(fù)用都是同步調(diào)用。因?yàn)樗鼈冊(cè)?read 調(diào)用時(shí),內(nèi)核將數(shù)據(jù)從內(nèi)核空間拷貝到應(yīng)用程序空間(epoll應(yīng)該是從mmap),過(guò)程都是需要等待的,也就是說(shuō)這個(gè)過(guò)程是同步的,如果內(nèi)核實(shí)現(xiàn)的拷貝效率不高,read 調(diào)用就會(huì)在這個(gè)同步過(guò)程中等待比較長(zhǎng)的時(shí)間。
epoll事件: EPOLLIN : 表示對(duì)應(yīng)的文件描述符可以讀(包括對(duì)端SOCKET正常關(guān)閉); EPOLLOUT: 表示對(duì)應(yīng)的文件描述符可以寫(xiě); EPOLLPRI: 表示對(duì)應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來(lái)); EPOLLERR: 表示對(duì)應(yīng)的文件描述符發(fā)生錯(cuò)誤; EPOLLHUP: 表示對(duì)應(yīng)的文件描述符被掛斷;
epoll高效的核心是:1、用戶態(tài)和內(nèi)核太共享內(nèi)存mmap。2、數(shù)據(jù)到來(lái)采用事件通知機(jī)制(而不需要輪詢)。
epoll的接口
epoll的接口非常簡(jiǎn)單,一共就三個(gè)函數(shù):
(1)epoll_create系統(tǒng)調(diào)用
epoll_create在C庫(kù)中的原型如下。
int epoll_create(int size);
epoll_create返回一個(gè)句柄,之后 epoll的使用都將依靠這個(gè)句柄來(lái)標(biāo)識(shí)。參數(shù) size是告訴 epoll所要處理的大致事件數(shù)目。不再使用 epoll時(shí),必須調(diào)用 close關(guān)閉這個(gè)句柄。
注意:size參數(shù)只是告訴內(nèi)核這個(gè) epoll對(duì)象會(huì)處理的事件大致數(shù)目,而不是能夠處理的事件的最大個(gè)數(shù)。在 Linux最新的一些內(nèi)核版本的實(shí)現(xiàn)中,這個(gè) size參數(shù)沒(méi)有任何意義。
(2)epoll_ctl系統(tǒng)調(diào)用
epoll_ctl在C庫(kù)中的原型如下。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
epoll_ctl向 epoll對(duì)象中添加、修改或者刪除感興趣的事件,返回0表示成功,否則返回–1,此時(shí)需要根據(jù)errno錯(cuò)誤碼判斷錯(cuò)誤類型。epoll_wait方法返回的事件必然是通過(guò) epoll_ctl添加到 epoll中的。
參數(shù):
epfd: epoll_create返回的句柄,
op:的意義見(jiàn)下表:
EPOLL_CTL_ADD:注冊(cè)新的fd到epfd中;
EPOLL_CTL_MOD:修改已經(jīng)注冊(cè)的fd的監(jiān)聽(tīng)事件;
EPOLL_CTL_DEL:從epfd中刪除一個(gè)fd;
fd:需要監(jiān)聽(tīng)的socket句柄fd,
event:告訴內(nèi)核需要監(jiān)聽(tīng)什么事的結(jié)構(gòu)體,struct epoll_event結(jié)構(gòu)如下:
epoll_data_t; struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ };
__uint32_t events就要監(jiān)聽(tīng)的事件(感興趣的事件):
EPOLLIN :表示對(duì)應(yīng)的文件描述符可以讀(包括對(duì)端SOCKET正常關(guān)閉);
EPOLLOUT:表示對(duì)應(yīng)的文件描述符可以寫(xiě);
EPOLLPRI:表示對(duì)應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來(lái));
EPOLLERR:表示對(duì)應(yīng)的文件描述符發(fā)生錯(cuò)誤;
EPOLLHUP:表示對(duì)應(yīng)的文件描述符被掛斷;
EPOLLET: 將EPOLL設(shè)為邊緣觸發(fā)(Edge Triggered)模式,這是相對(duì)于水平觸發(fā)(Level Triggered)來(lái)說(shuō)的。
EPOLLONESHOT:只監(jiān)聽(tīng)一次事件,當(dāng)監(jiān)聽(tīng)完這次事件之后,如果還需要繼續(xù)監(jiān)聽(tīng)這個(gè)socket的話,需要再次把這個(gè)socket加入到EPOLL隊(duì)列里
data成員是一個(gè)epoll_data聯(lián)合,其定義如下: typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; 可見(jiàn),這個(gè) data成員還與具體的使用方式相關(guān)。例如,ngx_epoll_module模塊只使用了聯(lián)合中的 ptr成員, 作為指向 ngx_connection_t連接的指針。我們?cè)陧?xiàng)目中一般使用的也是 ptr成員,因?yàn)樗梢灾赶蛉我獾慕Y(jié)構(gòu) 體地址。
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
epoll_wait在C庫(kù)中的原型如下:
int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout);
收集在 epoll監(jiān)控的事件中已經(jīng)發(fā)生的事件,如果 epoll中沒(méi)有任何一個(gè)事件發(fā)生,則最多等待timeout毫秒后返回。epoll_wait的返回值表示當(dāng)前發(fā)生的事件個(gè)數(shù),如果返回0,則表示本次調(diào)用中沒(méi)有事件發(fā)生,如果返回–1,則表示出現(xiàn)錯(cuò)誤,需要檢查 errno錯(cuò)誤碼判斷錯(cuò)誤類型。
epfd:epoll的描述符。
events:分配好的 epoll_event結(jié)構(gòu)體數(shù)組,epoll將會(huì)把發(fā)生的事件復(fù)制到 events數(shù)組中(events不可以是空指針,內(nèi)核只負(fù)責(zé)把數(shù)據(jù)復(fù)制到這個(gè) events數(shù)組中,不會(huì)去幫助我們?cè)谟脩魬B(tài)中分配內(nèi)存。內(nèi)核這種做法效率很高)。
maxevents:表示本次可以返回的最大事件數(shù)目,通常 maxevents參數(shù)與預(yù)分配的events數(shù)組的大小是相等的。
timeout:表示在沒(méi)有檢測(cè)到事件發(fā)生時(shí)最多等待的時(shí)間(單位為毫秒),如果 timeout為0,則表示 epoll_wait在 rdllist鏈表中為空,立刻返回,不會(huì)等待。
epoll有兩種工作模式:LT(水平觸發(fā))模式和ET(邊緣觸發(fā))模式。
默認(rèn)情況下,epoll采用 LT模式工作,這時(shí)可以處理阻塞和非阻塞套接字,而上表中的 EPOLLET表示可以將一個(gè)事件改為 ET模式。ET模式的效率要比 LT模式高,它只支持非阻塞套接字。
(水平觸發(fā)LT:當(dāng)被監(jiān)控的文件描述符上有可讀寫(xiě)事件發(fā)生時(shí),epoll_wait()會(huì)通知處理程序去讀寫(xiě)。如果這次沒(méi)有把數(shù)據(jù)一次性全部讀寫(xiě)完(如讀寫(xiě)緩沖區(qū)太小),那么下次調(diào)用 epoll_wait()時(shí),它還會(huì)通知你在上次沒(méi)讀寫(xiě)完的文件描述符上繼續(xù)讀寫(xiě)
邊緣觸發(fā)ET:當(dāng)被監(jiān)控的文件描述符上有可讀寫(xiě)事件發(fā)生時(shí),epoll_wait()會(huì)通知處理程序去讀寫(xiě)。如果這次沒(méi)有把數(shù)據(jù)全部讀寫(xiě)完(如讀寫(xiě)緩沖區(qū)太小),那么下次調(diào)用epoll_wait()時(shí),它不會(huì)通知你,也就是它只會(huì)通知你一次,直到該文件描述符上出現(xiàn)第二次可讀寫(xiě)事件才會(huì)通知你
此可見(jiàn),水平觸發(fā)時(shí)如果系統(tǒng)中有大量你不需要讀寫(xiě)的就緒文件描述符,而它們每次都會(huì)返回,這樣會(huì)大大降低處理程序檢索自己關(guān)心的就緒文件描述符的效率,而邊緣觸發(fā),則不會(huì)充斥大量你不關(guān)心的就緒文件描述符,從而性能差異,高下立見(jiàn)。)
如何來(lái)使用epoll
1、包含一個(gè)頭文件#include
2、create_epoll(int maxfds)來(lái)創(chuàng)建一個(gè)epoll的句柄,其中maxfds為你epoll所支持的最大句柄數(shù)。這個(gè)函數(shù)會(huì)返回一個(gè)新的epoll句柄,之后的所有操作將通過(guò)這個(gè)句柄來(lái)進(jìn)行操作。在用完之后,記得用close()來(lái)關(guān)閉這個(gè)創(chuàng)建出來(lái)的epoll句柄。
3、之后在你的網(wǎng)絡(luò)主循環(huán)里面,每一幀的調(diào)用epoll_wait(int epfd, epoll_event events, int max events, int timeout)來(lái)查詢所有的網(wǎng)絡(luò)接口,看哪一個(gè)可以讀,哪一個(gè)可以寫(xiě)了。基本的語(yǔ)法為:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd為用epoll_create創(chuàng)建之后的句柄,events是一個(gè)epoll_event*的指針,當(dāng)epoll_wait這個(gè)函數(shù)操作成功之后,epoll_events里面將儲(chǔ)存所有的讀寫(xiě)事件。max_events是當(dāng)前需要監(jiān)聽(tīng)的所有socket句柄數(shù)。
最后一個(gè)timeout:是epoll_wait的超時(shí),
為0的時(shí)候表示馬上返回,
為-1的時(shí)候表示一直等下去,直到有事件返回,
為任意正整數(shù)的時(shí)候表示等這么長(zhǎng)的時(shí)間,如果一直沒(méi)有事件,則返回。
一般如果網(wǎng)絡(luò)主循環(huán)是單獨(dú)的線程的話,可以用-1來(lái)等,這樣可以保證一些效率,如果是和主邏輯在同一個(gè)線程的話,則可以用0來(lái)保證主循環(huán)的效率。
epoll_wait范圍之后應(yīng)該是一個(gè)循環(huán),遍利所有的事件。
epoll通過(guò)在Linux內(nèi)核中申請(qǐng)一個(gè)簡(jiǎn)易的文件系統(tǒng)(文件系統(tǒng)一般用什么數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)?B+樹(shù))。把原先的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萬(wàn)個(gè)連接的套接字
3)調(diào)用epoll_wait收集發(fā)生的事件的連接
epoll程序框架
幾乎所有的epoll程序都使用下面的框架:
偽代碼:
listenfd為全局變量,服務(wù)端監(jiān)聽(tīng)的套接字的fd。
關(guān)于epoll_wait返回值的一個(gè)簡(jiǎn)單測(cè)試 void test(int epollfd) { struct epoll_event events[MAX_EVENT_NUMBER]; int number; while (1) { number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1); printf("number : %2d ", number); for (i = 0; i < number; i++) { sockfd = events[i].data.fd; if (sockfd == listenfd) {/*用戶上線*/ } else if (events[i].events & EPOLLIN) {/*有數(shù)據(jù)可讀*/ } else if (events[i].events & EPOLLOUT) {/*有數(shù)據(jù)可寫(xiě)*/ } else {/*出錯(cuò)*/ } } } } 通過(guò)測(cè)試發(fā)現(xiàn)epoll_wait返回值number是不會(huì)大于MAX_EVENT_NUMBER的。 測(cè)試過(guò)程中,連接的客戶端數(shù)遠(yuǎn)大于MAX_EVENT_NUMBER,由此可以推論:epoll_wait()每次返回的是活躍客戶端的個(gè)數(shù),每次并將這些活躍的客戶端信息加入到events[MAX_EVENT_NUMBER]。 由此可見(jiàn),活躍客戶端的個(gè)數(shù)相同的情況下,events[MAX_EVENT_NUMBER]越大,epoll_wait()函數(shù)執(zhí)行次數(shù)越少,但是events[MAX_EVENT_NUMBER]越大越消耗存儲(chǔ)資源。 所以,MAX_EVENT_NUMBER的選擇應(yīng)該在效率和資源間取一個(gè)平衡點(diǎn)。
示例代碼
for( ; ; ) { nfds = epoll_wait(epfd,events,20,500); for(i=0;ifd; send( sockfd, md->ptr, strlen((char*)md->ptr), 0 ); //發(fā)送數(shù)據(jù) ev.data.fd=sockfd; ev.events=EPOLLIN|EPOLLET; epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改標(biāo)識(shí)符,等待下一個(gè)循環(huán)時(shí)接收數(shù)據(jù) } else { //其他的處理 } } }
大致流程
struct epoll_event ev, event_list[EVENT_MAX_COUNT];//ev用于注冊(cè)事件,event_list用于回傳要處理的事件 listenfd = socket(AF_INET, SOCK_STREAM, 0); if(0 != bind(listenfd, (struct sockaddr *) if(0 != listen(listenfd, LISTENQ)) //LISTENQ 定義了宏//#define LISTENQ 20 ev.data.fd = listenfd; //設(shè)置與要處理的事件相關(guān)的文件描述符 ev.events = EPOLLIN | EPOLLET; //設(shè)置要處理的事件類型EPOLLIN :表示對(duì)應(yīng)的文件描述符可以讀,EPOLLET狀態(tài)變化才通知 epfd = epoll_create(256); //生成用于處理accept的epoll專用的文件描述符 //注冊(cè)epoll事件 epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); //epfd epoll實(shí)例ID,EPOLL_CTL_ADD添加,listenfd:socket,ev事件(監(jiān)聽(tīng)listenfd) nfds = epoll_wait(epfd, event_list, EVENT_MAX_COUNT, TIMEOUT_MS); //等待epoll事件的發(fā)生
1. 首先熟悉下epoll的三個(gè)接口
int epoll_create(int size);
創(chuàng)建epoll相關(guān)數(shù)據(jù)結(jié)構(gòu),其最重要的是
1. 紅黑樹(shù), 用于存儲(chǔ)需要監(jiān)控的文件句柄以及事件
2. 就緒鏈表,用于存儲(chǔ)被觸發(fā)的文件句柄以及事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
用于設(shè)定,修改,或者刪除 監(jiān)控的文件句柄以及事件
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
阻塞等待timeout時(shí)間,如果文件句柄上相關(guān)事件被觸發(fā),則epoll_wait退出,并將觸發(fā)的事件 寫(xiě)入出參 events參數(shù),觸發(fā)的事件個(gè)數(shù)作為返回值返回
2. 如何使用這三個(gè)接口寫(xiě)一個(gè)server
首先使用epoll_create 創(chuàng)建epoll相關(guān)數(shù)據(jù)結(jié)構(gòu)
其次創(chuàng)建TCP socket 文件句柄acceptfd,綁定(ip:port),然后開(kāi)啟監(jiān)聽(tīng),并使用epoll_ctl 注冊(cè)到epoll中,監(jiān)聽(tīng)acceptfd句柄的EPOLL_IN事件(即可讀事件)
調(diào)用epoll_wait 開(kāi)始進(jìn)行阻塞等待
如果有客戶端連接過(guò)來(lái),則觸發(fā)acceptfd上的EPOLL_IN事件,epoll_wait返回后,可以得到觸發(fā)事件的信息, 這些信息其實(shí)就是一個(gè)struct epoll_event對(duì)象, 我們可以判斷這個(gè)epoll event對(duì)象fd是否和acceptfd一致,如果一致在認(rèn)為有新連接進(jìn)來(lái),則獲得新連接對(duì)應(yīng)的clientfd, 并使用epoll_ctl注冊(cè)到epoll, 監(jiān)控clientfd上的epoll_in事件,這個(gè)時(shí)候這個(gè)客戶端和服務(wù)器的連接就建立了
typedef union epoll_data { void *ptr; int fd; //可以用fd, 也可以用ptr來(lái)保存事件對(duì)應(yīng)的文件句柄 uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ };
用戶在客戶端輸入命令,將會(huì)觸發(fā)服務(wù)器端 clientfd上的epoll_in事件,epoll_wait返回觸發(fā)的event, 讀取event對(duì)應(yīng)的clientfd內(nèi)核緩沖區(qū)中的數(shù)據(jù),解析協(xié)議,執(zhí)行命令,得到返回結(jié)果,這個(gè)返回結(jié)果要返回給客戶端,則再使用epoll_ctl注冊(cè)clientfd的epoll_out事件到epoll,這個(gè)時(shí)候,我們會(huì)注意到clientfd上既有epoll_in,也有epoll_out,這樣其實(shí)沒(méi)有必要,客戶端在這個(gè)時(shí)候等待返回結(jié)果,不會(huì)再輸入命令,所以需要使用epoll_ctl把epoll_out刪除掉
如果clientfd內(nèi)核緩沖區(qū)可寫(xiě),epoll_wait這個(gè)時(shí)候會(huì)返回,并返回epoll_out事件,此時(shí)把返回的結(jié)果數(shù)據(jù)寫(xiě)入clientfd, 返回給客戶端
實(shí)例源碼
原文有相當(dāng)多的如錯(cuò)誤:
需要增加到頭文件和錯(cuò)誤修改
//'/0'->'' //bzero() 替換為memset (注意二者參數(shù)不一樣,bzero將前n個(gè)字節(jié)設(shè)為0,memset將前n 個(gè)字節(jié)的值設(shè)為值 c) //local_addr 由char* 改為 string #include#include //atoi #include //memset #include //std:cout 等
修正后的源碼C++ lnux:
#include#include #include #include #include #include #include #include #include //'/0'->'' //bzero() 替換為memset (注意二者參數(shù)不一樣,bzero將前n個(gè)字節(jié)設(shè)為0,memset將前n 個(gè)字節(jié)的值設(shè)為值 c) //local_addr 由char* 改為 string #include #include //atoi #include //memset #include //std:cout 等 using namespace std; #define MAXLINE 255 //讀寫(xiě)緩沖 #define OPEN_MAX 100 #define LISTENQ 20 //listen的第二個(gè)參數(shù) 定義TCP鏈接未完成隊(duì)列的大?。╨inux >2.6 則表示accpet之前的隊(duì)列) #define SERV_PORT 5000 #define INFTIM 1000 #define TIMEOUT_MS 500 #define EVENT_MAX_COUNT 20 void setnonblocking(int sock) { int opts; opts = fcntl(sock, F_GETFL); if(opts < 0) { perror("fcntl(sock,GETFL)"); exit(1); } opts = opts | O_NONBLOCK; if(fcntl(sock, F_SETFL, opts) < 0) { perror("fcntl(sock,SETFL,opts)"); exit(1); } } int main(int argc, char *argv[]) { int i, maxi, listenfd, connfd, sockfd, epfd, nfds, portnumber; ssize_t n; char line_buff[MAXLINE]; if ( 2 == argc ) { if( (portnumber = atoi(argv[1])) < 0 ) { fprintf(stderr, "Usage:%s portnumber/r/n", argv[0]); //fprintf()函數(shù)根據(jù)指定的format(格式)(格式)發(fā)送信息(參數(shù))到由stream(流)指定的文件 //printf 將內(nèi)容發(fā)送到Default的輸出設(shè)備,通常為本機(jī)的顯示器,fprintf需要指定輸出設(shè)備,可以為文件,設(shè)備。 //stderr return 1; } } else { fprintf(stderr, "Usage:%s portnumber/r/n", argv[0]); return 1; } //聲明epoll_event結(jié)構(gòu)體的變量,ev用于注冊(cè)事件,數(shù)組用于回傳要處理的事件 struct epoll_event ev, event_list[EVENT_MAX_COUNT]; //生成用于處理accept的epoll專用的文件描述符 epfd = epoll_create(256); //生成epoll文件描述符,既在內(nèi)核申請(qǐng)一空間,存放關(guān)注的socket fd上是否發(fā)生以及發(fā)生事件。size既epoll fd上能關(guān)注的最大socket fd數(shù)。隨你定好了。只要你有空間。 struct sockaddr_in clientaddr; socklen_t clilenaddrLen; struct sockaddr_in serveraddr; listenfd = socket(AF_INET, SOCK_STREAM, 0);//Unix/Linux“一切皆文件”,創(chuàng)建(套接字)文件,id=listenfd if (listenfd < 0) { printf("socket error,errno %d:%s ",errno,strerror(errno)); } //把socket設(shè)置為非阻塞方式 //setnonblocking(listenfd); //設(shè)置與要處理的事件相關(guān)的文件描述符 ev.data.fd = listenfd; //設(shè)置要處理的事件類型 ev.events = EPOLLIN | EPOLLET; //EPOLLIN :表示對(duì)應(yīng)的文件描述符可以讀,EPOLLET狀態(tài)變化才通知 //ev.events=EPOLLIN; //注冊(cè)epoll事件 epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); //epfd epoll實(shí)例ID,EPOLL_CTL_ADD添加,listenfd:socket,ev事件(監(jiān)聽(tīng)listenfd) memset(&serveraddr, 0, sizeof(serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_addr.s_addr=htonl(INADDR_ANY); /*IP,INADDR_ANY轉(zhuǎn)換過(guò)來(lái)就是0.0.0.0,泛指本機(jī)的意思,也就是表示本機(jī)的所有IP*/ serveraddr.sin_port = htons(portnumber); if(0 != bind(listenfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr))) { printf("bind error,errno %d:%s ",errno,strerror(errno)); } if(0 != listen(listenfd, LISTENQ)) //LISTENQ 定義了宏 { printf("listen error,errno %d:%s ",errno,strerror(errno)); } maxi = 0; for ( ; ; ) { //等待epoll事件的發(fā)生 nfds = epoll_wait(epfd, event_list, EVENT_MAX_COUNT, TIMEOUT_MS); //epoll_wait(int epfd, struct epoll_event * event_list, int maxevents, int timeout),返回需要處理的事件數(shù)目 //處理所發(fā)生的所有事件 for(i = 0; i < nfds; ++i) { if(event_list[i].data.fd == listenfd) //如果新監(jiān)測(cè)到一個(gè)SOCKET用戶連接到了綁定的SOCKET端口,建立新的連接。 { clilenaddrLen = sizeof(struct sockaddr_in);//在調(diào)用accept()前,要給addrLen賦值,這樣才不會(huì)出錯(cuò),addrLen = sizeof(clientaddr);或addrLen = sizeof(struct sockaddr_in); connfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clilenaddrLen);//(accpet詳解:https://blog.csdn.net/David_xtd/article/details/7087843) if(connfd < 0) { //perror("connfd<0:connfd= %d",connfd); printf("connfd<0,accept error,errno %d:%s ",errno,strerror(errno)); exit(1); } //setnonblocking(connfd); char *str = inet_ntoa(clientaddr.sin_addr);//將一個(gè)32位網(wǎng)絡(luò)字節(jié)序的二進(jìn)制IP地址轉(zhuǎn)換成相應(yīng)的點(diǎn)分十進(jìn)制的IP地址 cout << "accapt a connection from " << str << endl; //設(shè)置用于讀操作的文件描述符 ev.data.fd = connfd; //設(shè)置用于注測(cè)的讀操作事件 ev.events = EPOLLIN | EPOLLET; //ev.events=EPOLLIN; //注冊(cè)ev epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev); //將accpet的句柄添加進(jìn)入(增加監(jiān)聽(tīng)的對(duì)象) } else if(event_list[i].events & EPOLLIN) //如果是已經(jīng)連接的用戶,并且收到數(shù)據(jù),那么進(jìn)行讀入。 { cout << "EPOLLIN" << endl; if ( (sockfd = event_list[i].data.fd) < 0) continue; if ( (n = read(sockfd, line_buff, MAXLINE)) < 0) //read時(shí)fd中的數(shù)據(jù)如果小于要讀取的數(shù)據(jù),就會(huì)引起阻塞? { //當(dāng)read()或者write()返回-1時(shí),一般要判斷errno if (errno == ECONNRESET)//與客戶端的Socket被客戶端強(qiáng)行被斷開(kāi),而服務(wù)器還企圖read { close(sockfd); event_list[i].data.fd = -1; } else std::cout << "readline error" << std::endl; } else if (n == 0) //返回的n為0時(shí),說(shuō)明客戶端已經(jīng)關(guān)閉 { close(sockfd); event_list[i].data.fd = -1; } line_buff[n] = ''; cout << "read " << line_buff << endl; //設(shè)置用于寫(xiě)操作的文件描述符 ev.data.fd = sockfd; //設(shè)置用于注測(cè)的寫(xiě)操作事件 ev.events = EPOLLOUT | EPOLLET; //EPOLLOUT:表示對(duì)應(yīng)的文件描述符可以寫(xiě); //修改sockfd上要處理的事件為EPOLLOUT //epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); } else if(event_list[i].events & EPOLLOUT) // 如果有數(shù)據(jù)發(fā)送 { sockfd = event_list[i].data.fd; write(sockfd, line_buff, n); //設(shè)置用于讀操作的文件描述符 ev.data.fd = sockfd; //設(shè)置用于注測(cè)的讀操作事件 ev.events = EPOLLIN | EPOLLET; //修改sockfd上要處理的事件為EPOLIN epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev); } } } return 0; }
編譯命令
linux下編譯:g++ epoll.cpp -o epoll
命令行簡(jiǎn)單測(cè)試
curl 192.168.0.250:5000 -d "phone=123456789&name=Hwei"
相關(guān)知識(shí)
如何動(dòng)態(tài)的改變listen監(jiān)聽(tīng)的個(gè)數(shù)呢?
如果指定值在源代碼中是一個(gè)常值,那么增長(zhǎng)其大小需要重新編譯服務(wù)器程序。那么,我們可以為它設(shè)定一個(gè)缺省值,不過(guò)允許通過(guò)命令行選項(xiàng)或者環(huán)境變量來(lái)覆寫(xiě)該值。
void Listen(int fd, int backlog) { char *ptr; if((ptr = getenv("LISTENQ")) != NULL) backlog = atoi(ptr); if(listen(fd, backlog) < 0) printf("listen error "); }
隊(duì)列已滿的情況,如何處理?
當(dāng)一個(gè)客戶SYN到達(dá)時(shí),若這個(gè)隊(duì)列是滿的,TCP就忽略該分節(jié),也就是不會(huì)發(fā)送RST。
這么做的原因在于,隊(duì)列已滿的情況是暫時(shí)的,客戶TCP如果沒(méi)收收到RST,就會(huì)重發(fā)SYN,在隊(duì)列有空閑的時(shí)候處理該請(qǐng)求。如果服務(wù)器TCP立即響應(yīng)一個(gè)RST,客戶的connect調(diào)用就會(huì)立即返回一個(gè)錯(cuò)誤,強(qiáng)制應(yīng)用進(jìn)程處理這種情況,而不會(huì)再次重發(fā)SYN。而且客戶端也不無(wú)區(qū)別該套接口的狀態(tài),是“隊(duì)列已滿”還是“該端口沒(méi)有在監(jiān)聽(tīng)”。
SYN泛濫攻擊
向某一目標(biāo)服務(wù)器發(fā)送大量的SYN,用以填滿一個(gè)或多個(gè)TCP端口的未完成隊(duì)列。每個(gè)SYN的源IP地址都置成隨機(jī)數(shù)(IP欺騙),這樣防止攻擊服務(wù)器獲悉黑客的真實(shí)IP地址。通過(guò)偽造的SYN裝滿未完成連接隊(duì)列,使得合法的SYN不能排上隊(duì),導(dǎo)致針對(duì)合法用戶的服務(wù)被拒絕。
防御方法:
針對(duì)服務(wù)器主機(jī)的方法。增加連接緩沖隊(duì)列長(zhǎng)度和縮短連接請(qǐng)求占用緩沖隊(duì)列的超時(shí)時(shí)間。該方式最簡(jiǎn)單,被很多操作系統(tǒng)采用,但防御性能也最弱。
針對(duì)路由器過(guò)濾的方法。由于DDoS攻擊,包括SYN-Flood,都使用地址偽裝技術(shù),所以在路由器上使用規(guī)則過(guò)濾掉被認(rèn)為地址偽裝的包,會(huì)有效的遏制攻擊流量。
針對(duì)防火墻的方法。在SYN請(qǐng)求連接到真正的服務(wù)器之前,使用基于防火墻的網(wǎng)關(guān)來(lái)測(cè)試其合法性。它是一種被普遍采用的專門針對(duì)SYN-Flood攻擊的防御機(jī)制。
SYN:同步序列編號(hào)(Synchronize Sequence Numbers)
它們的含義是: SYN表示建立連接, FIN表示關(guān)閉連接, ACK表示響應(yīng), PSH表示有 DATA數(shù)據(jù)傳輸, RST表示連接重置。
SYN(synchronous建立聯(lián)機(jī))
ACK(acknowledgement 確認(rèn))
PSH(push傳送)
FIN(finish結(jié)束)
RST(reset重置)
URG(urgent緊急)
Sequence number(順序號(hào)碼)
Acknowledge number(確認(rèn)號(hào)碼)
三次握手:
在TCP/IP協(xié)議中,TCP協(xié)議提供可靠的連接服務(wù),采用三次握手建立一個(gè)連接。 第一次握手:建立連接時(shí),客戶端發(fā)送syn包(syn=j)到服務(wù)器,并進(jìn)入SYN_SEND狀態(tài),等待服務(wù)器確認(rèn); 第二次握手:服務(wù)器收到syn包,必須確認(rèn)客戶的SYN(ack=j+1),同時(shí)自己也發(fā)送一個(gè)SYN包(syn=k),即SYN+ACK包,此時(shí)服務(wù)器進(jìn)入SYN_RECV狀態(tài); 第三次握手:客戶端收到服務(wù)器的SYN+ACK包,向服務(wù)器發(fā)送確認(rèn)包ACK(ack=k+1),此包發(fā)送完畢,客戶端和服務(wù)器進(jìn)入ESTABLISHED狀態(tài),完成三次握手。完成三次握手,客戶端與服務(wù)器開(kāi)始傳送數(shù)據(jù).
第一次握手:主機(jī)A發(fā)送位碼為syn=1,隨機(jī)產(chǎn)生seq number=1234567的數(shù)據(jù)包到服務(wù)器,主機(jī)B由SYN=1知道,A要求建立聯(lián)機(jī);
第二次握手:主機(jī)B收到請(qǐng)求后要確認(rèn)聯(lián)機(jī)信息,向A發(fā)送ack number=(主機(jī)A的seq+1),syn=1,ack=1,隨機(jī)產(chǎn)生seq=7654321的包;
第三次握手:主機(jī)A收到后檢查ack number是否正確,即第一次發(fā)送的seq number+1,以及位碼ack是否為1,若正確,主機(jī)A會(huì)再發(fā)送ack number=(主機(jī)B的seq+1),ack=1,主機(jī)B收到后確認(rèn)seq值與ack=1則連接建立成功。
實(shí)例代碼二
多進(jìn)程Epoll:
#include#include #include #include #include #include #include #include #include #include #include #define PROCESS_NUM 10 static int create_and_bind (char *port) { int fd = socket(PF_INET, SOCK_STREAM, 0); struct sockaddr_in serveraddr; serveraddr.sin_family = AF_INET; serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); serveraddr.sin_port = htons(atoi(port)); bind(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)); return fd; } static int make_socket_non_blocking (int sfd) { int flags, s; flags = fcntl (sfd, F_GETFL, 0); if (flags == -1) { perror ("fcntl"); return -1; } flags |= O_NONBLOCK; s = fcntl (sfd, F_SETFL, flags); if (s == -1) { perror ("fcntl"); return -1; } return 0; } #define MAXEVENTS 64 int main (int argc, char *argv[]) { int sfd, s; int efd; struct epoll_event event; struct epoll_event *events; sfd = create_and_bind("1234"); if (sfd == -1) abort (); s = make_socket_non_blocking (sfd); if (s == -1) abort (); s = listen(sfd, SOMAXCONN); if (s == -1) { perror ("listen"); abort (); } efd = epoll_create(MAXEVENTS); if (efd == -1) { perror("epoll_create"); abort(); } event.data.fd = sfd; //event.events = EPOLLIN | EPOLLET; event.events = EPOLLIN; s = epoll_ctl(efd, EPOLL_CTL_ADD, sfd, &event); if (s == -1) { perror("epoll_ctl"); abort(); } /* Buffer where events are returned */ events = calloc(MAXEVENTS, sizeof event); int k; for(k = 0; k < PROCESS_NUM; k++) { int pid = fork(); if(pid == 0) { /* The event loop */ while (1) { int n, i; n = epoll_wait(efd, events, MAXEVENTS, -1); printf("process %d return from epoll_wait! ", getpid()); /* sleep here is very important!*/ //sleep(2); for (i = 0; i < n; i++) { if ((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP) || (!(events[i].events & EPOLLIN))) { /* An error has occured on this fd, or the socket is not ready for reading (why were we notified then?) */ fprintf (stderr, "epoll error "); close (events[i].data.fd); continue; } else if (sfd == events[i].data.fd) { /* We have a notification on the listening socket, which means one or more incoming connections. */ struct sockaddr in_addr; socklen_t in_len; int infd; char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV]; in_len = sizeof in_addr; infd = accept(sfd, &in_addr, &in_len); if (infd == -1) { printf("process %d accept failed! ", getpid()); break; } printf("process %d accept successed! ", getpid()); /* Make the incoming socket non-blocking and add it to the list of fds to monitor. */ close(infd); } } } } } int status; wait(&status); free (events); close (sfd); return EXIT_SUCCESS; }
建立2000+個(gè)鏈接的測(cè)試代碼
#include#include #include #include #include #include #include #include #include #include const int MAXLINE = 5; int count = 1; static int make_socket_non_blocking(int fd) { int flags, s; flags = fcntl (fd, F_GETFL, 0); if (flags == -1) { perror ("fcntl"); return -1; } flags |= O_NONBLOCK; s = fcntl (fd, F_SETFL, flags); if (s == -1) { perror ("fcntl"); return -1; } return 0; } void sockconn() { int sockfd; struct sockaddr_in server_addr; struct hostent *host; char buf[100]; unsigned int value = 1; host = gethostbyname("127.0.0.1"); sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { perror("socket error "); return; } //setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value)); //make_socket_non_blocking(sockfd); bzero(&server_addr, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(8080); server_addr.sin_addr = *((struct in_addr*) host->h_addr); int cn = connect(sockfd, (struct sockaddr *) &server_addr, sizeof(server_addr)); if (cn == -1) { printf("connect error errno=%d ", errno); return; } //char *buf = "h"; sprintf(buf, "%d", count); count++; write(sockfd, buf, strlen(buf)); close(sockfd); printf("client send %s ", buf); return; } int main(void) { int i; for (i = 0; i < 2000; i++) { sockconn(); } return 0; }
關(guān)于ET、LT兩種工作模式
水平觸發(fā)LT:
其中LT就是與select和poll類似,當(dāng)被監(jiān)控的文件描述符上有可讀寫(xiě)事件發(fā)生時(shí),epoll_wait()會(huì)通知處理程序去讀寫(xiě)。如果這次沒(méi)有把數(shù)據(jù)一次性全部讀寫(xiě)完(如讀寫(xiě)緩沖區(qū)太小),那么下次調(diào)用 epoll_wait()時(shí),它還會(huì)通知你在上次沒(méi)讀寫(xiě)完的文件描述符上繼續(xù)讀寫(xiě)
邊緣觸發(fā)ET:
當(dāng)被監(jiān)控的文件描述符上有可讀寫(xiě)事件發(fā)生時(shí),epoll_wait()會(huì)通知處理程序去讀寫(xiě)。如果這次沒(méi)有把數(shù)據(jù)全部讀寫(xiě)完(如讀寫(xiě)緩沖區(qū)太小),那么下次調(diào)用epoll_wait()時(shí),它不會(huì)通知你,也就是它只會(huì)通知你一次,直到該文件描述符上出現(xiàn)第二次可讀寫(xiě)事件才會(huì)通知你
水平觸發(fā):只要緩沖區(qū)有數(shù)據(jù)就會(huì)一直觸發(fā)
邊沿觸發(fā):只有在緩沖區(qū)增加數(shù)據(jù)的那一刻才會(huì)觸發(fā)
由此可見(jiàn),水平觸發(fā)時(shí)如果系統(tǒng)中有大量你不需要讀寫(xiě)的就緒文件描述符(有些fd有數(shù)據(jù),但是你不處理那些fd),而它們每次都會(huì)返回,這樣會(huì)大大降低處理程序檢索自己關(guān)心的就緒文件描述符的效率,而邊緣觸發(fā),則不會(huì)充斥大量你不關(guān)心的就緒文件描述符,從而性能差異,高下立見(jiàn)。
4、關(guān)于ET、LT兩種工作模式
可以得出這樣的結(jié)論:
ET模式僅當(dāng)狀態(tài)發(fā)生變化的時(shí)候才獲得通知,這里所謂的狀態(tài)的變化并不包括緩沖區(qū)中還有未處理的數(shù)據(jù),也就是說(shuō),如果要采用ET模式,需要一直read/write直到出錯(cuò)為止,很多人反映為什么采用ET模式只接收了一部分?jǐn)?shù)據(jù)就再也得不到通知了,大多因?yàn)檫@樣;而LT模式是只要有數(shù)據(jù)沒(méi)有處理就會(huì)一直通知下去的.
epoll中讀寫(xiě)數(shù)據(jù) 的注意事項(xiàng)
在一個(gè)非阻塞的socket上調(diào)用read/write函數(shù),返回EAGAIN或者EWOULDBLOCK(注:EAGAIN就是EWOULDBLOCK)。
從字面上看,意思是:
EAGAIN: 再試一次
EWOULDBLOCK:如果這是一個(gè)阻塞socket, 操作將被block
perror輸出:Resource temporarily unavailable
總結(jié):
這個(gè)錯(cuò)誤表示資源暫時(shí)不夠,可能read時(shí), 讀緩沖區(qū)沒(méi)有數(shù)據(jù), 或者write時(shí),寫(xiě)緩沖區(qū)滿了。
遇到這種情況,如果是阻塞socket、 read/write就要阻塞掉。而如果是非阻塞socket、 read/write立即返回-1, 同 時(shí)errno設(shè)置為EAGAIN。
所以對(duì)于阻塞socket、 read/write返回-1代表網(wǎng)絡(luò)出錯(cuò)了。但對(duì)于非阻塞socket、read/write返回-1不一定網(wǎng)絡(luò)真的出錯(cuò)了??赡苁荝esource temporarily unavailable。這時(shí)你應(yīng)該再試,直到Resource available。
本文主要講述epoll模型(不完全是針對(duì)epoll)下讀寫(xiě)數(shù)據(jù)接口使用的注意事項(xiàng)
1、read write
函數(shù)原型如下:
#includessize_t read(int filedes, void* buf, size_t nbytes) ssize_t write(int filedes, const void* buf, size_t nbytes)
其中,read返回實(shí)際讀取到的字節(jié)數(shù)。但實(shí)際讀取的字節(jié)很有可能少于指定要讀取的字節(jié)數(shù)nbytes。因此會(huì)分為:
①返回值大于0。 讀取正常,返回實(shí)際讀取到的字節(jié)數(shù)
②返回值等于0。 讀取異常,讀取到文件filedes結(jié)尾處了。這里邏輯上要理解為read已經(jīng)讀取完數(shù)據(jù)
③返回值小于0(-1)。 讀取出錯(cuò),在處理網(wǎng)絡(luò)請(qǐng)求時(shí)可能是網(wǎng)絡(luò)異常。著重注意當(dāng)返回-1,此時(shí)errno的值EAGAIN、EWOULLDBLOCK,表示內(nèi)核對(duì)應(yīng)的讀緩沖區(qū)為空
而write返回的實(shí)際寫(xiě)入字節(jié)數(shù)正常情況是與制定寫(xiě)入的字節(jié)數(shù)nbytes相同的,不相等說(shuō)明寫(xiě)入異常了,著重注意,此時(shí)errno的值EAGAIN、EWOULLDBLOCK,表示內(nèi)核對(duì)應(yīng)的寫(xiě)緩沖區(qū)為空。注,EAGAIN等同于EWOULLDBLOCK。
總之,這個(gè)錯(cuò)誤表示資源暫時(shí)不夠,可能read時(shí)讀緩沖區(qū)沒(méi)有數(shù)據(jù), 或者write時(shí)寫(xiě)緩沖區(qū)滿了。遇到這種情況,如果是阻塞socket、 read/write就要阻塞掉。而如果是非阻塞socket、 read/write立即返回-1, 同時(shí)errno設(shè)置為EAGAIN。
所以對(duì)于阻塞socket、 read/write返回-1代表網(wǎng)絡(luò)出錯(cuò)了。但對(duì)于非阻塞socket、read/write返回-1不一定網(wǎng)絡(luò)真的出錯(cuò)了。可能支持緩沖區(qū)空或者滿,這時(shí)應(yīng)該再試,直到Resource available。
綜上,對(duì)于非阻塞的socket,正確的讀寫(xiě)操作為:
LT模式
讀: 忽略掉errno = EAGAIN的錯(cuò)誤,下次繼續(xù)讀;
寫(xiě):忽略掉errno = EAGAIN的錯(cuò)誤,下次繼續(xù)寫(xiě)。
對(duì)于select和epoll的LT模式,這種讀寫(xiě)方式是沒(méi)有問(wèn)題的。但對(duì)于epoll的ET模式,這種方式還有漏洞。
下面來(lái)介紹下epoll事件的兩種模式LT(水平觸發(fā))和ET(邊沿觸發(fā)),根據(jù)可以理解為,文件描述符的讀寫(xiě)狀態(tài)發(fā)生變化才會(huì)觸發(fā)epoll事件,具體說(shuō)來(lái)如下:二者的差異在于 level-trigger 模式下只要某個(gè) socket 處于 readable/writable 狀態(tài),無(wú)論什么時(shí)候進(jìn)行 epoll_wait 都會(huì)返回該 socket;而 edge-trigger 模式下只有某個(gè) socket 從 unreadable 變?yōu)?readable,或從unwritable 變?yōu)閣ritable時(shí),epoll_wait 才會(huì)返回該 socket。如下兩個(gè)示意圖:
從socket讀數(shù)據(jù):
往socket寫(xiě)數(shù)據(jù):
所以在epoll的ET模式下,正確的讀寫(xiě)方式為:
讀: 只要可讀, 就一直讀,直到返回0,或者 errno = EAGAIN寫(xiě):只要可寫(xiě), 就一直寫(xiě),直到數(shù)據(jù)發(fā)送完,或者 errno = EAGAIN
這里的意思是,對(duì)于ET模式,相當(dāng)于我們要自己重寫(xiě)read和write,使其像”原子操作“一樣,保證一次read 或 write能夠完整的讀完緩沖區(qū)的數(shù)據(jù)或者寫(xiě)完要寫(xiě)入緩沖區(qū)的數(shù)據(jù)。因此,實(shí)現(xiàn)為用while包住read和write即可。但是對(duì)于select或者LT模式,我們可以只使用一次read和write,因?yàn)樵谥鞒绦蛑袝?huì)一直while,而事件再下一次select時(shí)還會(huì)被獲取到。但也可以實(shí)現(xiàn)為用while包住read和write。從邏輯上講,一次性把數(shù)據(jù)讀取完整可以保證數(shù)據(jù)的完整性。
下面來(lái)說(shuō)明這種”原子操作“read和write
int n = 0; while(1) { nread = read(fd, buf + n, BUFSIZ - 1); //讀時(shí),用戶進(jìn)程指定的接收數(shù)據(jù)緩沖區(qū)大小固定,一般要比數(shù)據(jù)大 if(nread < 0) { if(errno == EAGAIN || errno == EWOULDBLOCK) { continue; } else { break; //or return; } } else if(nread == 0) { break; //or return. because read the EOF } else { n += nread; } }
int data_size = strlen(buf); int n = 0; while(1) { nwrite = write(fd, buf + n, data_size);//寫(xiě)時(shí),數(shù)據(jù)大小一直在變化 if(nwrite < data_size) { if(errno == EAGAIN || errno == EWOULDBLOCK) { continue; } else { break;//or return; } } else { n += nwrite; data_size -= nwrite; } }
正確的accept,accept 要考慮 2 個(gè)問(wèn)題:
(1) LT模式下或ET模式下,阻塞的監(jiān)聽(tīng)socket, accept 存在的問(wèn)題
accept每次都是從已經(jīng)完成三次握手的tcp隊(duì)列中取出一個(gè)連接,考慮這種情況: TCP 連接被客戶端夭折,即在服務(wù)器調(diào)用 accept 之前,客戶端主動(dòng)發(fā)送 RST 終止連接,導(dǎo)致剛剛建立的連接從就緒隊(duì)列中移出,如果套接口被設(shè)置成阻塞模式,服務(wù)器就會(huì)一直阻塞在 accept 調(diào)用上,直到其他某個(gè)客戶建立一個(gè)新的連接為止。但是在此期間,服務(wù)器單純地阻塞在accept 調(diào)用上,就緒隊(duì)列中的其他描述符都得不到處理。
解決辦法是:把監(jiān)聽(tīng)套接口設(shè)置為非阻塞,當(dāng)客戶在服務(wù)器調(diào)用 accept 之前中止某個(gè)連接時(shí),accept 調(diào)用可以立即返回 -1, 這時(shí)源自 Berkeley 的實(shí)現(xiàn)會(huì)在內(nèi)核中處理該事件,并不會(huì)將該事件通知給 epoll,而其他實(shí)現(xiàn)把 errno 設(shè)置為 ECONNABORTED 或者 EPROTO 錯(cuò)誤,我們應(yīng)該忽略這兩個(gè)錯(cuò)誤。
(2) ET 模式下 accept 存在的問(wèn)題
考慮這種情況:多個(gè)連接同時(shí)到達(dá),服務(wù)器的 TCP 就緒隊(duì)列瞬間積累多個(gè)就緒連接,由于是邊緣觸發(fā)模式,epoll 只會(huì)通知一次,accept 只處理一個(gè)連接,導(dǎo)致 TCP 就緒隊(duì)列中剩下的連接都得不到處理。
解決辦法是:將監(jiān)聽(tīng)套接字設(shè)置為非阻塞模式,用 while 循環(huán)抱住 accept 調(diào)用,處理完 TCP 就緒隊(duì)列中的所有連接后再退出循環(huán)。如何知道是否處理完就緒隊(duì)列中的所有連接呢? accept 返回 -1 并且 errno 設(shè)置為 EAGAIN 就表示所有連接都處理完。
綜合以上兩種情況,服務(wù)器應(yīng)該使用非阻塞地 accept, accept 在 ET 模式下 的正確使用方式為:
while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote, (size_t *)&addrlen)) > 0) { handle_client(conn_sock); } if (conn_sock == -1) { if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR) perror("accept"); }
一道騰訊后臺(tái)開(kāi)發(fā)的面試題:
使用Linux epoll模型,水平觸發(fā)模式;當(dāng)socket可寫(xiě)時(shí),會(huì)不停的觸發(fā) socket 可寫(xiě)的事件,如何處理?
第一種最普遍的方式:
需要向 socket 寫(xiě)數(shù)據(jù)的時(shí)候才把 socket 加入 epoll ,等待可寫(xiě)事件。接受到可寫(xiě)事件后,調(diào)用 write 或者 send 發(fā)送數(shù)據(jù)。當(dāng)所有數(shù)據(jù)都寫(xiě)完后,把 socket 移出 epoll。
這種方式的缺點(diǎn)是,即使發(fā)送很少的數(shù)據(jù),也要把 socket 加入 epoll,寫(xiě)完后在移出 epoll,有一定操作代價(jià)。
一種改進(jìn)的方式:
開(kāi)始不把 socket 加入 epoll,需要向 socket 寫(xiě)數(shù)據(jù)的時(shí)候,直接調(diào)用 write 或者 send 發(fā)送數(shù)據(jù)。如果返回 EAGAIN,把 socket 加入 epoll,在 epoll 的驅(qū)動(dòng)下寫(xiě)數(shù)據(jù),全部數(shù)據(jù)發(fā)送完畢后,再移出 epoll。
這種方式的優(yōu)點(diǎn)是:數(shù)據(jù)不多的時(shí)候可以避免 epoll 的事件處理,提高效率。
多路復(fù)用
如文初的說(shuō)明表示,這三者都是 I/O 多路復(fù)用機(jī)制,且簡(jiǎn)要介紹了多路復(fù)用的定義,那么如何更加直觀地了解多路復(fù)用呢?這里有張圖:
對(duì)于網(wǎng)頁(yè)服務(wù)器 Nginx 來(lái)說(shuō),會(huì)有很多連接進(jìn)來(lái), epoll 會(huì)把他們都監(jiān)視起來(lái),然后像撥開(kāi)關(guān)一樣,誰(shuí)有數(shù)據(jù)就撥向誰(shuí),然后調(diào)用相應(yīng)的代碼處理。
一般來(lái)說(shuō)以下場(chǎng)合需要使用 I/O 多路復(fù)用:
當(dāng)客戶處理多個(gè)描述字時(shí)(一般是交互式輸入和網(wǎng)絡(luò)套接口)
如果一個(gè)服務(wù)器既要處理 TCP,又要處理 UDP,一般要使用 I/O 復(fù)用
如果一個(gè) TCP 服務(wù)器既要處理監(jiān)聽(tīng)套接口,又要處理已連接套接口
為什么epoll可以支持百萬(wàn)級(jí)別的連接?
在server的處理過(guò)程中,大家可以看到其中重要的操作是,使用epoll_ctl修改clientfd在epoll中注冊(cè)的epoll_event, 這個(gè)操作首先在紅黑樹(shù)中找到fd對(duì)應(yīng)的epoll_event, 然后進(jìn)行修改,紅黑樹(shù)是典型的二叉平衡樹(shù),其時(shí)間復(fù)雜度是log2(n), 1百萬(wàn)的文件句柄,只需要16次左右的查找,速度是非??斓?,支持百萬(wàn)級(jí)別毫無(wú)壓力
另外,epoll通過(guò)注冊(cè)fd上的回調(diào)函數(shù),回調(diào)函數(shù)監(jiān)控到有事件發(fā)生,則準(zhǔn)備好相關(guān)的數(shù)據(jù)放到到就緒鏈表里面去,這個(gè)動(dòng)作非??欤杀疽卜浅P?/p>
socket讀寫(xiě)返回值的處理
在調(diào)用socket讀寫(xiě)函數(shù)read(),write()時(shí),都會(huì)有返回值。如果沒(méi)有正確處理返回值,就可能引入一些問(wèn)題
總結(jié)了以下幾點(diǎn)
1當(dāng)read()或者write()函數(shù)返回值大于0時(shí),表示實(shí)際從緩沖區(qū)讀取或者寫(xiě)入的字節(jié)數(shù)目
2當(dāng)read()函數(shù)返回值為0時(shí),表示對(duì)端已經(jīng)關(guān)閉了 socket,這時(shí)候也要關(guān)閉這個(gè)socket,否則會(huì)導(dǎo)致socket泄露。netstat命令查看下,如果有closewait狀態(tài)的socket,就是socket泄露了
當(dāng)write()函數(shù)返回0時(shí),表示當(dāng)前寫(xiě)緩沖區(qū)已滿,是正常情況,下次再來(lái)寫(xiě)就行了。
3當(dāng)read()或者write()返回-1時(shí),一般要判斷errno
如果errno == EINTR,表示系統(tǒng)當(dāng)前中斷了,直接忽略
如果errno == EAGAIN或者EWOULDBLOCK,非阻塞socket直接忽略;如果是阻塞的socket,一般是讀寫(xiě)操作超時(shí)了,還未返回。這個(gè)超時(shí)是指socket的SO_RCVTIMEO與SO_SNDTIMEO兩個(gè)屬性。所以在使用阻塞socket時(shí),不要將超時(shí)時(shí)間設(shè)置的過(guò)小。不然返回了-1,你也不知道是socket連接是真的斷開(kāi)了,還是正常的網(wǎng)絡(luò)抖動(dòng)。一般情況下,阻塞的socket返回了-1,都需要關(guān)閉重新連接。
4.另外,對(duì)于非阻塞的connect,可能返回-1.這時(shí)需要判斷errno,如果 errno == EINPROGRESS,表示正在處理中,否則表示連接出錯(cuò)了,需要關(guān)閉重連。之后使用select,檢測(cè)到該socket的可寫(xiě)事件時(shí),要判斷getsockopt(c->fd, SOL_SOCKET, SO_ERROR, &err, &errlen),看socket是否出錯(cuò)了。如果err值為0,則表示connect成功;否則也應(yīng)該關(guān)閉重連
5 在使用epoll時(shí),有ET與LT兩種模式。ET模式下,socket需要read或者write到返回-1為止。對(duì)于非阻塞的socket沒(méi)有問(wèn)題,但是如果是阻塞的socket,正如第三條中所說(shuō)的,只有超時(shí)才會(huì)返回。所以在ET模式下千萬(wàn)不要使用阻塞的socket。那么LT模式為什么沒(méi)問(wèn)題呢?一般情況下,使用LT模式,我們只要調(diào)用一次read或者write函數(shù),如果沒(méi)有讀完或者沒(méi)有寫(xiě)完,下次再來(lái)就是了。由于已經(jīng)返回了可讀或者可寫(xiě)事件,所以可以保證調(diào)用一次read或者write會(huì)正常返回。
nread為-1且errno==EAGAIN,說(shuō)明數(shù)據(jù)已經(jīng)讀完,設(shè)置EPOLLOUT。
網(wǎng)絡(luò)狀態(tài)查詢命令
sar、iostat、lsof
問(wèn)題記錄
客戶端
1、Cannot assign requested address
大致上是由于客戶端頻繁的連服務(wù)器,由于每次連接都在很短的時(shí)間內(nèi)結(jié)束,導(dǎo)致很多的TIME_WAIT,以至于用光了可用的端 口號(hào),所以新的連接沒(méi)辦法綁定端口,即“Cannot assign requested address”。是客戶端的問(wèn)題不是服務(wù)器端的問(wèn)題。通過(guò)netstat,的確看到很多TIME_WAIT狀態(tài)的連接。
client端頻繁建立連接,而端口釋放較慢,導(dǎo)致建立新連接時(shí)無(wú)可用端口。
netstat -a|grep TIME_WAIT tcp 0 0 e100069210180.zmf:49477 e100069202104.zmf.tbs:websm TIME_WAIT tcp 0 0 e100069210180.zmf:49481 e100069202104.zmf.tbs:websm TIME_WAIT tcp 0 0 e100069210180.zmf:49469 e100069202104.zmf.tbs:websm TIME_WAIT ……
解決辦法
執(zhí)行命令修改如下內(nèi)核參數(shù) (需要root權(quán)限)
調(diào)低端口釋放后的等待時(shí)間,默認(rèn)為60s,修改為15~30s:
sysctl -w net.ipv4.tcp_fin_timeout=30
修改tcp/ip協(xié)議配置, 通過(guò)配置/proc/sys/net/ipv4/tcp_tw_resue, 默認(rèn)為0,修改為1,釋放TIME_WAIT端口給新連接使用:
sysctl -w net.ipv4.tcp_timestamps=1
修改tcp/ip協(xié)議配置,快速回收socket資源,默認(rèn)為0,修改為1:
sysctl -w net.ipv4.tcp_tw_recycle=1
允許端口重用:
sysctl -w net.ipv4.tcp_tw_reuse = 1
2、2.8萬(wàn)左右的鏈接,報(bào)錯(cuò)誤(可能端口用盡)
如果沒(méi)有TIME_WAIT 狀態(tài)的連接,那有可能端口用盡,特別是長(zhǎng)連接的時(shí)候,查看開(kāi)放的端口范圍:
[root@VM_0_8_centos usr]# sysctl -a |grep port_range net.ipv4.ip_local_port_range = 32768 60999 sysctl: reading key "net.ipv6.conf.all.stable_secret" sysctl: reading key "net.ipv6.conf.default.stable_secret" sysctl: reading key "net.ipv6.conf.eth0.stable_secret" sysctl: reading key "net.ipv6.conf.lo.stable_secret" [root@VM_0_8_centos usr]#
60999 - 32768 = 28,231,剛好2.8萬(wàn),長(zhǎng)連接把端口用光了。修改端口開(kāi)放范圍:
vi /etc/sysctl.conf net.ipv4.ip_local_port_range = 10000 65535
執(zhí)行sysctl -p 使得生效
服務(wù)端
影響鏈接不往上走的原因:
1、端口用完了 (客戶端,
查看端口范圍sysctl -a |grep port_range,返回:
net.ipv4.ip_local_port_range = 32768 60999,所以可用端口是60999-32768 =2.8w )
2、文件fd用完了
3、內(nèi)存用完了
4、網(wǎng)絡(luò)
5、配置:fsfile-max = 1048576 #文件fd的最大值,
fd=open(),fd從3開(kāi)始,0:stdin標(biāo)準(zhǔn)輸入1:stdout標(biāo)準(zhǔn)輸出 2:stderr錯(cuò)誤輸出
Epoll 難以解決的問(wèn)題
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 還是要從這個(gè)api說(shuō)起,這個(gè)api 可以監(jiān)聽(tīng)很多個(gè)fd,但是timeout 只有一個(gè)。 有這一個(gè)場(chǎng)景: 你想關(guān)注 fd == 10的這個(gè)描述符,你希望該描述符有數(shù)據(jù)到來(lái)的時(shí)候通知你,并且,如果一直沒(méi)有數(shù)據(jù)來(lái),那么希望20s之后能通知你。 你改怎么做?
進(jìn)一步假設(shè),你希望關(guān)注 100 個(gè)fd, 并希望這個(gè)一百個(gè)fd,從他們加入監(jiān)聽(tīng)隊(duì)列的時(shí)候開(kāi)始計(jì)算,20s后通知你超時(shí)
如果使用這個(gè)epoll_wait的話,是不是要自己記住所有fd的剩余超時(shí)時(shí)間呢?
libevent就解決了這個(gè)困擾。
配置和調(diào)試
net.ipv4.tcp_syncookies = 1 表示開(kāi)啟SYN Cookies。當(dāng)出現(xiàn)SYN等待隊(duì)列溢出時(shí),啟用cookies來(lái)處理,可防范少量SYN攻擊,默認(rèn)為0,表示關(guān)閉;
net.ipv4.tcp_tw_reuse = 1 表示開(kāi)啟重用。允許將TIME-WAIT sockets重新用于新的TCP連接,默認(rèn)為0,表示關(guān)閉;
net.ipv4.tcp_tw_recycle = 1 表示開(kāi)啟TCP連接中TIME-WAIT sockets的快速回收,默認(rèn)為0,表示關(guān)閉。
net.ipv4.tcp_fin_timeout 修改系默認(rèn)的 TIMEOUT 時(shí)間
優(yōu)化time_wait為啥要開(kāi)syncookie呢? syncookie可以繞過(guò)seq queue的限制,跟優(yōu)化time_wait沒(méi)有關(guān)系
另外得打開(kāi)timestamps,才能讓reuse、recycle生效。
修改fin_timeout,如何調(diào)?調(diào)大調(diào)小呢?
//測(cè)試數(shù)據(jù)
單線程,峰值處理鏈接,貌似目前測(cè)試條件可以測(cè)試到2.9K
CONNECT: 2.9K/s
QPS : 27.6萬(wàn)
5萬(wàn)的鏈接。
QPS : 1.9萬(wàn)
客戶端、服務(wù)端支持多少連接
客戶端
現(xiàn)在我們終于可以得出更為正確的結(jié)論了,對(duì)于有1個(gè)Ip的客戶端來(lái)說(shuō),受限于ip_local_port_range參數(shù),也受限于65535。但單Linux可以配置多個(gè)ip,有幾個(gè)ip,最大理論值就翻幾倍
多張網(wǎng)卡不是必須的。即使只有一張網(wǎng)卡,也可以配置多ip。k8s就是這么干的,在k8s里,一臺(tái)物理機(jī)上可以部署多個(gè)pod。但每一個(gè)pod都會(huì)被分配一個(gè)獨(dú)立的ip,所以完全不用擔(dān)心物理機(jī)上部署了過(guò)多的pod而影響你用的pod里的TCP連接數(shù)量。在ip給你的那一刻,你的pod就和其它應(yīng)用隔離開(kāi)了。
服務(wù)器端
一條TCP連接如果不發(fā)送數(shù)據(jù)的話,消耗內(nèi)存是3.3K左右。如果有數(shù)據(jù)發(fā)送,需要為每條TCP分配發(fā)送緩存區(qū),大小受你的參數(shù)net.ipv4.tcp_wmem配置影響,默認(rèn)情況下最小是4K。如果發(fā)送結(jié)束,緩存區(qū)消耗的內(nèi)存會(huì)被回收。
假設(shè)你只保持連接不發(fā)送數(shù)據(jù),那么你服務(wù)器可以建立的連接最大數(shù)量 = 你的內(nèi)存/3.3K。假如是4GB的內(nèi)存,那么大約可接受的TCP連接數(shù)量是100萬(wàn)左右。
這個(gè)例子里,我們考慮的前提是在一個(gè)進(jìn)程下hold所有的服務(wù)器端連接。而在實(shí)際中的項(xiàng)目里,為了收發(fā)數(shù)據(jù)方便,很多網(wǎng)絡(luò)IO模型還會(huì)為TCP連接再創(chuàng)建一個(gè)線程或協(xié)程。拿最輕量的golang來(lái)說(shuō),一個(gè)協(xié)程棧也需要2KB的內(nèi)存開(kāi)銷。
結(jié)論
TCP連接的客戶端機(jī):每一個(gè)ip可建立的TCP連接理論受限于ip_local_port_range參數(shù),也受限于65535。但可以通過(guò)配置多ip的方式來(lái)加大自己的建立連接的能力。
TCP連接的服務(wù)器機(jī):每一個(gè)監(jiān)聽(tīng)的端口雖然理論值很大,但這個(gè)數(shù)字沒(méi)有實(shí)際意義。最大并發(fā)數(shù)取決你的內(nèi)存大小,每一條靜止?fàn)顟B(tài)的TCP連接大約需要吃3.3K的內(nèi)存。
select、poll、epoll之間的區(qū)別(搜狗面試)
(1)select==>時(shí)間復(fù)雜度O(n)
它僅僅知道了,有I/O事件發(fā)生了,卻并不知道是哪那幾個(gè)流(可能有一個(gè),多個(gè),甚至全部),我們只能無(wú)差別輪詢所有流,找出能讀出數(shù)據(jù),或者寫(xiě)入數(shù)據(jù)的流,對(duì)他們進(jìn)行操作。所以select具有O(n)的無(wú)差別輪詢復(fù)雜度,同時(shí)處理的流越多,無(wú)差別輪詢時(shí)間就越長(zhǎng)。
(2)poll==>時(shí)間復(fù)雜度O(n)
poll本質(zhì)上和select沒(méi)有區(qū)別,它將用戶傳入的數(shù)組拷貝到內(nèi)核空間,然后查詢每個(gè)fd對(duì)應(yīng)的設(shè)備狀態(tài), 但是它沒(méi)有最大連接數(shù)的限制,原因是它是基于鏈表來(lái)存儲(chǔ)的.
(3)epoll==>時(shí)間復(fù)雜度O(1)
epoll可以理解為event poll,不同于忙輪詢和無(wú)差別輪詢,epoll會(huì)把哪個(gè)流發(fā)生了怎樣的I/O事件通知我們。所以我們說(shuō)epoll實(shí)際上是事件驅(qū)動(dòng)(每個(gè)事件關(guān)聯(lián)上fd)的,此時(shí)我們對(duì)這些流的操作都是有意義的。(復(fù)雜度降低到了O(1))
select,poll,epoll都是IO多路復(fù)用的機(jī)制。I/O多路復(fù)用就通過(guò)一種機(jī)制,可以監(jiān)視多個(gè)描述符,一旦某個(gè)描述符就緒(一般是讀就緒或者寫(xiě)就緒),能夠通知程序進(jìn)行相應(yīng)的讀寫(xiě)操作。但select,poll,epoll本質(zhì)上都是同步I/O,因?yàn)樗麄兌夹枰谧x寫(xiě)事件就緒后自己負(fù)責(zé)進(jìn)行讀寫(xiě),也就是說(shuō)這個(gè)讀寫(xiě)過(guò)程是阻塞的,而異步I/O則無(wú)需自己負(fù)責(zé)進(jìn)行讀寫(xiě),異步I/O的實(shí)現(xiàn)會(huì)負(fù)責(zé)把數(shù)據(jù)從內(nèi)核拷貝到用戶空間。
epoll跟select都能提供多路I/O復(fù)用的解決方案。在現(xiàn)在的Linux內(nèi)核里有都能夠支持,其中epoll是Linux所特有,而select則應(yīng)該是POSIX所規(guī)定,一般操作系統(tǒng)均有實(shí)現(xiàn)
select:
select本質(zhì)上是通過(guò)設(shè)置或者檢查存放fd標(biāo)志位的數(shù)據(jù)結(jié)構(gòu)來(lái)進(jìn)行下一步處理。這樣所帶來(lái)的缺點(diǎn)是:
1、 單個(gè)進(jìn)程可監(jiān)視的fd數(shù)量被限制,即能監(jiān)聽(tīng)端口的大小有限。
一般來(lái)說(shuō)這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大,具體數(shù)目可以cat /proc/sys/fs/file-max察看。32位機(jī)默認(rèn)是1024個(gè)。64位機(jī)默認(rèn)是2048.
2、 對(duì)socket進(jìn)行掃描時(shí)是線性掃描,即采用輪詢的方法,效率較低:
當(dāng)套接字比較多的時(shí)候,每次select()都要通過(guò)遍歷FD_SETSIZE個(gè)Socket來(lái)完成調(diào)度,不管哪個(gè)Socket是活躍的,都遍歷一遍。這會(huì)浪費(fèi)很多CPU時(shí)間。如果能給套接字注冊(cè)某個(gè)回調(diào)函數(shù),當(dāng)他們活躍時(shí),自動(dòng)完成相關(guān)操作,那就避免了輪詢,這正是epoll與kqueue做的。
3、需要維護(hù)一個(gè)用來(lái)存放大量fd的數(shù)據(jù)結(jié)構(gòu),這樣會(huì)使得用戶空間和內(nèi)核空間在傳遞該結(jié)構(gòu)時(shí)復(fù)制開(kāi)銷大
poll:
poll本質(zhì)上和select沒(méi)有區(qū)別,它將用戶傳入的數(shù)組拷貝到內(nèi)核空間,然后查詢每個(gè)fd對(duì)應(yīng)的設(shè)備狀態(tài),如果設(shè)備就緒則在設(shè)備等待隊(duì)列中加入一項(xiàng)并繼續(xù)遍歷,如果遍歷完所有fd后沒(méi)有發(fā)現(xiàn)就緒設(shè)備,則掛起當(dāng)前進(jìn)程,直到設(shè)備就緒或者主動(dòng)超時(shí),被喚醒后它又要再次遍歷fd。這個(gè)過(guò)程經(jīng)歷了多次無(wú)謂的遍歷。
它沒(méi)有最大連接數(shù)的限制,原因是它是基于鏈表來(lái)存儲(chǔ)的,但是同樣有一個(gè)缺點(diǎn):
1、大量的fd的數(shù)組被整體復(fù)制于用戶態(tài)和內(nèi)核地址空間之間,而不管這樣的復(fù)制是不是有意義。
2、poll還有一個(gè)特點(diǎn)是“水平觸發(fā)”,如果報(bào)告了fd后,沒(méi)有被處理,那么下次poll時(shí)會(huì)再次報(bào)告該fd。
epoll:
epoll有EPOLLLT和EPOLLET兩種觸發(fā)模式,LT是默認(rèn)的模式,ET是“高速”模式。LT模式下,只要這個(gè)fd還有數(shù)據(jù)可讀,每次 epoll_wait都會(huì)返回它的事件,提醒用戶程序去操作,而在ET(邊緣觸發(fā))模式中,它只會(huì)提示一次,直到下次再有數(shù)據(jù)流入之前都不會(huì)再提示了,無(wú) 論fd中是否還有數(shù)據(jù)可讀。所以在ET模式下,read一個(gè)fd的時(shí)候一定要把它的buffer讀光,也就是說(shuō)一直讀到read的返回值小于請(qǐng)求值,或者 遇到EAGAIN錯(cuò)誤。還有一個(gè)特點(diǎn)是,epoll使用“事件”的就緒通知方式,通過(guò)epoll_ctl注冊(cè)fd,一旦該fd就緒,內(nèi)核就會(huì)采用類似callback的回調(diào)機(jī)制來(lái)激活該fd,epoll_wait便可以收到通知。
epoll為什么要有EPOLLET觸發(fā)模式?
如果采用EPOLLLT模式的話,系統(tǒng)中一旦有大量你不需要讀寫(xiě)的就緒文件描述符,它們每次調(diào)用epoll_wait都會(huì)返回,這樣會(huì)大大降低處理程序檢索自己關(guān)心的就緒文件描述符的效率.。而采用EPOLLET這種邊沿觸發(fā)模式的話,當(dāng)被監(jiān)控的文件描述符上有可讀寫(xiě)事件發(fā)生時(shí),epoll_wait()會(huì)通知處理程序去讀寫(xiě)。如果這次沒(méi)有把數(shù)據(jù)全部讀寫(xiě)完(如讀寫(xiě)緩沖區(qū)太小),那么下次調(diào)用epoll_wait()時(shí),它不會(huì)通知你,也就是它只會(huì)通知你一次,直到該文件描述符上出現(xiàn)第二次可讀寫(xiě)事件才會(huì)通知你?。?!這種模式比水平觸發(fā)效率高,系統(tǒng)不會(huì)充斥大量你不關(guān)心的就緒文件描述符
epoll的優(yōu)點(diǎn):
1、沒(méi)有最大并發(fā)連接的限制,能打開(kāi)的FD的上限遠(yuǎn)大于1024(1G的內(nèi)存上能監(jiān)聽(tīng)約10萬(wàn)個(gè)端口);
2、效率提升,不是輪詢的方式,不會(huì)隨著FD數(shù)目的增加效率下降。只有活躍可用的FD才會(huì)調(diào)用callback函數(shù);
即Epoll最大的優(yōu)點(diǎn)就在于它只管你“活躍”的連接,而跟連接總數(shù)無(wú)關(guān),因此在實(shí)際的網(wǎng)絡(luò)環(huán)境中,Epoll的效率就會(huì)遠(yuǎn)遠(yuǎn)高于select和poll。
3、 內(nèi)存拷貝,利用mmap()文件映射內(nèi)存加速與內(nèi)核空間的消息傳遞;即epoll使用mmap減少?gòu)?fù)制開(kāi)銷。
select、poll、epoll 區(qū)別總結(jié):
1、支持一個(gè)進(jìn)程所能打開(kāi)的最大連接數(shù)
select
單個(gè)進(jìn)程所能打開(kāi)的最大連接數(shù)有FD_SETSIZE宏定義,其大小是32個(gè)整數(shù)的大?。ㄔ?2位的機(jī)器上,大小就是3232,同理64位機(jī)器上FD_SETSIZE為3264),當(dāng)然我們可以對(duì)進(jìn)行修改,然后重新編譯內(nèi)核,但是性能可能會(huì)受到影響,這需要進(jìn)一步的測(cè)試。
poll
poll本質(zhì)上和select沒(méi)有區(qū)別,但是它沒(méi)有最大連接數(shù)的限制,原因是它是基于鏈表來(lái)存儲(chǔ)的
epoll
雖然連接數(shù)有上限,但是很大,1G內(nèi)存的機(jī)器上可以打開(kāi)10萬(wàn)左右的連接,2G內(nèi)存的機(jī)器可以打開(kāi)20萬(wàn)左右的連接
2、FD劇增后帶來(lái)的IO效率問(wèn)題
select
因?yàn)槊看握{(diào)用時(shí)都會(huì)對(duì)連接進(jìn)行線性遍歷,所以隨著FD的增加會(huì)造成遍歷速度慢的“線性下降性能問(wèn)題”。
poll
同上
epoll
因?yàn)閑poll內(nèi)核中實(shí)現(xiàn)是根據(jù)每個(gè)fd上的callback函數(shù)來(lái)實(shí)現(xiàn)的,只有活躍的socket才會(huì)主動(dòng)調(diào)用callback,所以在活躍socket較少的情況下,使用epoll沒(méi)有前面兩者的線性下降的性能問(wèn)題,但是所有socket都很活躍的情況下,可能會(huì)有性能問(wèn)題。
3、 消息傳遞方式
select
內(nèi)核需要將消息傳遞到用戶空間,都需要內(nèi)核拷貝動(dòng)作
poll
同上
epoll
epoll通過(guò)內(nèi)核和用戶空間共享一塊內(nèi)存來(lái)實(shí)現(xiàn)的。
總結(jié):
綜上,在選擇select,poll,epoll時(shí)要根據(jù)具體的使用場(chǎng)合以及這三種方式的自身特點(diǎn)。
1、表面上看epoll的性能最好,但是在連接數(shù)少并且連接都十分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機(jī)制需要很多函數(shù)回調(diào)。
2、select低效是因?yàn)槊看嗡夹枰喸?。但低效也是相?duì)的,視情況而定,也可通過(guò)良好的設(shè)計(jì)改善
今天對(duì)這三種IO多路復(fù)用進(jìn)行對(duì)比,參考網(wǎng)上和書(shū)上面的資料,整理如下:
1、select實(shí)現(xiàn)
select的調(diào)用過(guò)程如下所示:
(1)使用copy_from_user從用戶空間拷貝fd_set到內(nèi)核空間
(2)注冊(cè)回調(diào)函數(shù)__pollwait
(3)遍歷所有fd,調(diào)用其對(duì)應(yīng)的poll方法(對(duì)于socket,這個(gè)poll方法是sock_poll,sock_poll根據(jù)情況會(huì)調(diào)用到tcp_poll,udp_poll或者datagram_poll)
(4)以tcp_poll為例,其核心實(shí)現(xiàn)就是__pollwait,也就是上面注冊(cè)的回調(diào)函數(shù)。
(5)__pollwait的主要工作就是把current(當(dāng)前進(jìn)程)掛到設(shè)備的等待隊(duì)列中,不同的設(shè)備有不同的等待隊(duì)列,對(duì)于tcp_poll來(lái)說(shuō),其等待隊(duì)列是sk->sk_sleep(注意把進(jìn)程掛到等待隊(duì)列中并不代表進(jìn)程已經(jīng)睡眠了)。在設(shè)備收到一條消息(網(wǎng)絡(luò)設(shè)備)或填寫(xiě)完文件數(shù)據(jù)(磁盤設(shè)備)后,會(huì)喚醒設(shè)備等待隊(duì)列上睡眠的進(jìn)程,這時(shí)current便被喚醒了。
(6)poll方法返回時(shí)會(huì)返回一個(gè)描述讀寫(xiě)操作是否就緒的mask掩碼,根據(jù)這個(gè)mask掩碼給fd_set賦值。
(7)如果遍歷完所有的fd,還沒(méi)有返回一個(gè)可讀寫(xiě)的mask掩碼,則會(huì)調(diào)用schedule_timeout是調(diào)用select的進(jìn)程(也就是current)進(jìn)入睡眠。當(dāng)設(shè)備驅(qū)動(dòng)發(fā)生自身資源可讀寫(xiě)后,會(huì)喚醒其等待隊(duì)列上睡眠的進(jìn)程。如果超過(guò)一定的超時(shí)時(shí)間(schedule_timeout指定),還是沒(méi)人喚醒,則調(diào)用select的進(jìn)程會(huì)重新被喚醒獲得CPU,進(jìn)而重新遍歷fd,判斷有沒(méi)有就緒的fd。
(8)把fd_set從內(nèi)核空間拷貝到用戶空間。
總結(jié):
select的幾大缺點(diǎn):
(1)每次調(diào)用select,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài),這個(gè)開(kāi)銷在fd很多時(shí)會(huì)很大
(2)同時(shí)每次調(diào)用select都需要在內(nèi)核遍歷傳遞進(jìn)來(lái)的所有fd,這個(gè)開(kāi)銷在fd很多時(shí)也很大
(3)select支持的文件描述符數(shù)量太小了,默認(rèn)是1024
2 poll實(shí)現(xiàn)
poll的實(shí)現(xiàn)和select非常相似,只是描述fd集合的方式不同,poll使用pollfd結(jié)構(gòu)而不是select的fd_set結(jié)構(gòu),其他的都差不多,管理多個(gè)描述符也是進(jìn)行輪詢,根據(jù)描述符的狀態(tài)進(jìn)行處理,但是poll沒(méi)有最大文件描述符數(shù)量的限制。poll和select同樣存在一個(gè)缺點(diǎn)就是,包含大量文件描述符的數(shù)組被整體復(fù)制于用戶態(tài)和內(nèi)核的地址空間之間,而不論這些文件描述符是否就緒,它的開(kāi)銷隨著文件描述符數(shù)量的增加而線性增大。
3、epoll
epoll既然是對(duì)select和poll的改進(jìn),就應(yīng)該能避免上述的三個(gè)缺點(diǎn)。那epoll都是怎么解決的呢?在此之前,我們先看一下epoll和select和poll的調(diào)用接口上的不同,select和poll都只提供了一個(gè)函數(shù)——select或者poll函數(shù)。而epoll提供了三個(gè)函數(shù),epoll_create,epoll_ctl和epoll_wait,epoll_create是創(chuàng)建一個(gè)epoll句柄;epoll_ctl是注冊(cè)要監(jiān)聽(tīng)的事件類型;epoll_wait則是等待事件的產(chǎn)生。
對(duì)于第一個(gè)缺點(diǎn),epoll的解決方案在epoll_ctl函數(shù)中。每次注冊(cè)新的事件到epoll句柄中時(shí)(在epoll_ctl中指定EPOLL_CTL_ADD),會(huì)把所有的fd拷貝進(jìn)內(nèi)核,而不是在epoll_wait的時(shí)候重復(fù)拷貝。epoll保證了每個(gè)fd在整個(gè)過(guò)程中只會(huì)拷貝一次。
對(duì)于第二個(gè)缺點(diǎn),epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對(duì)應(yīng)的設(shè)備等待隊(duì)列中,而只在epoll_ctl時(shí)把current掛一遍(這一遍必不可少)并為每個(gè)fd指定一個(gè)回調(diào)函數(shù),當(dāng)設(shè)備就緒,喚醒等待隊(duì)列上的等待者時(shí),就會(huì)調(diào)用這個(gè)回調(diào)函數(shù),而這個(gè)回調(diào)函數(shù)會(huì)把就緒的fd加入一個(gè)就緒鏈表)。epoll_wait的工作實(shí)際上就是在這個(gè)就緒鏈表中查看有沒(méi)有就緒的fd(利用schedule_timeout()實(shí)現(xiàn)睡一會(huì),判斷一會(huì)的效果,和select實(shí)現(xiàn)中的第7步是類似的)。
對(duì)于第三個(gè)缺點(diǎn),epoll沒(méi)有這個(gè)限制,它所支持的FD上限是最大可以打開(kāi)文件的數(shù)目,這個(gè)數(shù)字一般遠(yuǎn)大于2048,舉個(gè)例子,在1GB內(nèi)存的機(jī)器上大約是10萬(wàn)左右,具體數(shù)目可以cat /proc/sys/fs/file-max察看,一般來(lái)說(shuō)這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大。
總結(jié):
(1)select,poll實(shí)現(xiàn)需要自己不斷輪詢所有fd集合,直到設(shè)備就緒,期間可能要睡眠和喚醒多次交替。而epoll其實(shí)也需要調(diào)用epoll_wait不斷輪詢就緒鏈表,期間也可能多次睡眠和喚醒交替,但是它是設(shè)備就緒時(shí),調(diào)用回調(diào)函數(shù),把就緒fd放入就緒鏈表中,并喚醒在epoll_wait中進(jìn)入睡眠的進(jìn)程。雖然都要睡眠和交替,但是select和poll在“醒著”的時(shí)候要遍歷整個(gè)fd集合,而epoll在“醒著”的時(shí)候只要判斷一下就緒鏈表是否為空就行了,這節(jié)省了大量的CPU時(shí)間。這就是回調(diào)機(jī)制帶來(lái)的性能提升。
(2)select,poll每次調(diào)用都要把fd集合從用戶態(tài)往內(nèi)核態(tài)拷貝一次,并且要把current往設(shè)備等待隊(duì)列中掛一次,而epoll只要一次拷貝,而且把current往等待隊(duì)列上掛也只掛一次(在epoll_wait的開(kāi)始,注意這里的等待隊(duì)列并不是設(shè)備等待隊(duì)列,只是一個(gè)epoll內(nèi)部定義的等待隊(duì)列)。這也能節(jié)省不少的開(kāi)銷。
審核編輯:黃飛
-
服務(wù)器
+關(guān)注
關(guān)注
12文章
9321瀏覽量
86103 -
路由器
+關(guān)注
關(guān)注
22文章
3745瀏覽量
114516 -
epoll
+關(guān)注
關(guān)注
0文章
28瀏覽量
2985 -
select
+關(guān)注
關(guān)注
0文章
28瀏覽量
3957
原文標(biāo)題:select、poll、epoll之間的區(qū)別(搜狗面試)
文章出處:【微信號(hào):LinuxHub,微信公眾號(hào):Linux愛(ài)好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
epoll的使用
我讀過(guò)的最好的epoll講解
epoll使用方法與poll的區(qū)別
揭示EPOLL一些原理性的東西
epoll和select的區(qū)別
![<b class='flag-5'>epoll</b>和<b class='flag-5'>select</b>的<b class='flag-5'>區(qū)別</b>](https://file1.elecfans.com//web2/M00/A6/E2/wKgZomUMQQyAVequAAAwAbKTtKg595.png)
關(guān)于Epoll,你應(yīng)該知道的那些細(xì)節(jié)
詳細(xì)解讀Linux內(nèi)核的poll機(jī)制
![詳細(xì)解讀Linux內(nèi)核的<b class='flag-5'>poll</b>機(jī)制](https://file.elecfans.com/web1/M00/91/C1/o4YBAFzaeqqAIRSwAABEM9lrGKs625.png)
Linux內(nèi)核中select, poll和epoll的區(qū)別
Linux中epoll IO多路復(fù)用機(jī)制
![Linux中<b class='flag-5'>epoll</b> IO多路復(fù)用機(jī)制](https://file.elecfans.com/web1/M00/92/55/pIYBAFzdGiOALNnSAABGDLuF-2Q193.png)
epoll LT和ET方式下的讀寫(xiě)差別
epoll和select使用區(qū)別
![<b class='flag-5'>epoll</b>和<b class='flag-5'>select</b>使用<b class='flag-5'>區(qū)別</b>](https://file1.elecfans.com/web2/M00/AD/37/wKgaomVMeB-AXyaAAAE8133RnsU069.jpg)
epoll底層如何使用紅黑樹(shù)
![<b class='flag-5'>epoll</b>底層如何使用紅黑樹(shù)](https://file1.elecfans.com/web2/M00/AF/49/wKgZomVN1_KAf3VIAAAnrxwSLVE856.jpg)
評(píng)論