0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

教你如何區(qū)別select、poll、epoll?

Linux愛好者 ? 來源:Linux愛好者 ? 2023-11-21 15:25 ? 次閱讀

epoll和select

相比于select,epoll最大的好處在于它不會隨著監(jiān)聽fd數(shù)目的增長而降低效率。因為在內(nèi)核中的select實現(xiàn)中,它是采用輪詢來處理的,輪詢的fd數(shù)目越多,自然耗時越多。

并且,在linux/posix_types.h頭文件有這樣的聲明:

#define __FD_SETSIZE 1024

表示select最多同時監(jiān)聽1024個fd,當(dāng)然,可以通過修改頭文件再重編譯內(nèi)核來擴(kuò)大這個數(shù)目,但這似乎并不治本。

一、IO多路復(fù)用的select

IO多路復(fù)用相對于阻塞式和非阻塞式的好處就是它可以監(jiān)聽多個 socket ,并且不會消耗過多資源。當(dāng)用戶進(jìn)程調(diào)用 select 時,它會監(jiān)聽其中所有 socket 直到有一個或多個 socket 數(shù)據(jù)已經(jīng)準(zhǔn)備好,否則就一直處于阻塞狀態(tài)。select的缺點在于單個進(jìn)程能夠監(jiān)視的文件描述符的數(shù)量存在最大限制,select()所維護(hù)的存儲大量文件描述符的數(shù)據(jù)結(jié)構(gòu),隨著文件描述符數(shù)量的增大,其復(fù)制的的開銷也線性增長。同時,由于網(wǎng)絡(luò)響應(yīng)時間的延遲使得大量的tcp鏈接處于非?;钴S狀態(tài),但調(diào)用select()會對所有的socket進(jìn)行一次線性掃描,所以這也浪費了一定的開銷。不過它的好處還有就是它的跨平臺特性。

c008854c-716d-11ee-939d-92fbcf53809c.jpg

二、 epoll

epoll的ET是必須對非阻塞的socket才能工作,LT對于阻塞的socket也可以

所有I/O多路復(fù)用操作都是同步的,涵蓋select/poll。

阻塞/非阻塞是相對于同步I/O來說的,與異步I/O無關(guān)。

select/poll/epoll本身是同步的,可以阻塞也可以不阻塞。

(阻塞和非阻塞 與同步不同步不同;阻塞與否 是自身,異步與否是與外部協(xié)作的關(guān)系)

skater:

無論是阻塞 I/O、非阻塞 I/O,還是基于非阻塞 I/O 的多路復(fù)用都是同步調(diào)用。因為它們在 read 調(diào)用時,內(nèi)核將數(shù)據(jù)從內(nèi)核空間拷貝到應(yīng)用程序空間(epoll應(yīng)該是從mmap),過程都是需要等待的,也就是說這個過程是同步的,如果內(nèi)核實現(xiàn)的拷貝效率不高,read 調(diào)用就會在這個同步過程中等待比較長的時間。

c017d02e-716d-11ee-939d-92fbcf53809c.jpg

epoll事件:
   EPOLLIN : 表示對應(yīng)的文件描述符可以讀(包括對端SOCKET正常關(guān)閉);
   EPOLLOUT: 表示對應(yīng)的文件描述符可以寫;
   EPOLLPRI: 表示對應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來);
   EPOLLERR: 表示對應(yīng)的文件描述符發(fā)生錯誤;
   EPOLLHUP: 表示對應(yīng)的文件描述符被掛斷;

epoll高效的核心是:1、用戶態(tài)和內(nèi)核太共享內(nèi)存mmap。2、數(shù)據(jù)到來采用事件通知機制(而不需要輪詢)。

epoll的接口

epoll的接口非常簡單,一共就三個函數(shù):

(1)epoll_create系統(tǒng)調(diào)用

epoll_create在C庫中的原型如下。

int epoll_create(int size);

epoll_create返回一個句柄,之后 epoll的使用都將依靠這個句柄來標(biāo)識。參數(shù) size是告訴 epoll所要處理的大致事件數(shù)目。不再使用 epoll時,必須調(diào)用 close關(guān)閉這個句柄。

注意:size參數(shù)只是告訴內(nèi)核這個 epoll對象會處理的事件大致數(shù)目,而不是能夠處理的事件的最大個數(shù)。在 Linux最新的一些內(nèi)核版本的實現(xiàn)中,這個 size參數(shù)沒有任何意義。

(2)epoll_ctl系統(tǒng)調(diào)用

epoll_ctl在C庫中的原型如下。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);

epoll_ctl向 epoll對象中添加、修改或者刪除感興趣的事件,返回0表示成功,否則返回–1,此時需要根據(jù)errno錯誤碼判斷錯誤類型。epoll_wait方法返回的事件必然是通過 epoll_ctl添加到 epoll中的。

參數(shù):

epfd: epoll_create返回的句柄,

op:的意義見下表:

EPOLL_CTL_ADD:注冊新的fd到epfd中;

EPOLL_CTL_MOD:修改已經(jīng)注冊的fd的監(jiān)聽事件;

EPOLL_CTL_DEL:從epfd中刪除一個fd;

fd:需要監(jiān)聽的socket句柄fd,

event:告訴內(nèi)核需要監(jiān)聽什么事的結(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)聽的事件(感興趣的事件):

EPOLLIN :表示對應(yīng)的文件描述符可以讀(包括對端SOCKET正常關(guān)閉);

EPOLLOUT:表示對應(yīng)的文件描述符可以寫;

EPOLLPRI:表示對應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來);

EPOLLERR:表示對應(yīng)的文件描述符發(fā)生錯誤;

EPOLLHUP:表示對應(yīng)的文件描述符被掛斷;

EPOLLET: 將EPOLL設(shè)為邊緣觸發(fā)(Edge Triggered)模式,這是相對于水平觸發(fā)(Level Triggered)來說的。

EPOLLONESHOT:只監(jiān)聽一次事件,當(dāng)監(jiān)聽完這次事件之后,如果還需要繼續(xù)監(jiān)聽這個socket的話,需要再次把這個socket加入到EPOLL隊列里

c022c074-716d-11ee-939d-92fbcf53809c.jpg

data成員是一個epoll_data聯(lián)合,其定義如下:
 
typedef union epoll_data {
 
void *ptr;
 
int fd;
 
uint32_t u32;
 
uint64_t u64;
 
} epoll_data_t;
 
可見,這個 data成員還與具體的使用方式相關(guān)。例如,ngx_epoll_module模塊只使用了聯(lián)合中的 ptr成員,
作為指向 ngx_connection_t連接的指針。我們在項目中一般使用的也是 ptr成員,因為它可以指向任意的結(jié)構(gòu)
體地址。

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監(jiān)控的事件中已經(jīng)發(fā)生的事件,如果 epoll中沒有任何一個事件發(fā)生,則最多等待timeout毫秒后返回。epoll_wait的返回值表示當(dāng)前發(fā)生的事件個數(shù),如果返回0,則表示本次調(diào)用中沒有事件發(fā)生,如果返回–1,則表示出現(xiàn)錯誤,需要檢查 errno錯誤碼判斷錯誤類型。

epfd:epoll的描述符。

events:分配好的 epoll_event結(jié)構(gòu)體數(shù)組,epoll將會把發(fā)生的事件復(fù)制到 events數(shù)組中(events不可以是空指針,內(nèi)核只負(fù)責(zé)把數(shù)據(jù)復(fù)制到這個 events數(shù)組中,不會去幫助我們在用戶態(tài)中分配內(nèi)存。內(nèi)核這種做法效率很高)。

maxevents:表示本次可以返回的最大事件數(shù)目,通常 maxevents參數(shù)與預(yù)分配的events數(shù)組的大小是相等的。

timeout:表示在沒有檢測到事件發(fā)生時最多等待的時間(單位為毫秒),如果 timeout為0,則表示 epoll_wait在 rdllist鏈表中為空,立刻返回,不會等待。

epoll有兩種工作模式:LT(水平觸發(fā))模式和ET(邊緣觸發(fā))模式。

默認(rèn)情況下,epoll采用 LT模式工作,這時可以處理阻塞和非阻塞套接字,而上表中的 EPOLLET表示可以將一個事件改為 ET模式。ET模式的效率要比 LT模式高,它只支持非阻塞套接字。

