epoll和select
相比于select,epoll最大的好處在于它不會隨著監聽fd數目的增長而降低效率。因為在內核中的select實現中,它是采用輪詢來處理的,輪詢的fd數目越多,自然耗時越多。
并且,在linux/posix_types.h頭文件有這樣的聲明:
#define __FD_SETSIZE 1024
表示select最多同時監聽1024個fd,當然,可以通過修改頭文件再重編譯內核來擴大這個數目,但這似乎并不治本。
一、IO多路復用的select
IO多路復用相對于阻塞式和非阻塞式的好處就是它可以監聽多個 socket ,并且不會消耗過多資源。當用戶進程調用 select 時,它會監聽其中所有 socket 直到有一個或多個 socket 數據已經準備好,否則就一直處于阻塞狀態。select的缺點在于單個進程能夠監視的文件描述符的數量存在最大限制,select()所維護的存儲大量文件描述符的數據結構,隨著文件描述符數量的增大,其復制的的開銷也線性增長。同時,由于網絡響應時間的延遲使得大量的tcp鏈接處于非常活躍狀態,但調用select()會對所有的socket進行一次線性掃描,所以這也浪費了一定的開銷。不過它的好處還有就是它的跨平臺特性。
二、 epoll
epoll的ET是必須對非阻塞的socket才能工作,LT對于阻塞的socket也可以
所有I/O多路復用操作都是同步的,涵蓋select/poll。
阻塞/非阻塞是相對于同步I/O來說的,與異步I/O無關。
select/poll/epoll本身是同步的,可以阻塞也可以不阻塞。
(阻塞和非阻塞 與同步不同步不同;阻塞與否 是自身,異步與否是與外部協作的關系)
skater:
無論是阻塞 I/O、非阻塞 I/O,還是基于非阻塞 I/O 的多路復用都是同步調用。因為它們在 read 調用時,內核將數據從內核空間拷貝到應用程序空間(epoll應該是從mmap),過程都是需要等待的,也就是說這個過程是同步的,如果內核實現的拷貝效率不高,read 調用就會在這個同步過程中等待比較長的時間。
epoll事件: EPOLLIN : 表示對應的文件描述符可以讀(包括對端SOCKET正常關閉); EPOLLOUT: 表示對應的文件描述符可以寫; EPOLLPRI: 表示對應的文件描述符有緊急的數據可讀(這里應該表示有帶外數據到來); EPOLLERR: 表示對應的文件描述符發生錯誤; EPOLLHUP: 表示對應的文件描述符被掛斷;
epoll高效的核心是:1、用戶態和內核太共享內存mmap。2、數據到來采用事件通知機制(而不需要輪詢)。
epoll的接口
epoll的接口非常簡單,一共就三個函數:
(1)epoll_create系統調用
epoll_create在C庫中的原型如下。
int epoll_create(int size);
epoll_create返回一個句柄,之后 epoll的使用都將依靠這個句柄來標識。參數 size是告訴 epoll所要處理的大致事件數目。不再使用 epoll時,必須調用 close關閉這個句柄。
注意:size參數只是告訴內核這個 epoll對象會處理的事件大致數目,而不是能夠處理的事件的最大個數。在 Linux最新的一些內核版本的實現中,這個 size參數沒有任何意義。
(2)epoll_ctl系統調用
epoll_ctl在C庫中的原型如下。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
epoll_ctl向 epoll對象中添加、修改或者刪除感興趣的事件,返回0表示成功,否則返回–1,此時需要根據errno錯誤碼判斷錯誤類型。epoll_wait方法返回的事件必然是通過 epoll_ctl添加到 epoll中的。
參數:
epfd: epoll_create返回的句柄,
op:的意義見下表:
EPOLL_CTL_ADD:注冊新的fd到epfd中;
EPOLL_CTL_MOD:修改已經注冊的fd的監聽事件;
EPOLL_CTL_DEL:從epfd中刪除一個fd;
fd:需要監聽的socket句柄fd,
event:告訴內核需要監聽什么事的結構體,struct epoll_event結構如下:
epoll_data_t; struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ };
__uint32_t events就要監聽的事件(感興趣的事件):
EPOLLIN :表示對應的文件描述符可以讀(包括對端SOCKET正常關閉);
EPOLLOUT:表示對應的文件描述符可以寫;
EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(這里應該表示有帶外數據到來);
EPOLLERR:表示對應的文件描述符發生錯誤;
EPOLLHUP:表示對應的文件描述符被掛斷;
EPOLLET: 將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對于水平觸發(Level Triggered)來說的。
EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之后,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列里
data成員是一個epoll_data聯合,其定義如下: typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; 可見,這個 data成員還與具體的使用方式相關。例如,ngx_epoll_module模塊只使用了聯合中的 ptr成員, 作為指向 ngx_connection_t連接的指針。我們在項目中一般使用的也是 ptr成員,因為它可以指向任意的結構 體地址。
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
epoll_wait在C庫中的原型如下:
int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout);
收集在 epoll監控的事件中已經發生的事件,如果 epoll中沒有任何一個事件發生,則最多等待timeout毫秒后返回。epoll_wait的返回值表示當前發生的事件個數,如果返回0,則表示本次調用中沒有事件發生,如果返回–1,則表示出現錯誤,需要檢查 errno錯誤碼判斷錯誤類型。
epfd:epoll的描述符。
events:分配好的 epoll_event結構體數組,epoll將會把發生的事件復制到 events數組中(events不可以是空指針,內核只負責把數據復制到這個 events數組中,不會去幫助我們在用戶態中分配內存。內核這種做法效率很高)。
maxevents:表示本次可以返回的最大事件數目,通常 maxevents參數與預分配的events數組的大小是相等的。
timeout:表示在沒有檢測到事件發生時最多等待的時間(單位為毫秒),如果 timeout為0,則表示 epoll_wait在 rdllist鏈表中為空,立刻返回,不會等待。
epoll有兩種工作模式:LT(水平觸發)模式和ET(邊緣觸發)模式。
默認情況下,epoll采用 LT模式工作,這時可以處理阻塞和非阻塞套接字,而上表中的 EPOLLET表示可以將一個事件改為 ET模式。ET模式的效率要比 LT模式高,它只支持非阻塞套接字。
(水平觸發LT:當被監控的文件描述符上有可讀寫事件發生時,epoll_wait()會通知處理程序去讀寫。如果這次沒有把數據一次性全部讀寫完(如讀寫緩沖區太小),那么下次調用 epoll_wait()時,它還會通知你在上次沒讀寫完的文件描述符上繼續讀寫
邊緣觸發ET:當被監控的文件描述符上有可讀寫事件發生時,epoll_wait()會通知處理程序去讀寫。如果這次沒有把數據全部讀寫完(如讀寫緩沖區太小),那么下次調用epoll_wait()時,它不會通知你,也就是它只會通知你一次,直到該文件描述符上出現第二次可讀寫事件才會通知你
此可見,水平觸發時如果系統中有大量你不需要讀寫的就緒文件描述符,而它們每次都會返回,這樣會大大降低處理程序檢索自己關心的就緒文件描述符的效率,而邊緣觸發,則不會充斥大量你不關心的就緒文件描述符,從而性能差異,高下立見。)
如何來使用epoll
1、包含一個頭文件#include
2、create_epoll(int maxfds)來創建一個epoll的句柄,其中maxfds為你epoll所支持的最大句柄數。這個函數會返回一個新的epoll句柄,之后的所有操作將通過這個句柄來進行操作。在用完之后,記得用close()來關閉這個創建出來的epoll句柄。
3、之后在你的網絡主循環里面,每一幀的調用epoll_wait(int epfd, epoll_event events, int max events, int timeout)來查詢所有的網絡接口,看哪一個可以讀,哪一個可以寫了。基本的語法為:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd為用epoll_create創建之后的句柄,events是一個epoll_event*的指針,當epoll_wait這個函數操作成功之后,epoll_events里面將儲存所有的讀寫事件。max_events是當前需要監聽的所有socket句柄數。
最后一個timeout:是epoll_wait的超時,
為0的時候表示馬上返回,
為-1的時候表示一直等下去,直到有事件返回,
為任意正整數的時候表示等這么長的時間,如果一直沒有事件,則返回。
一般如果網絡主循環是單獨的線程的話,可以用-1來等,這樣可以保證一些效率,如果是和主邏輯在同一個線程的話,則可以用0來保證主循環的效率。
epoll_wait范圍之后應該是一個循環,遍利所有的事件。
epoll通過在Linux內核中申請一個簡易的文件系統(文件系統一般用什么數據結構實現?B+樹)。把原先的select/poll調用分成了3個部分:
1)調用epoll_create()建立一個epoll對象(在epoll文件系統中為這個句柄對象分配資源)
2)調用epoll_ctl向epoll對象中添加這100萬個連接的套接字
3)調用epoll_wait收集發生的事件的連接
epoll程序框架
幾乎所有的epoll程序都使用下面的框架:
偽代碼:
listenfd為全局變量,服務端監聽的套接字的fd。
關于epoll_wait返回值的一個簡單測試 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) {/*有數據可讀*/ } else if (events[i].events & EPOLLOUT) {/*有數據可寫*/ } else {/*出錯*/ } } } } 通過測試發現epoll_wait返回值number是不會大于MAX_EVENT_NUMBER的。 測試過程中,連接的客戶端數遠大于MAX_EVENT_NUMBER,由此可以推論:epoll_wait()每次返回的是活躍客戶端的個數,每次并將這些活躍的客戶端信息加入到events[MAX_EVENT_NUMBER]。 由此可見,活躍客戶端的個數相同的情況下,events[MAX_EVENT_NUMBER]越大,epoll_wait()函數執行次數越少,但是events[MAX_EVENT_NUMBER]越大越消耗存儲資源。 所以,MAX_EVENT_NUMBER的選擇應該在效率和資源間取一個平衡點。
示例代碼
for( ; ; ) { nfds = epoll_wait(epfd,events,20,500); for(i=0;ifd; send( sockfd, md->ptr, strlen((char*)md->ptr), 0 ); //發送數據 ev.data.fd=sockfd; ev.events=EPOLLIN|EPOLLET; epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改標識符,等待下一個循環時接收數據 } else { //其他的處理 } } }
大致流程
struct epoll_event ev, event_list[EVENT_MAX_COUNT];//ev用于注冊事件,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; //設置與要處理的事件相關的文件描述符 ev.events = EPOLLIN | EPOLLET; //設置要處理的事件類型EPOLLIN :表示對應的文件描述符可以讀,EPOLLET狀態變化才通知 epfd = epoll_create(256); //生成用于處理accept的epoll專用的文件描述符 //注冊epoll事件 epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); //epfd epoll實例ID,EPOLL_CTL_ADD添加,listenfd:socket,ev事件(監聽listenfd) nfds = epoll_wait(epfd, event_list, EVENT_MAX_COUNT, TIMEOUT_MS); //等待epoll事件的發生
1. 首先熟悉下epoll的三個接口
int epoll_create(int size);
創建epoll相關數據結構,其最重要的是
1. 紅黑樹, 用于存儲需要監控的文件句柄以及事件
2. 就緒鏈表,用于存儲被觸發的文件句柄以及事件
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);
阻塞等待timeout時間,如果文件句柄上相關事件被觸發,則epoll_wait退出,并將觸發的事件 寫入出參 events參數,觸發的事件個數作為返回值返回
2. 如何使用這三個接口寫一個server
首先使用epoll_create 創建epoll相關數據結構
其次創建TCP socket 文件句柄acceptfd,綁定(ip:port),然后開啟監聽,并使用epoll_ctl 注冊到epoll中,監聽acceptfd句柄的EPOLL_IN事件(即可讀事件)
調用epoll_wait 開始進行阻塞等待
如果有客戶端連接過來,則觸發acceptfd上的EPOLL_IN事件,epoll_wait返回后,可以得到觸發事件的信息, 這些信息其實就是一個struct epoll_event對象, 我們可以判斷這個epoll event對象fd是否和acceptfd一致,如果一致在認為有新連接進來,則獲得新連接對應的clientfd, 并使用epoll_ctl注冊到epoll, 監控clientfd上的epoll_in事件,這個時候這個客戶端和服務器的連接就建立了
typedef union epoll_data { void *ptr; int fd; //可以用fd, 也可以用ptr來保存事件對應的文件句柄 uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ };
用戶在客戶端輸入命令,將會觸發服務器端 clientfd上的epoll_in事件,epoll_wait返回觸發的event, 讀取event對應的clientfd內核緩沖區中的數據,解析協議,執行命令,得到返回結果,這個返回結果要返回給客戶端,則再使用epoll_ctl注冊clientfd的epoll_out事件到epoll,這個時候,我們會注意到clientfd上既有epoll_in,也有epoll_out,這樣其實沒有必要,客戶端在這個時候等待返回結果,不會再輸入命令,所以需要使用epoll_ctl把epoll_out刪除掉
如果clientfd內核緩沖區可寫,epoll_wait這個時候會返回,并返回epoll_out事件,此時把返回的結果數據寫入clientfd, 返回給客戶端
實例源碼
原文有相當多的如錯誤:
需要增加到頭文件和錯誤修改
//'/0'->'' //bzero() 替換為memset (注意二者參數不一樣,bzero將前n個字節設為0,memset將前n 個字節的值設為值 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 (注意二者參數不一樣,bzero將前n個字節設為0,memset將前n 個字節的值設為值 c) //local_addr 由char* 改為 string #include #include //atoi #include //memset #include //std:cout 等 using namespace std; #define MAXLINE 255 //讀寫緩沖 #define OPEN_MAX 100 #define LISTENQ 20 //listen的第二個參數 定義TCP鏈接未完成隊列的大小(linux >2.6 則表示accpet之前的隊列) #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()函數根據指定的format(格式)(格式)發送信息(參數)到由stream(流)指定的文件 //printf 將內容發送到Default的輸出設備,通常為本機的顯示器,fprintf需要指定輸出設備,可以為文件,設備。 //stderr return 1; } } else { fprintf(stderr, "Usage:%s portnumber/r/n", argv[0]); return 1; } //聲明epoll_event結構體的變量,ev用于注冊事件,數組用于回傳要處理的事件 struct epoll_event ev, event_list[EVENT_MAX_COUNT]; //生成用于處理accept的epoll專用的文件描述符 epfd = epoll_create(256); //生成epoll文件描述符,既在內核申請一空間,存放關注的socket fd上是否發生以及發生事件。size既epoll fd上能關注的最大socket fd數。隨你定好了。只要你有空間。 struct sockaddr_in clientaddr; socklen_t clilenaddrLen; struct sockaddr_in serveraddr; listenfd = socket(AF_INET, SOCK_STREAM, 0);//Unix/Linux“一切皆文件”,創建(套接字)文件,id=listenfd if (listenfd < 0) { printf("socket error,errno %d:%s ",errno,strerror(errno)); } //把socket設置為非阻塞方式 //setnonblocking(listenfd); //設置與要處理的事件相關的文件描述符 ev.data.fd = listenfd; //設置要處理的事件類型 ev.events = EPOLLIN | EPOLLET; //EPOLLIN :表示對應的文件描述符可以讀,EPOLLET狀態變化才通知 //ev.events=EPOLLIN; //注冊epoll事件 epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); //epfd epoll實例ID,EPOLL_CTL_ADD添加,listenfd:socket,ev事件(監聽listenfd) memset(&serveraddr, 0, sizeof(serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_addr.s_addr=htonl(INADDR_ANY); /*IP,INADDR_ANY轉換過來就是0.0.0.0,泛指本機的意思,也就是表示本機的所有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事件的發生 nfds = epoll_wait(epfd, event_list, EVENT_MAX_COUNT, TIMEOUT_MS); //epoll_wait(int epfd, struct epoll_event * event_list, int maxevents, int timeout),返回需要處理的事件數目 //處理所發生的所有事件 for(i = 0; i < nfds; ++i) { if(event_list[i].data.fd == listenfd) //如果新監測到一個SOCKET用戶連接到了綁定的SOCKET端口,建立新的連接。 { clilenaddrLen = sizeof(struct sockaddr_in);//在調用accept()前,要給addrLen賦值,這樣才不會出錯,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);//將一個32位網絡字節序的二進制IP地址轉換成相應的點分十進制的IP地址 cout << "accapt a connection from " << str << endl; //設置用于讀操作的文件描述符 ev.data.fd = connfd; //設置用于注測的讀操作事件 ev.events = EPOLLIN | EPOLLET; //ev.events=EPOLLIN; //注冊ev epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev); //將accpet的句柄添加進入(增加監聽的對象) } else if(event_list[i].events & EPOLLIN) //如果是已經連接的用戶,并且收到數據,那么進行讀入。 { cout << "EPOLLIN" << endl; if ( (sockfd = event_list[i].data.fd) < 0) continue; if ( (n = read(sockfd, line_buff, MAXLINE)) < 0) //read時fd中的數據如果小于要讀取的數據,就會引起阻塞? { //當read()或者write()返回-1時,一般要判斷errno if (errno == ECONNRESET)//與客戶端的Socket被客戶端強行被斷開,而服務器還企圖read { close(sockfd); event_list[i].data.fd = -1; } else std::cout << "readline error" << std::endl; } else if (n == 0) //返回的n為0時,說明客戶端已經關閉 { close(sockfd); event_list[i].data.fd = -1; } line_buff[n] = ''; cout << "read " << line_buff << endl; //設置用于寫操作的文件描述符 ev.data.fd = sockfd; //設置用于注測的寫操作事件 ev.events = EPOLLOUT | EPOLLET; //EPOLLOUT:表示對應的文件描述符可以寫; //修改sockfd上要處理的事件為EPOLLOUT //epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); } else if(event_list[i].events & EPOLLOUT) // 如果有數據發送 { sockfd = event_list[i].data.fd; write(sockfd, line_buff, n); //設置用于讀操作的文件描述符 ev.data.fd = sockfd; //設置用于注測的讀操作事件 ev.events = EPOLLIN | EPOLLET; //修改sockfd上要處理的事件為EPOLIN epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev); } } } return 0; }
編譯命令
linux下編譯:g++ epoll.cpp -o epoll
命令行簡單測試
curl 192.168.0.250:5000 -d "phone=123456789&name=Hwei"
相關知識
如何動態的改變listen監聽的個數呢?
如果指定值在源代碼中是一個常值,那么增長其大小需要重新編譯服務器程序。那么,我們可以為它設定一個缺省值,不過允許通過命令行選項或者環境變量來覆寫該值。
void Listen(int fd, int backlog) { char *ptr; if((ptr = getenv("LISTENQ")) != NULL) backlog = atoi(ptr); if(listen(fd, backlog) < 0) printf("listen error "); }
隊列已滿的情況,如何處理?
當一個客戶SYN到達時,若這個隊列是滿的,TCP就忽略該分節,也就是不會發送RST。
這么做的原因在于,隊列已滿的情況是暫時的,客戶TCP如果沒收收到RST,就會重發SYN,在隊列有空閑的時候處理該請求。如果服務器TCP立即響應一個RST,客戶的connect調用就會立即返回一個錯誤,強制應用進程處理這種情況,而不會再次重發SYN。而且客戶端也不無區別該套接口的狀態,是“隊列已滿”還是“該端口沒有在監聽”。
SYN泛濫攻擊
向某一目標服務器發送大量的SYN,用以填滿一個或多個TCP端口的未完成隊列。每個SYN的源IP地址都置成隨機數(IP欺騙),這樣防止攻擊服務器獲悉黑客的真實IP地址。通過偽造的SYN裝滿未完成連接隊列,使得合法的SYN不能排上隊,導致針對合法用戶的服務被拒絕。
防御方法:
針對服務器主機的方法。增加連接緩沖隊列長度和縮短連接請求占用緩沖隊列的超時時間。該方式最簡單,被很多操作系統采用,但防御性能也最弱。
針對路由器過濾的方法。由于DDoS攻擊,包括SYN-Flood,都使用地址偽裝技術,所以在路由器上使用規則過濾掉被認為地址偽裝的包,會有效的遏制攻擊流量。
針對防火墻的方法。在SYN請求連接到真正的服務器之前,使用基于防火墻的網關來測試其合法性。它是一種被普遍采用的專門針對SYN-Flood攻擊的防御機制。
SYN:同步序列編號(Synchronize Sequence Numbers)
它們的含義是: SYN表示建立連接, FIN表示關閉連接, ACK表示響應, PSH表示有 DATA數據傳輸, RST表示連接重置。
SYN(synchronous建立聯機)
ACK(acknowledgement 確認)
PSH(push傳送)
FIN(finish結束)
RST(reset重置)
URG(urgent緊急)
Sequence number(順序號碼)
Acknowledge number(確認號碼)
三次握手:
在TCP/IP協議中,TCP協議提供可靠的連接服務,采用三次握手建立一個連接。 第一次握手:建立連接時,客戶端發送syn包(syn=j)到服務器,并進入SYN_SEND狀態,等待服務器確認; 第二次握手:服務器收到syn包,必須確認客戶的SYN(ack=j+1),同時自己也發送一個SYN包(syn=k),即SYN+ACK包,此時服務器進入SYN_RECV狀態; 第三次握手:客戶端收到服務器的SYN+ACK包,向服務器發送確認包ACK(ack=k+1),此包發送完畢,客戶端和服務器進入ESTABLISHED狀態,完成三次握手。完成三次握手,客戶端與服務器開始傳送數據.
第一次握手:主機A發送位碼為syn=1,隨機產生seq number=1234567的數據包到服務器,主機B由SYN=1知道,A要求建立聯機;
第二次握手:主機B收到請求后要確認聯機信息,向A發送ack number=(主機A的seq+1),syn=1,ack=1,隨機產生seq=7654321的包;
第三次握手:主機A收到后檢查ack number是否正確,即第一次發送的seq number+1,以及位碼ack是否為1,若正確,主機A會再發送ack number=(主機B的seq+1),ack=1,主機B收到后確認seq值與ack=1則連接建立成功。
實例代碼二
多進程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+個鏈接的測試代碼
#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; }
關于ET、LT兩種工作模式
水平觸發LT:
其中LT就是與select和poll類似,當被監控的文件描述符上有可讀寫事件發生時,epoll_wait()會通知處理程序去讀寫。如果這次沒有把數據一次性全部讀寫完(如讀寫緩沖區太小),那么下次調用 epoll_wait()時,它還會通知你在上次沒讀寫完的文件描述符上繼續讀寫
邊緣觸發ET:
當被監控的文件描述符上有可讀寫事件發生時,epoll_wait()會通知處理程序去讀寫。如果這次沒有把數據全部讀寫完(如讀寫緩沖區太小),那么下次調用epoll_wait()時,它不會通知你,也就是它只會通知你一次,直到該文件描述符上出現第二次可讀寫事件才會通知你
水平觸發:只要緩沖區有數據就會一直觸發
邊沿觸發:只有在緩沖區增加數據的那一刻才會觸發
由此可見,水平觸發時如果系統中有大量你不需要讀寫的就緒文件描述符(有些fd有數據,但是你不處理那些fd),而它們每次都會返回,這樣會大大降低處理程序檢索自己關心的就緒文件描述符的效率,而邊緣觸發,則不會充斥大量你不關心的就緒文件描述符,從而性能差異,高下立見。
4、關于ET、LT兩種工作模式
可以得出這樣的結論:
ET模式僅當狀態發生變化的時候才獲得通知,這里所謂的狀態的變化并不包括緩沖區中還有未處理的數據,也就是說,如果要采用ET模式,需要一直read/write直到出錯為止,很多人反映為什么采用ET模式只接收了一部分數據就再也得不到通知了,大多因為這樣;而LT模式是只要有數據沒有處理就會一直通知下去的.
epoll中讀寫數據 的注意事項
在一個非阻塞的socket上調用read/write函數,返回EAGAIN或者EWOULDBLOCK(注:EAGAIN就是EWOULDBLOCK)。
從字面上看,意思是:
EAGAIN: 再試一次
EWOULDBLOCK:如果這是一個阻塞socket, 操作將被block
perror輸出:Resource temporarily unavailable
總結:
這個錯誤表示資源暫時不夠,可能read時, 讀緩沖區沒有數據, 或者write時,寫緩沖區滿了。
遇到這種情況,如果是阻塞socket、 read/write就要阻塞掉。而如果是非阻塞socket、 read/write立即返回-1, 同 時errno設置為EAGAIN。
所以對于阻塞socket、 read/write返回-1代表網絡出錯了。但對于非阻塞socket、read/write返回-1不一定網絡真的出錯了。可能是Resource temporarily unavailable。這時你應該再試,直到Resource available。
本文主要講述epoll模型(不完全是針對epoll)下讀寫數據接口使用的注意事項
1、read write
函數原型如下:
#includessize_t read(int filedes, void* buf, size_t nbytes) ssize_t write(int filedes, const void* buf, size_t nbytes)
其中,read返回實際讀取到的字節數。但實際讀取的字節很有可能少于指定要讀取的字節數nbytes。因此會分為:
①返回值大于0。 讀取正常,返回實際讀取到的字節數
②返回值等于0。 讀取異常,讀取到文件filedes結尾處了。這里邏輯上要理解為read已經讀取完數據
③返回值小于0(-1)。 讀取出錯,在處理網絡請求時可能是網絡異常。著重注意當返回-1,此時errno的值EAGAIN、EWOULLDBLOCK,表示內核對應的讀緩沖區為空
而write返回的實際寫入字節數正常情況是與制定寫入的字節數nbytes相同的,不相等說明寫入異常了,著重注意,此時errno的值EAGAIN、EWOULLDBLOCK,表示內核對應的寫緩沖區為空。注,EAGAIN等同于EWOULLDBLOCK。
總之,這個錯誤表示資源暫時不夠,可能read時讀緩沖區沒有數據, 或者write時寫緩沖區滿了。遇到這種情況,如果是阻塞socket、 read/write就要阻塞掉。而如果是非阻塞socket、 read/write立即返回-1, 同時errno設置為EAGAIN。
所以對于阻塞socket、 read/write返回-1代表網絡出錯了。但對于非阻塞socket、read/write返回-1不一定網絡真的出錯了。可能支持緩沖區空或者滿,這時應該再試,直到Resource available。
綜上,對于非阻塞的socket,正確的讀寫操作為:
LT模式
讀: 忽略掉errno = EAGAIN的錯誤,下次繼續讀;
寫:忽略掉errno = EAGAIN的錯誤,下次繼續寫。
對于select和epoll的LT模式,這種讀寫方式是沒有問題的。但對于epoll的ET模式,這種方式還有漏洞。
下面來介紹下epoll事件的兩種模式LT(水平觸發)和ET(邊沿觸發),根據可以理解為,文件描述符的讀寫狀態發生變化才會觸發epoll事件,具體說來如下:二者的差異在于 level-trigger 模式下只要某個 socket 處于 readable/writable 狀態,無論什么時候進行 epoll_wait 都會返回該 socket;而 edge-trigger 模式下只有某個 socket 從 unreadable 變為 readable,或從unwritable 變為writable時,epoll_wait 才會返回該 socket。如下兩個示意圖:
從socket讀數據:
往socket寫數據:
所以在epoll的ET模式下,正確的讀寫方式為:
讀: 只要可讀, 就一直讀,直到返回0,或者 errno = EAGAIN寫:只要可寫, 就一直寫,直到數據發送完,或者 errno = EAGAIN
這里的意思是,對于ET模式,相當于我們要自己重寫read和write,使其像”原子操作“一樣,保證一次read 或 write能夠完整的讀完緩沖區的數據或者寫完要寫入緩沖區的數據。因此,實現為用while包住read和write即可。但是對于select或者LT模式,我們可以只使用一次read和write,因為在主程序中會一直while,而事件再下一次select時還會被獲取到。但也可以實現為用while包住read和write。從邏輯上講,一次性把數據讀取完整可以保證數據的完整性。
下面來說明這種”原子操作“read和write
int n = 0; while(1) { nread = read(fd, buf + n, BUFSIZ - 1); //讀時,用戶進程指定的接收數據緩沖區大小固定,一般要比數據大 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);//寫時,數據大小一直在變化 if(nwrite < data_size) { if(errno == EAGAIN || errno == EWOULDBLOCK) { continue; } else { break;//or return; } } else { n += nwrite; data_size -= nwrite; } }
正確的accept,accept 要考慮 2 個問題:
(1) LT模式下或ET模式下,阻塞的監聽socket, accept 存在的問題
accept每次都是從已經完成三次握手的tcp隊列中取出一個連接,考慮這種情況: TCP 連接被客戶端夭折,即在服務器調用 accept 之前,客戶端主動發送 RST 終止連接,導致剛剛建立的連接從就緒隊列中移出,如果套接口被設置成阻塞模式,服務器就會一直阻塞在 accept 調用上,直到其他某個客戶建立一個新的連接為止。但是在此期間,服務器單純地阻塞在accept 調用上,就緒隊列中的其他描述符都得不到處理。
解決辦法是:把監聽套接口設置為非阻塞,當客戶在服務器調用 accept 之前中止某個連接時,accept 調用可以立即返回 -1, 這時源自 Berkeley 的實現會在內核中處理該事件,并不會將該事件通知給 epoll,而其他實現把 errno 設置為 ECONNABORTED 或者 EPROTO 錯誤,我們應該忽略這兩個錯誤。
(2) ET 模式下 accept 存在的問題
考慮這種情況:多個連接同時到達,服務器的 TCP 就緒隊列瞬間積累多個就緒連接,由于是邊緣觸發模式,epoll 只會通知一次,accept 只處理一個連接,導致 TCP 就緒隊列中剩下的連接都得不到處理。
解決辦法是:將監聽套接字設置為非阻塞模式,用 while 循環抱住 accept 調用,處理完 TCP 就緒隊列中的所有連接后再退出循環。如何知道是否處理完就緒隊列中的所有連接呢? accept 返回 -1 并且 errno 設置為 EAGAIN 就表示所有連接都處理完。
綜合以上兩種情況,服務器應該使用非阻塞地 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"); }
一道騰訊后臺開發的面試題:
使用Linux epoll模型,水平觸發模式;當socket可寫時,會不停的觸發 socket 可寫的事件,如何處理?
第一種最普遍的方式:
需要向 socket 寫數據的時候才把 socket 加入 epoll ,等待可寫事件。接受到可寫事件后,調用 write 或者 send 發送數據。當所有數據都寫完后,把 socket 移出 epoll。
這種方式的缺點是,即使發送很少的數據,也要把 socket 加入 epoll,寫完后在移出 epoll,有一定操作代價。
一種改進的方式:
開始不把 socket 加入 epoll,需要向 socket 寫數據的時候,直接調用 write 或者 send 發送數據。如果返回 EAGAIN,把 socket 加入 epoll,在 epoll 的驅動下寫數據,全部數據發送完畢后,再移出 epoll。
這種方式的優點是:數據不多的時候可以避免 epoll 的事件處理,提高效率。
多路復用
如文初的說明表示,這三者都是 I/O 多路復用機制,且簡要介紹了多路復用的定義,那么如何更加直觀地了解多路復用呢?這里有張圖:
對于網頁服務器 Nginx 來說,會有很多連接進來, epoll 會把他們都監視起來,然后像撥開關一樣,誰有數據就撥向誰,然后調用相應的代碼處理。
一般來說以下場合需要使用 I/O 多路復用:
當客戶處理多個描述字時(一般是交互式輸入和網絡套接口)
如果一個服務器既要處理 TCP,又要處理 UDP,一般要使用 I/O 復用
如果一個 TCP 服務器既要處理監聽套接口,又要處理已連接套接口
為什么epoll可以支持百萬級別的連接?
在server的處理過程中,大家可以看到其中重要的操作是,使用epoll_ctl修改clientfd在epoll中注冊的epoll_event, 這個操作首先在紅黑樹中找到fd對應的epoll_event, 然后進行修改,紅黑樹是典型的二叉平衡樹,其時間復雜度是log2(n), 1百萬的文件句柄,只需要16次左右的查找,速度是非常快的,支持百萬級別毫無壓力
另外,epoll通過注冊fd上的回調函數,回調函數監控到有事件發生,則準備好相關的數據放到到就緒鏈表里面去,這個動作非常快,成本也非常小
socket讀寫返回值的處理
在調用socket讀寫函數read(),write()時,都會有返回值。如果沒有正確處理返回值,就可能引入一些問題
總結了以下幾點
1當read()或者write()函數返回值大于0時,表示實際從緩沖區讀取或者寫入的字節數目
2當read()函數返回值為0時,表示對端已經關閉了 socket,這時候也要關閉這個socket,否則會導致socket泄露。netstat命令查看下,如果有closewait狀態的socket,就是socket泄露了
當write()函數返回0時,表示當前寫緩沖區已滿,是正常情況,下次再來寫就行了。
3當read()或者write()返回-1時,一般要判斷errno
如果errno == EINTR,表示系統當前中斷了,直接忽略
如果errno == EAGAIN或者EWOULDBLOCK,非阻塞socket直接忽略;如果是阻塞的socket,一般是讀寫操作超時了,還未返回。這個超時是指socket的SO_RCVTIMEO與SO_SNDTIMEO兩個屬性。所以在使用阻塞socket時,不要將超時時間設置的過小。不然返回了-1,你也不知道是socket連接是真的斷開了,還是正常的網絡抖動。一般情況下,阻塞的socket返回了-1,都需要關閉重新連接。
4.另外,對于非阻塞的connect,可能返回-1.這時需要判斷errno,如果 errno == EINPROGRESS,表示正在處理中,否則表示連接出錯了,需要關閉重連。之后使用select,檢測到該socket的可寫事件時,要判斷getsockopt(c->fd, SOL_SOCKET, SO_ERROR, &err, &errlen),看socket是否出錯了。如果err值為0,則表示connect成功;否則也應該關閉重連
5 在使用epoll時,有ET與LT兩種模式。ET模式下,socket需要read或者write到返回-1為止。對于非阻塞的socket沒有問題,但是如果是阻塞的socket,正如第三條中所說的,只有超時才會返回。所以在ET模式下千萬不要使用阻塞的socket。那么LT模式為什么沒問題呢?一般情況下,使用LT模式,我們只要調用一次read或者write函數,如果沒有讀完或者沒有寫完,下次再來就是了。由于已經返回了可讀或者可寫事件,所以可以保證調用一次read或者write會正常返回。
nread為-1且errno==EAGAIN,說明數據已經讀完,設置EPOLLOUT。
網絡狀態查詢命令
sar、iostat、lsof
問題記錄
客戶端
1、Cannot assign requested address
大致上是由于客戶端頻繁的連服務器,由于每次連接都在很短的時間內結束,導致很多的TIME_WAIT,以至于用光了可用的端 口號,所以新的連接沒辦法綁定端口,即“Cannot assign requested address”。是客戶端的問題不是服務器端的問題。通過netstat,的確看到很多TIME_WAIT狀態的連接。
client端頻繁建立連接,而端口釋放較慢,導致建立新連接時無可用端口。
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 ……
解決辦法
執行命令修改如下內核參數 (需要root權限)
調低端口釋放后的等待時間,默認為60s,修改為15~30s:
sysctl -w net.ipv4.tcp_fin_timeout=30
修改tcp/ip協議配置, 通過配置/proc/sys/net/ipv4/tcp_tw_resue, 默認為0,修改為1,釋放TIME_WAIT端口給新連接使用:
sysctl -w net.ipv4.tcp_timestamps=1
修改tcp/ip協議配置,快速回收socket資源,默認為0,修改為1:
sysctl -w net.ipv4.tcp_tw_recycle=1
允許端口重用:
sysctl -w net.ipv4.tcp_tw_reuse = 1
2、2.8萬左右的鏈接,報錯誤(可能端口用盡)
如果沒有TIME_WAIT 狀態的連接,那有可能端口用盡,特別是長連接的時候,查看開放的端口范圍:
[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萬,長連接把端口用光了。修改端口開放范圍:
vi /etc/sysctl.conf net.ipv4.ip_local_port_range = 10000 65535
執行sysctl -p 使得生效
服務端
影響鏈接不往上走的原因:
1、端口用完了 (客戶端,
查看端口范圍sysctl -a |grep port_range,返回:
net.ipv4.ip_local_port_range = 32768 60999,所以可用端口是60999-32768 =2.8w )
2、文件fd用完了
3、內存用完了
4、網絡
5、配置:fsfile-max = 1048576 #文件fd的最大值,
fd=open(),fd從3開始,0:stdin標準輸入1:stdout標準輸出 2:stderr錯誤輸出
Epoll 難以解決的問題
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 還是要從這個api說起,這個api 可以監聽很多個fd,但是timeout 只有一個。 有這一個場景: 你想關注 fd == 10的這個描述符,你希望該描述符有數據到來的時候通知你,并且,如果一直沒有數據來,那么希望20s之后能通知你。 你改怎么做?
進一步假設,你希望關注 100 個fd, 并希望這個一百個fd,從他們加入監聽隊列的時候開始計算,20s后通知你超時
如果使用這個epoll_wait的話,是不是要自己記住所有fd的剩余超時時間呢?
libevent就解決了這個困擾。
配置和調試
net.ipv4.tcp_syncookies = 1 表示開啟SYN Cookies。當出現SYN等待隊列溢出時,啟用cookies來處理,可防范少量SYN攻擊,默認為0,表示關閉;
net.ipv4.tcp_tw_reuse = 1 表示開啟重用。允許將TIME-WAIT sockets重新用于新的TCP連接,默認為0,表示關閉;
net.ipv4.tcp_tw_recycle = 1 表示開啟TCP連接中TIME-WAIT sockets的快速回收,默認為0,表示關閉。
net.ipv4.tcp_fin_timeout 修改系默認的 TIMEOUT 時間
優化time_wait為啥要開syncookie呢? syncookie可以繞過seq queue的限制,跟優化time_wait沒有關系
另外得打開timestamps,才能讓reuse、recycle生效。
修改fin_timeout,如何調?調大調小呢?
//測試數據
單線程,峰值處理鏈接,貌似目前測試條件可以測試到2.9K
CONNECT: 2.9K/s
QPS : 27.6萬
5萬的鏈接。
QPS : 1.9萬
客戶端、服務端支持多少連接
客戶端
現在我們終于可以得出更為正確的結論了,對于有1個Ip的客戶端來說,受限于ip_local_port_range參數,也受限于65535。但單Linux可以配置多個ip,有幾個ip,最大理論值就翻幾倍
多張網卡不是必須的。即使只有一張網卡,也可以配置多ip。k8s就是這么干的,在k8s里,一臺物理機上可以部署多個pod。但每一個pod都會被分配一個獨立的ip,所以完全不用擔心物理機上部署了過多的pod而影響你用的pod里的TCP連接數量。在ip給你的那一刻,你的pod就和其它應用隔離開了。
服務器端
一條TCP連接如果不發送數據的話,消耗內存是3.3K左右。如果有數據發送,需要為每條TCP分配發送緩存區,大小受你的參數net.ipv4.tcp_wmem配置影響,默認情況下最小是4K。如果發送結束,緩存區消耗的內存會被回收。
假設你只保持連接不發送數據,那么你服務器可以建立的連接最大數量 = 你的內存/3.3K。假如是4GB的內存,那么大約可接受的TCP連接數量是100萬左右。
這個例子里,我們考慮的前提是在一個進程下hold所有的服務器端連接。而在實際中的項目里,為了收發數據方便,很多網絡IO模型還會為TCP連接再創建一個線程或協程。拿最輕量的golang來說,一個協程棧也需要2KB的內存開銷。
結論
TCP連接的客戶端機:每一個ip可建立的TCP連接理論受限于ip_local_port_range參數,也受限于65535。但可以通過配置多ip的方式來加大自己的建立連接的能力。
TCP連接的服務器機:每一個監聽的端口雖然理論值很大,但這個數字沒有實際意義。最大并發數取決你的內存大小,每一條靜止狀態的TCP連接大約需要吃3.3K的內存。
select、poll、epoll之間的區別(搜狗面試)
(1)select==>時間復雜度O(n)
它僅僅知道了,有I/O事件發生了,卻并不知道是哪那幾個流(可能有一個,多個,甚至全部),我們只能無差別輪詢所有流,找出能讀出數據,或者寫入數據的流,對他們進行操作。所以select具有O(n)的無差別輪詢復雜度,同時處理的流越多,無差別輪詢時間就越長。
(2)poll==>時間復雜度O(n)
poll本質上和select沒有區別,它將用戶傳入的數組拷貝到內核空間,然后查詢每個fd對應的設備狀態, 但是它沒有最大連接數的限制,原因是它是基于鏈表來存儲的.
(3)epoll==>時間復雜度O(1)
epoll可以理解為event poll,不同于忙輪詢和無差別輪詢,epoll會把哪個流發生了怎樣的I/O事件通知我們。所以我們說epoll實際上是事件驅動(每個事件關聯上fd)的,此時我們對這些流的操作都是有意義的。(復雜度降低到了O(1))
select,poll,epoll都是IO多路復用的機制。I/O多路復用就通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。但select,poll,epoll本質上都是同步I/O,因為他們都需要在讀寫事件就緒后自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。
epoll跟select都能提供多路I/O復用的解決方案。在現在的Linux內核里有都能夠支持,其中epoll是Linux所特有,而select則應該是POSIX所規定,一般操作系統均有實現
select:
select本質上是通過設置或者檢查存放fd標志位的數據結構來進行下一步處理。這樣所帶來的缺點是:
1、 單個進程可監視的fd數量被限制,即能監聽端口的大小有限。
一般來說這個數目和系統內存關系很大,具體數目可以cat /proc/sys/fs/file-max察看。32位機默認是1024個。64位機默認是2048.
2、 對socket進行掃描時是線性掃描,即采用輪詢的方法,效率較低:
當套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成調度,不管哪個Socket是活躍的,都遍歷一遍。這會浪費很多CPU時間。如果能給套接字注冊某個回調函數,當他們活躍時,自動完成相關操作,那就避免了輪詢,這正是epoll與kqueue做的。
3、需要維護一個用來存放大量fd的數據結構,這樣會使得用戶空間和內核空間在傳遞該結構時復制開銷大
poll:
poll本質上和select沒有區別,它將用戶傳入的數組拷貝到內核空間,然后查詢每個fd對應的設備狀態,如果設備就緒則在設備等待隊列中加入一項并繼續遍歷,如果遍歷完所有fd后沒有發現就緒設備,則掛起當前進程,直到設備就緒或者主動超時,被喚醒后它又要再次遍歷fd。這個過程經歷了多次無謂的遍歷。
它沒有最大連接數的限制,原因是它是基于鏈表來存儲的,但是同樣有一個缺點:
1、大量的fd的數組被整體復制于用戶態和內核地址空間之間,而不管這樣的復制是不是有意義。
2、poll還有一個特點是“水平觸發”,如果報告了fd后,沒有被處理,那么下次poll時會再次報告該fd。
epoll:
epoll有EPOLLLT和EPOLLET兩種觸發模式,LT是默認的模式,ET是“高速”模式。LT模式下,只要這個fd還有數據可讀,每次 epoll_wait都會返回它的事件,提醒用戶程序去操作,而在ET(邊緣觸發)模式中,它只會提示一次,直到下次再有數據流入之前都不會再提示了,無 論fd中是否還有數據可讀。所以在ET模式下,read一個fd的時候一定要把它的buffer讀光,也就是說一直讀到read的返回值小于請求值,或者 遇到EAGAIN錯誤。還有一個特點是,epoll使用“事件”的就緒通知方式,通過epoll_ctl注冊fd,一旦該fd就緒,內核就會采用類似callback的回調機制來激活該fd,epoll_wait便可以收到通知。
epoll為什么要有EPOLLET觸發模式?
如果采用EPOLLLT模式的話,系統中一旦有大量你不需要讀寫的就緒文件描述符,它們每次調用epoll_wait都會返回,這樣會大大降低處理程序檢索自己關心的就緒文件描述符的效率.。而采用EPOLLET這種邊沿觸發模式的話,當被監控的文件描述符上有可讀寫事件發生時,epoll_wait()會通知處理程序去讀寫。如果這次沒有把數據全部讀寫完(如讀寫緩沖區太小),那么下次調用epoll_wait()時,它不會通知你,也就是它只會通知你一次,直到該文件描述符上出現第二次可讀寫事件才會通知你!!!這種模式比水平觸發效率高,系統不會充斥大量你不關心的就緒文件描述符
epoll的優點:
1、沒有最大并發連接的限制,能打開的FD的上限遠大于1024(1G的內存上能監聽約10萬個端口);
2、效率提升,不是輪詢的方式,不會隨著FD數目的增加效率下降。只有活躍可用的FD才會調用callback函數;
即Epoll最大的優點就在于它只管你“活躍”的連接,而跟連接總數無關,因此在實際的網絡環境中,Epoll的效率就會遠遠高于select和poll。
3、 內存拷貝,利用mmap()文件映射內存加速與內核空間的消息傳遞;即epoll使用mmap減少復制開銷。
select、poll、epoll 區別總結:
1、支持一個進程所能打開的最大連接數
select
單個進程所能打開的最大連接數有FD_SETSIZE宏定義,其大小是32個整數的大小(在32位的機器上,大小就是3232,同理64位機器上FD_SETSIZE為3264),當然我們可以對進行修改,然后重新編譯內核,但是性能可能會受到影響,這需要進一步的測試。
poll
poll本質上和select沒有區別,但是它沒有最大連接數的限制,原因是它是基于鏈表來存儲的
epoll
雖然連接數有上限,但是很大,1G內存的機器上可以打開10萬左右的連接,2G內存的機器可以打開20萬左右的連接
2、FD劇增后帶來的IO效率問題
select
因為每次調用時都會對連接進行線性遍歷,所以隨著FD的增加會造成遍歷速度慢的“線性下降性能問題”。
poll
同上
epoll
因為epoll內核中實現是根據每個fd上的callback函數來實現的,只有活躍的socket才會主動調用callback,所以在活躍socket較少的情況下,使用epoll沒有前面兩者的線性下降的性能問題,但是所有socket都很活躍的情況下,可能會有性能問題。
3、 消息傳遞方式
select
內核需要將消息傳遞到用戶空間,都需要內核拷貝動作
poll
同上
epoll
epoll通過內核和用戶空間共享一塊內存來實現的。
總結:
綜上,在選擇select,poll,epoll時要根據具體的使用場合以及這三種方式的自身特點。
1、表面上看epoll的性能最好,但是在連接數少并且連接都十分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機制需要很多函數回調。
2、select低效是因為每次它都需要輪詢。但低效也是相對的,視情況而定,也可通過良好的設計改善
今天對這三種IO多路復用進行對比,參考網上和書上面的資料,整理如下:
1、select實現
select的調用過程如下所示:
(1)使用copy_from_user從用戶空間拷貝fd_set到內核空間
(2)注冊回調函數__pollwait
(3)遍歷所有fd,調用其對應的poll方法(對于socket,這個poll方法是sock_poll,sock_poll根據情況會調用到tcp_poll,udp_poll或者datagram_poll)
(4)以tcp_poll為例,其核心實現就是__pollwait,也就是上面注冊的回調函數。
(5)__pollwait的主要工作就是把current(當前進程)掛到設備的等待隊列中,不同的設備有不同的等待隊列,對于tcp_poll來說,其等待隊列是sk->sk_sleep(注意把進程掛到等待隊列中并不代表進程已經睡眠了)。在設備收到一條消息(網絡設備)或填寫完文件數據(磁盤設備)后,會喚醒設備等待隊列上睡眠的進程,這時current便被喚醒了。
(6)poll方法返回時會返回一個描述讀寫操作是否就緒的mask掩碼,根據這個mask掩碼給fd_set賦值。
(7)如果遍歷完所有的fd,還沒有返回一個可讀寫的mask掩碼,則會調用schedule_timeout是調用select的進程(也就是current)進入睡眠。當設備驅動發生自身資源可讀寫后,會喚醒其等待隊列上睡眠的進程。如果超過一定的超時時間(schedule_timeout指定),還是沒人喚醒,則調用select的進程會重新被喚醒獲得CPU,進而重新遍歷fd,判斷有沒有就緒的fd。
(8)把fd_set從內核空間拷貝到用戶空間。
總結:
select的幾大缺點:
(1)每次調用select,都需要把fd集合從用戶態拷貝到內核態,這個開銷在fd很多時會很大
(2)同時每次調用select都需要在內核遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大
(3)select支持的文件描述符數量太小了,默認是1024
2 poll實現
poll的實現和select非常相似,只是描述fd集合的方式不同,poll使用pollfd結構而不是select的fd_set結構,其他的都差不多,管理多個描述符也是進行輪詢,根據描述符的狀態進行處理,但是poll沒有最大文件描述符數量的限制。poll和select同樣存在一個缺點就是,包含大量文件描述符的數組被整體復制于用戶態和內核的地址空間之間,而不論這些文件描述符是否就緒,它的開銷隨著文件描述符數量的增加而線性增大。
3、epoll
epoll既然是對select和poll的改進,就應該能避免上述的三個缺點。那epoll都是怎么解決的呢?在此之前,我們先看一下epoll和select和poll的調用接口上的不同,select和poll都只提供了一個函數——select或者poll函數。而epoll提供了三個函數,epoll_create,epoll_ctl和epoll_wait,epoll_create是創建一個epoll句柄;epoll_ctl是注冊要監聽的事件類型;epoll_wait則是等待事件的產生。
對于第一個缺點,epoll的解決方案在epoll_ctl函數中。每次注冊新的事件到epoll句柄中時(在epoll_ctl中指定EPOLL_CTL_ADD),會把所有的fd拷貝進內核,而不是在epoll_wait的時候重復拷貝。epoll保證了每個fd在整個過程中只會拷貝一次。
對于第二個缺點,epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對應的設備等待隊列中,而只在epoll_ctl時把current掛一遍(這一遍必不可少)并為每個fd指定一個回調函數,當設備就緒,喚醒等待隊列上的等待者時,就會調用這個回調函數,而這個回調函數會把就緒的fd加入一個就緒鏈表)。epoll_wait的工作實際上就是在這個就緒鏈表中查看有沒有就緒的fd(利用schedule_timeout()實現睡一會,判斷一會的效果,和select實現中的第7步是類似的)。
對于第三個缺點,epoll沒有這個限制,它所支持的FD上限是最大可以打開文件的數目,這個數字一般遠大于2048,舉個例子,在1GB內存的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統內存關系很大。
總結:
(1)select,poll實現需要自己不斷輪詢所有fd集合,直到設備就緒,期間可能要睡眠和喚醒多次交替。而epoll其實也需要調用epoll_wait不斷輪詢就緒鏈表,期間也可能多次睡眠和喚醒交替,但是它是設備就緒時,調用回調函數,把就緒fd放入就緒鏈表中,并喚醒在epoll_wait中進入睡眠的進程。雖然都要睡眠和交替,但是select和poll在“醒著”的時候要遍歷整個fd集合,而epoll在“醒著”的時候只要判斷一下就緒鏈表是否為空就行了,這節省了大量的CPU時間。這就是回調機制帶來的性能提升。
(2)select,poll每次調用都要把fd集合從用戶態往內核態拷貝一次,并且要把current往設備等待隊列中掛一次,而epoll只要一次拷貝,而且把current往等待隊列上掛也只掛一次(在epoll_wait的開始,注意這里的等待隊列并不是設備等待隊列,只是一個epoll內部定義的等待隊列)。這也能節省不少的開銷。
審核編輯:黃飛
-
服務器
+關注
關注
12文章
9277瀏覽量
85826 -
路由器
+關注
關注
22文章
3742瀏覽量
114224 -
epoll
+關注
關注
0文章
28瀏覽量
2972 -
select
+關注
關注
0文章
28瀏覽量
3929
原文標題:select、poll、epoll之間的區別(搜狗面試)
文章出處:【微信號:LinuxHub,微信公眾號:Linux愛好者】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論