(水平觸發(fā)LT:當(dāng)被監(jiān)控的文件描述符上有可讀寫事件發(fā)生時,epoll_wait()會通知處理程序去讀寫。如果這次沒有把數(shù)據(jù)一次性全部讀寫完(如讀寫緩沖區(qū)太小),那么下次調(diào)用 epoll_wait()時,它還會通知你在上次沒讀寫完的文件描述符上繼續(xù)讀寫

邊緣觸發(fā)ET:當(dāng)被監(jiān)控的文件描述符上有可讀寫事件發(fā)生時,epoll_wait()會通知處理程序去讀寫。如果這次沒有把數(shù)據(jù)全部讀寫完(如讀寫緩沖區(qū)太小),那么下次調(diào)用epoll_wait()時,它不會通知你,也就是它只會通知你一次,直到該文件描述符上出現(xiàn)第二次可讀寫事件才會通知你

此可見,水平觸發(fā)時如果系統(tǒng)中有大量你不需要讀寫的就緒文件描述符,而它們每次都會返回,這樣會大大降低處理程序檢索自己關(guān)心的就緒文件描述符的效率,而邊緣觸發(fā),則不會充斥大量你不關(guān)心的就緒文件描述符,從而性能差異,高下立見。)

如何來使用epoll

1、包含一個頭文件#include

2、create_epoll(int maxfds)來創(chuàng)建一個epoll的句柄,其中maxfds為你epoll所支持的最大句柄數(shù)。這個函數(shù)會返回一個新的epoll句柄,之后的所有操作將通過這個句柄來進(jìn)行操作。在用完之后,記得用close()來關(guān)閉這個創(chuàng)建出來的epoll句柄。

3、之后在你的網(wǎng)絡(luò)主循環(huán)里面,每一幀的調(diào)用epoll_wait(int epfd, epoll_event events, int max events, int timeout)來查詢所有的網(wǎng)絡(luò)接口,看哪一個可以讀,哪一個可以寫了。基本的語法為:

nfds = epoll_wait(kdpfd, events, maxevents, -1);

其中kdpfd為用epoll_create創(chuàng)建之后的句柄,events是一個epoll_event*的指針,當(dāng)epoll_wait這個函數(shù)操作成功之后,epoll_events里面將儲存所有的讀寫事件。max_events是當(dāng)前需要監(jiān)聽的所有socket句柄數(shù)。

最后一個timeout:是epoll_wait的超時,

為0的時候表示馬上返回,

為-1的時候表示一直等下去,直到有事件返回,

為任意正整數(shù)的時候表示等這么長的時間,如果一直沒有事件,則返回。

一般如果網(wǎng)絡(luò)主循環(huán)是單獨的線程的話,可以用-1來等,這樣可以保證一些效率,如果是和主邏輯在同一個線程的話,則可以用0來保證主循環(huán)的效率。

epoll_wait范圍之后應(yīng)該是一個循環(huán),遍利所有的事件。

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

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

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

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

epoll程序框架

幾乎所有的epoll程序都使用下面的框架:

偽代碼:

listenfd為全局變量,服務(wù)端監(jiān)聽的套接字的fd。

關(guān)于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)
      {/*有數(shù)據(jù)可讀*/
 
      }
      else if (events[i].events & EPOLLOUT)
      {/*有數(shù)據(jù)可寫*/
 
      }
      else
      {/*出錯*/
 
      }
    }
  }
}
 
通過測試發(fā)現(xiàn)epoll_wait返回值number是不會大于MAX_EVENT_NUMBER的。
 
測試過程中,連接的客戶端數(shù)遠(yuǎn)大于MAX_EVENT_NUMBER,由此可以推論:epoll_wait()每次返回的是活躍客戶端的個數(shù),每次并將這些活躍的客戶端信息加入到events[MAX_EVENT_NUMBER]。
 
由此可見,活躍客戶端的個數(shù)相同的情況下,events[MAX_EVENT_NUMBER]越大,epoll_wait()函數(shù)執(zhí)行次數(shù)越少,但是events[MAX_EVENT_NUMBER]越大越消耗存儲資源。
 
所以,MAX_EVENT_NUMBER的選擇應(yīng)該在效率和資源間取一個平衡點。

示例代碼

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)識符,等待下一個循環(huán)時接收數(shù)據(jù)
            }
            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; //設(shè)置與要處理的事件相關(guān)的文件描述符
 ev.events = EPOLLIN | EPOLLET; //設(shè)置要處理的事件類型EPOLLIN :表示對應(yīng)的文件描述符可以讀,EPOLLET狀態(tài)變化才通知
 
 
epfd = epoll_create(256);  //生成用于處理accept的epoll專用的文件描述符
//注冊epoll事件
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); //epfd epoll實例ID,EPOLL_CTL_ADD添加,listenfd:socket,ev事件(監(jiān)聽listenfd)
 
 
nfds = epoll_wait(epfd, event_list, EVENT_MAX_COUNT, TIMEOUT_MS); //等待epoll事件的發(fā)生

1. 首先熟悉下epoll的三個接口

int epoll_create(int size);

創(chuàng)建epoll相關(guān)數(shù)據(jù)結(jié)構(gòu),其最重要的是

1. 紅黑樹, 用于存儲需要監(jiān)控的文件句柄以及事件

2. 就緒鏈表,用于存儲被觸發(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時間,如果文件句柄上相關(guān)事件被觸發(fā),則epoll_wait退出,并將觸發(fā)的事件 寫入出參 events參數(shù),觸發(fā)的事件個數(shù)作為返回值返回

2. 如何使用這三個接口寫一個server

首先使用epoll_create 創(chuàng)建epoll相關(guān)數(shù)據(jù)結(jié)構(gòu)

其次創(chuàng)建TCP socket 文件句柄acceptfd,綁定(ip:port),然后開啟監(jiān)聽,并使用epoll_ctl 注冊到epoll中,監(jiān)聽acceptfd句柄的EPOLL_IN事件(即可讀事件)

調(diào)用epoll_wait 開始進(jìn)行阻塞等待

如果有客戶端連接過來,則觸發(fā)acceptfd上的EPOLL_IN事件,epoll_wait返回后,可以得到觸發(fā)事件的信息, 這些信息其實就是一個struct epoll_event對象, 我們可以判斷這個epoll event對象fd是否和acceptfd一致,如果一致在認(rèn)為有新連接進(jìn)來,則獲得新連接對應(yīng)的clientfd, 并使用epoll_ctl注冊到epoll, 監(jiān)控clientfd上的epoll_in事件,這個時候這個客戶端和服務(wù)器的連接就建立了

           typedef union epoll_data {
               void    *ptr;
               int      fd; //可以用fd, 也可以用ptr來保存事件對應(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 */
           };

用戶在客戶端輸入命令,將會觸發(fā)服務(wù)器端 clientfd上的epoll_in事件,epoll_wait返回觸發(fā)的event, 讀取event對應(yīng)的clientfd內(nèi)核緩沖區(qū)中的數(shù)據(jù),解析協(xié)議,執(zhí)行命令,得到返回結(jié)果,這個返回結(jié)果要返回給客戶端,則再使用epoll_ctl注冊clientfd的epoll_out事件到epoll,這個時候,我們會注意到clientfd上既有epoll_in,也有epoll_out,這樣其實沒有必要,客戶端在這個時候等待返回結(jié)果,不會再輸入命令,所以需要使用epoll_ctl把epoll_out刪除掉

如果clientfd內(nèi)核緩沖區(qū)可寫,epoll_wait這個時候會返回,并返回epoll_out事件,此時把返回的結(jié)果數(shù)據(jù)寫入clientfd, 返回給客戶端

實例源碼

原文有相當(dāng)多的如錯誤:

需要增加到頭文件和錯誤修改

//'/0'->''
//bzero() 替換為memset (注意二者參數(shù)不一樣,bzero將前n個字節(jié)設(shè)為0,memset將前n 個字節(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個字節(jié)設(shè)為0,memset將前n 個字節(jié)的值設(shè)為值 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的第二個參數(shù)  定義TCP鏈接未完成隊列的大?。╨inux >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()函數(shù)根據(jù)指定的format(格式)(格式)發(fā)送信息(參數(shù))到由stream(流)指定的文件
            //printf 將內(nèi)容發(fā)送到Default的輸出設(shè)備,通常為本機的顯示器,fprintf需要指定輸出設(shè)備,可以為文件,設(shè)備。
            //stderr
            return 1;
        }
    }
    else
    {
        fprintf(stderr, "Usage:%s portnumber/r/n", argv[0]);
        return 1;
    }
 
    //聲明epoll_event結(jié)構(gòu)體的變量,ev用于注冊事件,數(shù)組用于回傳要處理的事件
    struct epoll_event ev, event_list[EVENT_MAX_COUNT];
 
    //生成用于處理accept的epoll專用的文件描述符
    epfd = epoll_create(256); //生成epoll文件描述符,既在內(nèi)核申請一空間,存放關(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 :表示對應(yīng)的文件描述符可以讀,EPOLLET狀態(tài)變化才通知
    //ev.events=EPOLLIN;
 
    //注冊epoll事件
    epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); //epfd epoll實例ID,EPOLL_CTL_ADD添加,listenfd:socket,ev事件(監(jiān)聽listenfd)
   
 
    memset(&serveraddr, 0, sizeof(serveraddr));
    serveraddr.sin_family     = AF_INET;
    serveraddr.sin_addr.s_addr=htonl(INADDR_ANY); /*IP,INADDR_ANY轉(zhuǎn)換過來就是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事件的發(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)測到一個SOCKET用戶連接到了綁定的SOCKET端口,建立新的連接。
            {
                clilenaddrLen = sizeof(struct sockaddr_in);//在調(diào)用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位網(wǎng)絡(luò)字節(jié)序的二進(jìn)制IP地址轉(zhuǎn)換成相應(yīng)的點分十進(jìn)制的IP地址
 
                cout << "accapt a connection from " << str << endl;
 
                //設(shè)置用于讀操作的文件描述符
                ev.data.fd = connfd;
 
                //設(shè)置用于注測的讀操作事件
                ev.events = EPOLLIN | EPOLLET;
                //ev.events=EPOLLIN;
 
                //注冊ev
                epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev); //將accpet的句柄添加進(jìn)入(增加監(jiān)聽的對象)
            }
            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時fd中的數(shù)據(jù)如果小于要讀取的數(shù)據(jù),就會引起阻塞?
                {
               //當(dāng)read()或者write()返回-1時,一般要判斷errno
                    if (errno == ECONNRESET)//與客戶端的Socket被客戶端強行被斷開,而服務(wù)器還企圖read
                    {
                        close(sockfd);
                        event_list[i].data.fd = -1;
                    }
                    else
                        std::cout << "readline error" << std::endl;
                }
                else if (n == 0) //返回的n為0時,說明客戶端已經(jīng)關(guān)閉 
                {
                    close(sockfd);
                    event_list[i].data.fd = -1;
                }
 
                line_buff[n] = '';
                cout << "read " << line_buff << endl;
 
                //設(shè)置用于寫操作的文件描述符
                ev.data.fd = sockfd;
 
                //設(shè)置用于注測的寫操作事件
                ev.events = EPOLLOUT | EPOLLET; //EPOLLOUT:表示對應(yīng)的文件描述符可以寫;
 
                //修改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è)置用于注測的讀操作事件
                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"

相關(guān)知識

如何動態(tài)的改變listen監(jiān)聽的個數(shù)呢?

如果指定值在源代碼中是一個常值,那么增長其大小需要重新編譯服務(wù)器程序。那么,我們可以為它設(shè)定一個缺省值,不過允許通過命令行選項或者環(huán)境變量來覆寫該值。

void Listen(int fd, int backlog)
{
    char *ptr;
    
    if((ptr = getenv("LISTENQ")) != NULL)
        backlog = atoi(ptr);
 
    if(listen(fd, backlog) < 0)
        printf("listen error
");
}

隊列已滿的情況,如何處理?

當(dāng)一個客戶SYN到達(dá)時,若這個隊列是滿的,TCP就忽略該分節(jié),也就是不會發(fā)送RST。

這么做的原因在于,隊列已滿的情況是暫時的,客戶TCP如果沒收收到RST,就會重發(fā)SYN,在隊列有空閑的時候處理該請求。如果服務(wù)器TCP立即響應(yīng)一個RST,客戶的connect調(diào)用就會立即返回一個錯誤,強制應(yīng)用進(jìn)程處理這種情況,而不會再次重發(fā)SYN。而且客戶端也不無區(qū)別該套接口的狀態(tài),是“隊列已滿”還是“該端口沒有在監(jiān)聽”。

SYN泛濫攻擊

向某一目標(biāo)服務(wù)器發(fā)送大量的SYN,用以填滿一個或多個TCP端口的未完成隊列。每個SYN的源IP地址都置成隨機數(shù)(IP欺騙),這樣防止攻擊服務(wù)器獲悉黑客的真實IP地址。通過偽造的SYN裝滿未完成連接隊列,使得合法的SYN不能排上隊,導(dǎo)致針對合法用戶的服務(wù)被拒絕。

防御方法:

針對服務(wù)器主機的方法。增加連接緩沖隊列長度和縮短連接請求占用緩沖隊列的超時時間。該方式最簡單,被很多操作系統(tǒng)采用,但防御性能也最弱。

針對路由器過濾的方法。由于DDoS攻擊,包括SYN-Flood,都使用地址偽裝技術(shù),所以在路由器上使用規(guī)則過濾掉被認(rèn)為地址偽裝的包,會有效的遏制攻擊流量。

針對防火墻的方法。在SYN請求連接到真正的服務(wù)器之前,使用基于防火墻的網(wǎng)關(guān)來測試其合法性。它是一種被普遍采用的專門針對SYN-Flood攻擊的防御機制。

SYN:同步序列編號(Synchronize Sequence Numbers)

c026ed2a-716d-11ee-939d-92fbcf53809c.jpg

它們的含義是:
SYN表示建立連接,
FIN表示關(guān)閉連接,
ACK表示響應(yīng),
PSH表示有 
DATA數(shù)據(jù)傳輸,
RST表示連接重置。

SYN(synchronous建立聯(lián)機)

ACK(acknowledgement 確認(rèn))

PSH(push傳送)

FIN(finish結(jié)束)

RST(reset重置)

URG(urgent緊急)

Sequence number(順序號碼)

Acknowledge number(確認(rèn)號碼)

三次握手:

在TCP/IP協(xié)議中,TCP協(xié)議提供可靠的連接服務(wù),采用三次握手建立一個連接。
 第一次握手:建立連接時,客戶端發(fā)送syn包(syn=j)到服務(wù)器,并進(jìn)入SYN_SEND狀態(tài),等待服務(wù)器確認(rèn);
第二次握手:服務(wù)器收到syn包,必須確認(rèn)客戶的SYN(ack=j+1),同時自己也發(fā)送一個SYN包(syn=k),即SYN+ACK包,此時服務(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ù)器開始傳送數(shù)據(jù).

第一次握手:主機A發(fā)送位碼為syn=1,隨機產(chǎn)生seq number=1234567的數(shù)據(jù)包到服務(wù)器,主機B由SYN=1知道,A要求建立聯(lián)機;

第二次握手:主機B收到請求后要確認(rèn)聯(lián)機信息,向A發(fā)送ack number=(主機A的seq+1),syn=1,ack=1,隨機產(chǎn)生seq=7654321的包;

第三次握手:主機A收到后檢查ack number是否正確,即第一次發(fā)送的seq number+1,以及位碼ack是否為1,若正確,主機A會再發(fā)送ack number=(主機B的seq+1),ack=1,主機B收到后確認(rèn)seq值與ack=1則連接建立成功。

實例代碼二

多進(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+個鏈接的測試代碼

#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)控的文件描述符上有可讀寫事件發(fā)生時,epoll_wait()會通知處理程序去讀寫。如果這次沒有把數(shù)據(jù)一次性全部讀寫完(如讀寫緩沖區(qū)太小),那么下次調(diào)用 epoll_wait()時,它還會通知你在上次沒讀寫完的文件描述符上繼續(xù)讀寫

邊緣觸發(fā)ET:

當(dāng)被監(jiān)控的文件描述符上有可讀寫事件發(fā)生時,epoll_wait()會通知處理程序去讀寫。如果這次沒有把數(shù)據(jù)全部讀寫完(如讀寫緩沖區(qū)太小),那么下次調(diào)用epoll_wait()時,它不會通知你,也就是它只會通知你一次,直到該文件描述符上出現(xiàn)第二次可讀寫事件才會通知你

水平觸發(fā):只要緩沖區(qū)有數(shù)據(jù)就會一直觸發(fā)

邊沿觸發(fā):只有在緩沖區(qū)增加數(shù)據(jù)的那一刻才會觸發(fā)

由此可見,水平觸發(fā)時如果系統(tǒng)中有大量你不需要讀寫的就緒文件描述符(有些fd有數(shù)據(jù),但是你不處理那些fd),而它們每次都會返回,這樣會大大降低處理程序檢索自己關(guān)心的就緒文件描述符的效率,而邊緣觸發(fā),則不會充斥大量你不關(guān)心的就緒文件描述符,從而性能差異,高下立見。

c02ef88a-716d-11ee-939d-92fbcf53809c.jpg

c03dc0a4-716d-11ee-939d-92fbcf53809c.jpg

4、關(guān)于ET、LT兩種工作模式

可以得出這樣的結(jié)論:

ET模式僅當(dāng)狀態(tài)發(fā)生變化的時候才獲得通知,這里所謂的狀態(tài)的變化并不包括緩沖區(qū)中還有未處理的數(shù)據(jù),也就是說,如果要采用ET模式,需要一直read/write直到出錯為止,很多人反映為什么采用ET模式只接收了一部分?jǐn)?shù)據(jù)就再也得不到通知了,大多因為這樣;而LT模式是只要有數(shù)據(jù)沒有處理就會一直通知下去的.

epoll中讀寫數(shù)據(jù) 的注意事項

在一個非阻塞的socket上調(diào)用read/write函數(shù),返回EAGAIN或者EWOULDBLOCK(注:EAGAIN就是EWOULDBLOCK)。

從字面上看,意思是:

EAGAIN: 再試一次

EWOULDBLOCK:如果這是一個阻塞socket, 操作將被block

perror輸出:Resource temporarily unavailable

總結(jié):

這個錯誤表示資源暫時不夠,可能read時, 讀緩沖區(qū)沒有數(shù)據(jù), 或者write時,寫緩沖區(qū)滿了。

遇到這種情況,如果是阻塞socket、 read/write就要阻塞掉。而如果是非阻塞socket、 read/write立即返回-1, 同 時errno設(shè)置為EAGAIN。

所以對于阻塞socket、 read/write返回-1代表網(wǎng)絡(luò)出錯了。但對于非阻塞socket、read/write返回-1不一定網(wǎng)絡(luò)真的出錯了??赡苁荝esource temporarily unavailable。這時你應(yīng)該再試,直到Resource available。

本文主要講述epoll模型(不完全是針對epoll)下讀寫數(shù)據(jù)接口使用的注意事項

1、read write

函數(shù)原型如下:

#include 
ssize_t read(int filedes, void* buf, size_t nbytes)
ssize_t write(int filedes, const void* buf, size_t nbytes)

其中,read返回實際讀取到的字節(jié)數(shù)。但實際讀取的字節(jié)很有可能少于指定要讀取的字節(jié)數(shù)nbytes。因此會分為:

①返回值大于0。 讀取正常,返回實際讀取到的字節(jié)數(shù)

②返回值等于0。 讀取異常,讀取到文件filedes結(jié)尾處了。這里邏輯上要理解為read已經(jīng)讀取完數(shù)據(jù)

③返回值小于0(-1)。 讀取出錯,在處理網(wǎng)絡(luò)請求時可能是網(wǎng)絡(luò)異常。著重注意當(dāng)返回-1,此時errno的值EAGAIN、EWOULLDBLOCK,表示內(nèi)核對應(yīng)的讀緩沖區(qū)為空

而write返回的實際寫入字節(jié)數(shù)正常情況是與制定寫入的字節(jié)數(shù)nbytes相同的,不相等說明寫入異常了,著重注意,此時errno的值EAGAIN、EWOULLDBLOCK,表示內(nèi)核對應(yīng)的寫緩沖區(qū)為空。注,EAGAIN等同于EWOULLDBLOCK。

總之,這個錯誤表示資源暫時不夠,可能read時讀緩沖區(qū)沒有數(shù)據(jù), 或者write時寫緩沖區(qū)滿了。遇到這種情況,如果是阻塞socket、 read/write就要阻塞掉。而如果是非阻塞socket、 read/write立即返回-1, 同時errno設(shè)置為EAGAIN。

所以對于阻塞socket、 read/write返回-1代表網(wǎng)絡(luò)出錯了。但對于非阻塞socket、read/write返回-1不一定網(wǎng)絡(luò)真的出錯了??赡苤С志彌_區(qū)空或者滿,這時應(yīng)該再試,直到Resource available。

綜上,對于非阻塞的socket,正確的讀寫操作為:

LT模式

讀: 忽略掉errno = EAGAIN的錯誤,下次繼續(xù)讀;

寫:忽略掉errno = EAGAIN的錯誤,下次繼續(xù)寫。

對于select和epoll的LT模式,這種讀寫方式是沒有問題的。但對于epoll的ET模式,這種方式還有漏洞。

下面來介紹下epoll事件的兩種模式LT(水平觸發(fā))和ET(邊沿觸發(fā)),根據(jù)可以理解為,文件描述符的讀寫狀態(tài)發(fā)生變化才會觸發(fā)epoll事件,具體說來如下:二者的差異在于 level-trigger 模式下只要某個 socket 處于 readable/writable 狀態(tài),無論什么時候進(jìn)行 epoll_wait 都會返回該 socket;而 edge-trigger 模式下只有某個 socket 從 unreadable 變?yōu)?readable,或從unwritable 變?yōu)閣ritable時,epoll_wait 才會返回該 socket。如下兩個示意圖:

從socket讀數(shù)據(jù):

c02ef88a-716d-11ee-939d-92fbcf53809c.jpg

往socket寫數(shù)據(jù):

c03dc0a4-716d-11ee-939d-92fbcf53809c.jpg

所以在epoll的ET模式下,正確的讀寫方式為:

讀: 只要可讀, 就一直讀,直到返回0,或者 errno = EAGAIN寫:只要可寫, 就一直寫,直到數(shù)據(jù)發(fā)送完,或者 errno = EAGAIN

這里的意思是,對于ET模式,相當(dāng)于我們要自己重寫read和write,使其像”原子操作“一樣,保證一次read 或 write能夠完整的讀完緩沖區(qū)的數(shù)據(jù)或者寫完要寫入緩沖區(qū)的數(shù)據(jù)。因此,實現(xiàn)為用while包住read和write即可。但是對于select或者LT模式,我們可以只使用一次read和write,因為在主程序中會一直while,而事件再下一次select時還會被獲取到。但也可以實現(xiàn)為用while包住read和write。從邏輯上講,一次性把數(shù)據(jù)讀取完整可以保證數(shù)據(jù)的完整性。

下面來說明這種”原子操作“read和write

int n = 0;
while(1)
{
    nread = read(fd, buf + n, BUFSIZ - 1); //讀時,用戶進(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);//寫時,數(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 個問題:

(1) LT模式下或ET模式下,阻塞的監(jiān)聽socket, accept 存在的問題

accept每次都是從已經(jīng)完成三次握手的tcp隊列中取出一個連接,考慮這種情況: TCP 連接被客戶端夭折,即在服務(wù)器調(diào)用 accept 之前,客戶端主動發(fā)送 RST 終止連接,導(dǎo)致剛剛建立的連接從就緒隊列中移出,如果套接口被設(shè)置成阻塞模式,服務(wù)器就會一直阻塞在 accept 調(diào)用上,直到其他某個客戶建立一個新的連接為止。但是在此期間,服務(wù)器單純地阻塞在accept 調(diào)用上,就緒隊列中的其他描述符都得不到處理。

解決辦法是:把監(jiān)聽套接口設(shè)置為非阻塞,當(dāng)客戶在服務(wù)器調(diào)用 accept 之前中止某個連接時,accept 調(diào)用可以立即返回 -1, 這時源自 Berkeley 的實現(xiàn)會在內(nèi)核中處理該事件,并不會將該事件通知給 epoll,而其他實現(xiàn)把 errno 設(shè)置為 ECONNABORTED 或者 EPROTO 錯誤,我們應(yīng)該忽略這兩個錯誤。

(2) ET 模式下 accept 存在的問題

考慮這種情況:多個連接同時到達(dá),服務(wù)器的 TCP 就緒隊列瞬間積累多個就緒連接,由于是邊緣觸發(fā)模式,epoll 只會通知一次,accept 只處理一個連接,導(dǎo)致 TCP 就緒隊列中剩下的連接都得不到處理。

解決辦法是:將監(jiān)聽套接字設(shè)置為非阻塞模式,用 while 循環(huán)抱住 accept 調(diào)用,處理完 TCP 就緒隊列中的所有連接后再退出循環(huán)。如何知道是否處理完就緒隊列中的所有連接呢? 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");  
}

一道騰訊后臺開發(fā)的面試題:

使用Linux epoll模型,水平觸發(fā)模式;當(dāng)socket可寫時,會不停的觸發(fā) socket 可寫的事件,如何處理?

第一種最普遍的方式:

需要向 socket 寫數(shù)據(jù)的時候才把 socket 加入 epoll ,等待可寫事件。接受到可寫事件后,調(diào)用 write 或者 send 發(fā)送數(shù)據(jù)。當(dāng)所有數(shù)據(jù)都寫完后,把 socket 移出 epoll。

這種方式的缺點是,即使發(fā)送很少的數(shù)據(jù),也要把 socket 加入 epoll,寫完后在移出 epoll,有一定操作代價。

一種改進(jìn)的方式:

開始不把 socket 加入 epoll,需要向 socket 寫數(shù)據(jù)的時候,直接調(diào)用 write 或者 send 發(fā)送數(shù)據(jù)。如果返回 EAGAIN,把 socket 加入 epoll,在 epoll 的驅(qū)動下寫數(shù)據(jù),全部數(shù)據(jù)發(fā)送完畢后,再移出 epoll。

這種方式的優(yōu)點是:數(shù)據(jù)不多的時候可以避免 epoll 的事件處理,提高效率。

多路復(fù)用

如文初的說明表示,這三者都是 I/O 多路復(fù)用機制,且簡要介紹了多路復(fù)用的定義,那么如何更加直觀地了解多路復(fù)用呢?這里有張圖:

c0759f2e-716d-11ee-939d-92fbcf53809c.jpg

對于網(wǎng)頁服務(wù)器 Nginx 來說,會有很多連接進(jìn)來, epoll 會把他們都監(jiān)視起來,然后像撥開關(guān)一樣,誰有數(shù)據(jù)就撥向誰,然后調(diào)用相應(yīng)的代碼處理。

一般來說以下場合需要使用 I/O 多路復(fù)用:

當(dāng)客戶處理多個描述字時(一般是交互式輸入和網(wǎng)絡(luò)套接口)

如果一個服務(wù)器既要處理 TCP,又要處理 UDP,一般要使用 I/O 復(fù)用

如果一個 TCP 服務(wù)器既要處理監(jiān)聽套接口,又要處理已連接套接口

為什么epoll可以支持百萬級別的連接?

在server的處理過程中,大家可以看到其中重要的操作是,使用epoll_ctl修改clientfd在epoll中注冊的epoll_event, 這個操作首先在紅黑樹中找到fd對應(yīng)的epoll_event, 然后進(jìn)行修改,紅黑樹是典型的二叉平衡樹,其時間復(fù)雜度是log2(n), 1百萬的文件句柄,只需要16次左右的查找,速度是非??斓?,支持百萬級別毫無壓力

另外,epoll通過注冊fd上的回調(diào)函數(shù),回調(diào)函數(shù)監(jiān)控到有事件發(fā)生,則準(zhǔn)備好相關(guān)的數(shù)據(jù)放到到就緒鏈表里面去,這個動作非??欤杀疽卜浅P?/p>

socket讀寫返回值的處理

在調(diào)用socket讀寫函數(shù)read(),write()時,都會有返回值。如果沒有正確處理返回值,就可能引入一些問題

總結(jié)了以下幾點

1當(dāng)read()或者write()函數(shù)返回值大于0時,表示實際從緩沖區(qū)讀取或者寫入的字節(jié)數(shù)目

2當(dāng)read()函數(shù)返回值為0時,表示對端已經(jīng)關(guān)閉了 socket,這時候也要關(guān)閉這個socket,否則會導(dǎo)致socket泄露。netstat命令查看下,如果有closewait狀態(tài)的socket,就是socket泄露了

當(dāng)write()函數(shù)返回0時,表示當(dāng)前寫緩沖區(qū)已滿,是正常情況,下次再來寫就行了。

3當(dāng)read()或者write()返回-1時,一般要判斷errno

如果errno == EINTR,表示系統(tǒng)當(dāng)前中斷了,直接忽略

如果errno == EAGAIN或者EWOULDBLOCK,非阻塞socket直接忽略;如果是阻塞的socket,一般是讀寫操作超時了,還未返回。這個超時是指socket的SO_RCVTIMEO與SO_SNDTIMEO兩個屬性。所以在使用阻塞socket時,不要將超時時間設(shè)置的過小。不然返回了-1,你也不知道是socket連接是真的斷開了,還是正常的網(wǎng)絡(luò)抖動。一般情況下,阻塞的socket返回了-1,都需要關(guān)閉重新連接。

4.另外,對于非阻塞的connect,可能返回-1.這時需要判斷errno,如果 errno == EINPROGRESS,表示正在處理中,否則表示連接出錯了,需要關(guān)閉重連。之后使用select,檢測到該socket的可寫事件時,要判斷getsockopt(c->fd, SOL_SOCKET, SO_ERROR, &err, &errlen),看socket是否出錯了。如果err值為0,則表示connect成功;否則也應(yīng)該關(guān)閉重連

5 在使用epoll時,有ET與LT兩種模式。ET模式下,socket需要read或者write到返回-1為止。對于非阻塞的socket沒有問題,但是如果是阻塞的socket,正如第三條中所說的,只有超時才會返回。所以在ET模式下千萬不要使用阻塞的socket。那么LT模式為什么沒問題呢?一般情況下,使用LT模式,我們只要調(diào)用一次read或者write函數(shù),如果沒有讀完或者沒有寫完,下次再來就是了。由于已經(jīng)返回了可讀或者可寫事件,所以可以保證調(diào)用一次read或者write會正常返回。

nread為-1且errno==EAGAIN,說明數(shù)據(jù)已經(jīng)讀完,設(shè)置EPOLLOUT。

網(wǎng)絡(luò)狀態(tài)查詢命令

sar、iostat、lsof

問題記錄

客戶端

1、Cannot assign requested address

c07ec536-716d-11ee-939d-92fbcf53809c.jpg

大致上是由于客戶端頻繁的連服務(wù)器,由于每次連接都在很短的時間內(nèi)結(jié)束,導(dǎo)致很多的TIME_WAIT,以至于用光了可用的端 口號,所以新的連接沒辦法綁定端口,即“Cannot assign requested address”。是客戶端的問題不是服務(wù)器端的問題。通過netstat,的確看到很多TIME_WAIT狀態(tài)的連接。

client端頻繁建立連接,而端口釋放較慢,導(dǎo)致建立新連接時無可用端口。

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)低端口釋放后的等待時間,默認(rèn)為60s,修改為15~30s:

sysctl -w net.ipv4.tcp_fin_timeout=30

修改tcp/ip協(xié)議配置, 通過配置/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萬左右的鏈接,報錯誤(可能端口用盡)

如果沒有TIME_WAIT 狀態(tà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萬,長連接把端口用光了。修改端口開放范圍:

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開始,0:stdin標(biāo)準(zhǔn)輸入1:stdout標(biāo)準(zhǔn)輸出 2:stderr錯誤輸出

Epoll 難以解決的問題

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 還是要從這個api說起,這個api 可以監(jiān)聽很多個fd,但是timeout 只有一個。 有這一個場景: 你想關(guān)注 fd == 10的這個描述符,你希望該描述符有數(shù)據(jù)到來的時候通知你,并且,如果一直沒有數(shù)據(jù)來,那么希望20s之后能通知你。 你改怎么做?

進(jìn)一步假設(shè),你希望關(guān)注 100 個fd, 并希望這個一百個fd,從他們加入監(jiān)聽隊列的時候開始計算,20s后通知你超時

如果使用這個epoll_wait的話,是不是要自己記住所有fd的剩余超時時間呢?

libevent就解決了這個困擾。

配置和調(diào)試

net.ipv4.tcp_syncookies = 1 表示開啟SYN Cookies。當(dāng)出現(xiàn)SYN等待隊列溢出時,啟用cookies來處理,可防范少量SYN攻擊,默認(rèn)為0,表示關(guān)閉;

net.ipv4.tcp_tw_reuse = 1 表示開啟重用。允許將TIME-WAIT sockets重新用于新的TCP連接,默認(rèn)為0,表示關(guān)閉;

net.ipv4.tcp_tw_recycle = 1 表示開啟TCP連接中TIME-WAIT sockets的快速回收,默認(rèn)為0,表示關(guān)閉。

net.ipv4.tcp_fin_timeout 修改系默認(rèn)的 TIMEOUT 時間

優(yōu)化time_wait為啥要開syncookie呢? syncookie可以繞過seq queue的限制,跟優(yōu)化time_wait沒有關(guān)系

另外得打開timestamps,才能讓reuse、recycle生效。

修改fin_timeout,如何調(diào)?調(diào)大調(diào)小呢?

//測試數(shù)據(jù)

單線程,峰值處理鏈接,貌似目前測試條件可以測試到2.9K

CONNECT: 2.9K/s

QPS : 27.6萬

5萬的鏈接。

QPS : 1.9萬

客戶端、服務(wù)端支持多少連接

客戶端

現(xiàn)在我們終于可以得出更為正確的結(jié)論了,對于有1個Ip的客戶端來說,受限于ip_local_port_range參數(shù),也受限于65535。但單Linux可以配置多個ip,有幾個ip,最大理論值就翻幾倍

多張網(wǎng)卡不是必須的。即使只有一張網(wǎng)卡,也可以配置多ip。k8s就是這么干的,在k8s里,一臺物理機上可以部署多個pod。但每一個pod都會被分配一個獨立的ip,所以完全不用擔(dān)心物理機上部署了過多的pod而影響你用的pod里的TCP連接數(shù)量。在ip給你的那一刻,你的pod就和其它應(yīng)用隔離開了。

服務(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)存會被回收。

假設(shè)你只保持連接不發(fā)送數(shù)據(jù),那么你服務(wù)器可以建立的連接最大數(shù)量 = 你的內(nèi)存/3.3K。假如是4GB的內(nèi)存,那么大約可接受的TCP連接數(shù)量是100萬左右。

這個例子里,我們考慮的前提是在一個進(jìn)程下hold所有的服務(wù)器端連接。而在實際中的項目里,為了收發(fā)數(shù)據(jù)方便,很多網(wǎng)絡(luò)IO模型還會為TCP連接再創(chuàng)建一個線程或協(xié)程。拿最輕量的golang來說,一個協(xié)程棧也需要2KB的內(nèi)存開銷。

結(jié)論

TCP連接的客戶端機:每一個ip可建立的TCP連接理論受限于ip_local_port_range參數(shù),也受限于65535。但可以通過配置多ip的方式來加大自己的建立連接的能力。

TCP連接的服務(wù)器機:每一個監(jiān)聽的端口雖然理論值很大,但這個數(shù)字沒有實際意義。最大并發(fā)數(shù)取決你的內(nèi)存大小,每一條靜止?fàn)顟B(tài)的TCP連接大約需要吃3.3K的內(nèi)存。

select、poll、epoll之間的區(qū)別(搜狗面試)

(1)select==>時間復(fù)雜度O(n)

它僅僅知道了,有I/O事件發(fā)生了,卻并不知道是哪那幾個流(可能有一個,多個,甚至全部),我們只能無差別輪詢所有流,找出能讀出數(shù)據(jù),或者寫入數(shù)據(jù)的流,對他們進(jìn)行操作。所以select具有O(n)的無差別輪詢復(fù)雜度,同時處理的流越多,無差別輪詢時間就越長。

(2)poll==>時間復(fù)雜度O(n)

poll本質(zhì)上和select沒有區(qū)別,它將用戶傳入的數(shù)組拷貝到內(nèi)核空間,然后查詢每個fd對應(yīng)的設(shè)備狀態(tài), 但是它沒有最大連接數(shù)的限制,原因是它是基于鏈表來存儲的.

(3)epoll==>時間復(fù)雜度O(1)

epoll可以理解為event poll,不同于忙輪詢和無差別輪詢,epoll會把哪個流發(fā)生了怎樣的I/O事件通知我們。所以我們說epoll實際上是事件驅(qū)動(每個事件關(guān)聯(lián)上fd)的,此時我們對這些流的操作都是有意義的。(復(fù)雜度降低到了O(1))

select,poll,epoll都是IO多路復(fù)用的機制。I/O多路復(fù)用就通過一種機制,可以監(jiān)視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進(jìn)行相應(yīng)的讀寫操作。但select,poll,epoll本質(zhì)上都是同步I/O,因為他們都需要在讀寫事件就緒后自己負(fù)責(zé)進(jìn)行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負(fù)責(zé)進(jìn)行讀寫,異步I/O的實現(xiàn)會負(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)均有實現(xiàn)

select:

select本質(zhì)上是通過設(shè)置或者檢查存放fd標(biāo)志位的數(shù)據(jù)結(jié)構(gòu)來進(jìn)行下一步處理。這樣所帶來的缺點是:

1、 單個進(jìn)程可監(jiān)視的fd數(shù)量被限制,即能監(jiān)聽端口的大小有限。

一般來說這個數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大,具體數(shù)目可以cat /proc/sys/fs/file-max察看。32位機默認(rèn)是1024個。64位機默認(rèn)是2048.

2、 對socket進(jìn)行掃描時是線性掃描,即采用輪詢的方法,效率較低:

當(dāng)套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成調(diào)度,不管哪個Socket是活躍的,都遍歷一遍。這會浪費很多CPU時間。如果能給套接字注冊某個回調(diào)函數(shù),當(dāng)他們活躍時,自動完成相關(guān)操作,那就避免了輪詢,這正是epoll與kqueue做的。

3、需要維護(hù)一個用來存放大量fd的數(shù)據(jù)結(jié)構(gòu),這樣會使得用戶空間和內(nèi)核空間在傳遞該結(jié)構(gòu)時復(fù)制開銷大

poll:

poll本質(zhì)上和select沒有區(qū)別,它將用戶傳入的數(shù)組拷貝到內(nèi)核空間,然后查詢每個fd對應(yīng)的設(shè)備狀態(tài),如果設(shè)備就緒則在設(shè)備等待隊列中加入一項并繼續(xù)遍歷,如果遍歷完所有fd后沒有發(fā)現(xiàn)就緒設(shè)備,則掛起當(dāng)前進(jìn)程,直到設(shè)備就緒或者主動超時,被喚醒后它又要再次遍歷fd。這個過程經(jīng)歷了多次無謂的遍歷。

它沒有最大連接數(shù)的限制,原因是它是基于鏈表來存儲的,但是同樣有一個缺點:

1、大量的fd的數(shù)組被整體復(fù)制于用戶態(tài)和內(nèi)核地址空間之間,而不管這樣的復(fù)制是不是有意義。

2、poll還有一個特點是“水平觸發(fā)”,如果報告了fd后,沒有被處理,那么下次poll時會再次報告該fd。

epoll:

epoll有EPOLLLT和EPOLLET兩種觸發(fā)模式,LT是默認(rèn)的模式,ET是“高速”模式。LT模式下,只要這個fd還有數(shù)據(jù)可讀,每次 epoll_wait都會返回它的事件,提醒用戶程序去操作,而在ET(邊緣觸發(fā))模式中,它只會提示一次,直到下次再有數(shù)據(jù)流入之前都不會再提示了,無 論fd中是否還有數(shù)據(jù)可讀。所以在ET模式下,read一個fd的時候一定要把它的buffer讀光,也就是說一直讀到read的返回值小于請求值,或者 遇到EAGAIN錯誤。還有一個特點是,epoll使用“事件”的就緒通知方式,通過epoll_ctl注冊fd,一旦該fd就緒,內(nèi)核就會采用類似callback的回調(diào)機制來激活該fd,epoll_wait便可以收到通知。

epoll為什么要有EPOLLET觸發(fā)模式?

如果采用EPOLLLT模式的話,系統(tǒng)中一旦有大量你不需要讀寫的就緒文件描述符,它們每次調(diào)用epoll_wait都會返回,這樣會大大降低處理程序檢索自己關(guān)心的就緒文件描述符的效率.。而采用EPOLLET這種邊沿觸發(fā)模式的話,當(dāng)被監(jiān)控的文件描述符上有可讀寫事件發(fā)生時,epoll_wait()會通知處理程序去讀寫。如果這次沒有把數(shù)據(jù)全部讀寫完(如讀寫緩沖區(qū)太小),那么下次調(diào)用epoll_wait()時,它不會通知你,也就是它只會通知你一次,直到該文件描述符上出現(xiàn)第二次可讀寫事件才會通知你?。?!這種模式比水平觸發(fā)效率高,系統(tǒng)不會充斥大量你不關(guān)心的就緒文件描述符

epoll的優(yōu)點:

1、沒有最大并發(fā)連接的限制,能打開的FD的上限遠(yuǎn)大于1024(1G的內(nèi)存上能監(jiān)聽約10萬個端口);

2、效率提升,不是輪詢的方式,不會隨著FD數(shù)目的增加效率下降。只有活躍可用的FD才會調(diào)用callback函數(shù);

即Epoll最大的優(yōu)點就在于它只管你“活躍”的連接,而跟連接總數(shù)無關(guān),因此在實際的網(wǎng)絡(luò)環(huán)境中,Epoll的效率就會遠(yuǎn)遠(yuǎn)高于select和poll。

3、 內(nèi)存拷貝,利用mmap()文件映射內(nèi)存加速與內(nèi)核空間的消息傳遞;即epoll使用mmap減少復(fù)制開銷。

select、poll、epoll 區(qū)別總結(jié):

1、支持一個進(jìn)程所能打開的最大連接數(shù)

select

單個進(jìn)程所能打開的最大連接數(shù)有FD_SETSIZE宏定義,其大小是32個整數(shù)的大?。ㄔ?2位的機器上,大小就是3232,同理64位機器上FD_SETSIZE為3264),當(dāng)然我們可以對進(jìn)行修改,然后重新編譯內(nèi)核,但是性能可能會受到影響,這需要進(jìn)一步的測試。

poll

poll本質(zhì)上和select沒有區(qū)別,但是它沒有最大連接數(shù)的限制,原因是它是基于鏈表來存儲的

epoll

雖然連接數(shù)有上限,但是很大,1G內(nèi)存的機器上可以打開10萬左右的連接,2G內(nèi)存的機器可以打開20萬左右的連接

2、FD劇增后帶來的IO效率問題

select

因為每次調(diào)用時都會對連接進(jìn)行線性遍歷,所以隨著FD的增加會造成遍歷速度慢的“線性下降性能問題”。

poll

同上

epoll

因為epoll內(nèi)核中實現(xiàn)是根據(jù)每個fd上的callback函數(shù)來實現(xiàn)的,只有活躍的socket才會主動調(diào)用callback,所以在活躍socket較少的情況下,使用epoll沒有前面兩者的線性下降的性能問題,但是所有socket都很活躍的情況下,可能會有性能問題。

3、 消息傳遞方式

select

內(nèi)核需要將消息傳遞到用戶空間,都需要內(nèi)核拷貝動作

poll

同上

epoll

epoll通過內(nèi)核和用戶空間共享一塊內(nèi)存來實現(xiàn)的。

總結(jié):

綜上,在選擇select,poll,epoll時要根據(jù)具體的使用場合以及這三種方式的自身特點。

1、表面上看epoll的性能最好,但是在連接數(shù)少并且連接都十分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機制需要很多函數(shù)回調(diào)。

2、select低效是因為每次它都需要輪詢。但低效也是相對的,視情況而定,也可通過良好的設(shè)計改善

今天對這三種IO多路復(fù)用進(jìn)行對比,參考網(wǎng)上和書上面的資料,整理如下:

1、select實現(xiàn)

select的調(diào)用過程如下所示:

c088e39a-716d-11ee-939d-92fbcf53809c.jpg

(1)使用copy_from_user從用戶空間拷貝fd_set到內(nèi)核空間

(2)注冊回調(diào)函數(shù)__pollwait

(3)遍歷所有fd,調(diào)用其對應(yīng)的poll方法(對于socket,這個poll方法是sock_poll,sock_poll根據(jù)情況會調(diào)用到tcp_poll,udp_poll或者datagram_poll)

(4)以tcp_poll為例,其核心實現(xiàn)就是__pollwait,也就是上面注冊的回調(diào)函數(shù)。

(5)__pollwait的主要工作就是把current(當(dāng)前進(jìn)程)掛到設(shè)備的等待隊列中,不同的設(shè)備有不同的等待隊列,對于tcp_poll來說,其等待隊列是sk->sk_sleep(注意把進(jìn)程掛到等待隊列中并不代表進(jìn)程已經(jīng)睡眠了)。在設(shè)備收到一條消息(網(wǎng)絡(luò)設(shè)備)或填寫完文件數(shù)據(jù)(磁盤設(shè)備)后,會喚醒設(shè)備等待隊列上睡眠的進(jìn)程,這時current便被喚醒了。

(6)poll方法返回時會返回一個描述讀寫操作是否就緒的mask掩碼,根據(jù)這個mask掩碼給fd_set賦值。

(7)如果遍歷完所有的fd,還沒有返回一個可讀寫的mask掩碼,則會調(diào)用schedule_timeout是調(diào)用select的進(jìn)程(也就是current)進(jìn)入睡眠。當(dāng)設(shè)備驅(qū)動發(fā)生自身資源可讀寫后,會喚醒其等待隊列上睡眠的進(jìn)程。如果超過一定的超時時間(schedule_timeout指定),還是沒人喚醒,則調(diào)用select的進(jìn)程會重新被喚醒獲得CPU,進(jìn)而重新遍歷fd,判斷有沒有就緒的fd。

(8)把fd_set從內(nèi)核空間拷貝到用戶空間。

總結(jié):

select的幾大缺點:

(1)每次調(diào)用select,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài),這個開銷在fd很多時會很大

(2)同時每次調(diào)用select都需要在內(nèi)核遍歷傳遞進(jìn)來的所有fd,這個開銷在fd很多時也很大

(3)select支持的文件描述符數(shù)量太小了,默認(rèn)是1024

2 poll實現(xiàn)

poll的實現(xiàn)和select非常相似,只是描述fd集合的方式不同,poll使用pollfd結(jié)構(gòu)而不是select的fd_set結(jié)構(gòu),其他的都差不多,管理多個描述符也是進(jìn)行輪詢,根據(jù)描述符的狀態(tài)進(jìn)行處理,但是poll沒有最大文件描述符數(shù)量的限制。poll和select同樣存在一個缺點就是,包含大量文件描述符的數(shù)組被整體復(fù)制于用戶態(tài)和內(nèi)核的地址空間之間,而不論這些文件描述符是否就緒,它的開銷隨著文件描述符數(shù)量的增加而線性增大。

3、epoll

epoll既然是對select和poll的改進(jìn),就應(yīng)該能避免上述的三個缺點。那epoll都是怎么解決的呢?在此之前,我們先看一下epoll和select和poll的調(diào)用接口上的不同,select和poll都只提供了一個函數(shù)——select或者poll函數(shù)。而epoll提供了三個函數(shù),epoll_create,epoll_ctl和epoll_wait,epoll_create是創(chuàng)建一個epoll句柄;epoll_ctl是注冊要監(jiān)聽的事件類型;epoll_wait則是等待事件的產(chǎn)生。

對于第一個缺點,epoll的解決方案在epoll_ctl函數(shù)中。每次注冊新的事件到epoll句柄中時(在epoll_ctl中指定EPOLL_CTL_ADD),會把所有的fd拷貝進(jìn)內(nèi)核,而不是在epoll_wait的時候重復(fù)拷貝。epoll保證了每個fd在整個過程中只會拷貝一次。

對于第二個缺點,epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對應(yīng)的設(shè)備等待隊列中,而只在epoll_ctl時把current掛一遍(這一遍必不可少)并為每個fd指定一個回調(diào)函數(shù),當(dāng)設(shè)備就緒,喚醒等待隊列上的等待者時,就會調(diào)用這個回調(diào)函數(shù),而這個回調(diào)函數(shù)會把就緒的fd加入一個就緒鏈表)。epoll_wait的工作實際上就是在這個就緒鏈表中查看有沒有就緒的fd(利用schedule_timeout()實現(xiàn)睡一會,判斷一會的效果,和select實現(xiàn)中的第7步是類似的)。

對于第三個缺點,epoll沒有這個限制,它所支持的FD上限是最大可以打開文件的數(shù)目,這個數(shù)字一般遠(yuǎn)大于2048,舉個例子,在1GB內(nèi)存的機器上大約是10萬左右,具體數(shù)目可以cat /proc/sys/fs/file-max察看,一般來說這個數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大。

總結(jié):

(1)select,poll實現(xiàn)需要自己不斷輪詢所有fd集合,直到設(shè)備就緒,期間可能要睡眠和喚醒多次交替。而epoll其實也需要調(diào)用epoll_wait不斷輪詢就緒鏈表,期間也可能多次睡眠和喚醒交替,但是它是設(shè)備就緒時,調(diào)用回調(diào)函數(shù),把就緒fd放入就緒鏈表中,并喚醒在epoll_wait中進(jìn)入睡眠的進(jìn)程。雖然都要睡眠和交替,但是select和poll在“醒著”的時候要遍歷整個fd集合,而epoll在“醒著”的時候只要判斷一下就緒鏈表是否為空就行了,這節(jié)省了大量的CPU時間。這就是回調(diào)機制帶來的性能提升。

(2)select,poll每次調(diào)用都要把fd集合從用戶態(tài)往內(nèi)核態(tài)拷貝一次,并且要把current往設(shè)備等待隊列中掛一次,而epoll只要一次拷貝,而且把current往等待隊列上掛也只掛一次(在epoll_wait的開始,注意這里的等待隊列并不是設(shè)備等待隊列,只是一個epoll內(nèi)部定義的等待隊列)。這也能節(jié)省不少的開銷。

審核編輯:黃飛

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 服務(wù)器
    +關(guān)注

    關(guān)注

    12

    文章

    8700

    瀏覽量

    84539
  • 路由器
    +關(guān)注

    關(guān)注

    22

    文章

    3640

    瀏覽量

    112800
  • epoll
    +關(guān)注

    關(guān)注

    0

    文章

    28

    瀏覽量

    2938
  • select
    +關(guān)注

    關(guān)注

    0

    文章

    28

    瀏覽量

    3891

原文標(biāo)題:select、poll、epoll之間的區(qū)別(搜狗面試)

文章出處:【微信號:LinuxHub,微信公眾號:Linux愛好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    基于Select/Poll實現(xiàn)并發(fā)服務(wù)器(二)

    LWIP:2.0.2 3 Select/Poll概述 在LWIP中,如果要實現(xiàn)并發(fā)服務(wù)器,可以基于Sequentaial API來實現(xiàn),這種方式需要使用多線程,也就是為每個連接創(chuàng)建一個線程來處理數(shù)據(jù)。而在
    的頭像 發(fā)表于 06-20 00:26 ?4630次閱讀
    基于<b class='flag-5'>Select</b>/<b class='flag-5'>Poll</b>實現(xiàn)并發(fā)服務(wù)器(二)

    epoll的使用

    API可以檢查多個文件描述符上的I/O就緒狀態(tài)。epoll API的主要優(yōu)點1.當(dāng)有大量的文件描述符需要檢查時,epoll的性能延展性比select()和epoll(高很多)2.
    發(fā)表于 05-11 13:22

    我讀過的最好的epoll講解

    select以及epoll)處理甚至直接忽略。 為了避免CPU空轉(zhuǎn),可以引進(jìn)了一個代理(一開始有一位叫做select的代理,后來又有一位叫做poll的代理,不過兩者的本質(zhì)是一樣的)。
    發(fā)表于 05-12 15:30

    epoll使用方法與poll區(qū)別

    因為epoll的觸發(fā)機制是在內(nèi)核中直接完成整個功能 那個事件準(zhǔn)備就緒我就直接返回這個IO事件
    發(fā)表于 07-31 10:03

    揭示EPOLL一些原理性的東西

    事件交給其他對象(后文介紹的select以及epoll)處理甚至直接忽略。為了避免CPU空轉(zhuǎn),可以引進(jìn)了一個代理(一開始有一位叫做select的代理,后來又有一位叫做poll的代理,不
    發(fā)表于 08-24 16:32

    epollselect區(qū)別

     select,epoll都是IO多路復(fù)用的機制。I/O多路復(fù)用就通過一種機制,可以監(jiān)視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進(jìn)行相應(yīng)的讀寫操作。但select
    發(fā)表于 11-10 16:20 ?2.1w次閱讀
    <b class='flag-5'>epoll</b>和<b class='flag-5'>select</b>的<b class='flag-5'>區(qū)別</b>

    關(guān)于Epoll,你應(yīng)該知道的那些細(xì)節(jié)

    Epoll,位于頭文件sys/epoll.h,是Linux系統(tǒng)上的I/O事件通知基礎(chǔ)設(shè)施。epoll API為Linux系統(tǒng)專有,于內(nèi)核2.5.44中首次引入,glibc于2.3.2版本加入支持。其它提供類似的功能的系統(tǒng),包括F
    發(fā)表于 05-12 09:25 ?1135次閱讀

    poll&&epollepoll實現(xiàn)

    poll&&epollepoll實現(xiàn)
    發(fā)表于 05-14 14:34 ?2737次閱讀
    <b class='flag-5'>poll</b>&&<b class='flag-5'>epoll</b>之<b class='flag-5'>epoll</b>實現(xiàn)

    詳細(xì)解讀Linux內(nèi)核的poll機制

    對于系統(tǒng)調(diào)用pollselect,它們對應(yīng)的內(nèi)核函數(shù)都是sys_poll。分析sys_poll,即可理解poll機制。
    發(fā)表于 05-14 16:22 ?3973次閱讀
    詳細(xì)解讀Linux內(nèi)核的<b class='flag-5'>poll</b>機制

    Linux內(nèi)核中select, pollepoll區(qū)別

    先說poll,pollselect為大部分Unix/Linux程序員所熟悉,這倆個東西原理類似,性能上也不存在明顯差異,但select對所監(jiān)控的文件描述符數(shù)量有限制,所以這里選用
    發(fā)表于 05-14 16:24 ?1646次閱讀

    Linux中epoll IO多路復(fù)用機制

    epoll 是Linux內(nèi)核中的一種可擴(kuò)展IO事件處理機制,最早在 Linux 2.5.44內(nèi)核中引入,可被用于代替POSIX selectpoll 系統(tǒng)調(diào)用,并且在具有大量應(yīng)用程序請求時能夠
    發(fā)表于 05-16 16:07 ?664次閱讀
    Linux中<b class='flag-5'>epoll</b> IO多路復(fù)用機制

    epoll LT和ET方式下的讀寫差別

    epoll接口是為解決Linux內(nèi)核處理大量文件描述符而提出的方案。該接口屬于Linux下多路I/O復(fù)用接口中select/poll的增強。
    的頭像 發(fā)表于 07-07 10:34 ?1936次閱讀

    epollselect使用區(qū)別

    epollselect 相比于select,epoll最大的好處在于它不會隨著監(jiān)聽fd數(shù)目的增長而降低效率。因為在內(nèi)核中的select
    的頭像 發(fā)表于 11-09 14:14 ?753次閱讀
    <b class='flag-5'>epoll</b>和<b class='flag-5'>select</b>使用<b class='flag-5'>區(qū)別</b>

    epoll底層如何使用紅黑樹

    epollpoll的一個很大的區(qū)別在于,poll每次調(diào)用時都會存在一個將pollfd結(jié)構(gòu)體數(shù)組中的每個結(jié)構(gòu)體元素從用戶態(tài)向內(nèi)核態(tài)中的一個鏈表節(jié)點拷貝的過程,而內(nèi)核中的這個鏈表并不會一
    的頭像 發(fā)表于 11-10 15:13 ?585次閱讀
    <b class='flag-5'>epoll</b>底層如何使用紅黑樹

    Epoll封裝類實現(xiàn)

    關(guān)于epoll的原理,以及和poll、select、IOCP之間的比較,網(wǎng)上的資料很多,這些都屬于I/O復(fù)用的實現(xiàn)方法,即可以同時監(jiān)聽發(fā)生在多個I/O端口(socket套接字描述符或文件描述符
    的頭像 發(fā)表于 11-13 11:54 ?425次閱讀