I/O模型之二:Linux IO模式及 select、poll、epoll詳解

本文討論的背景是Linux環(huán)境下的network IO。

一、 概念說明

在進(jìn)行解釋之前,首先要說明幾個(gè)概念:

  • 用戶空間和內(nèi)核空間
  • 進(jìn)程切換
  • 進(jìn)程的阻塞
  • 文件描述符
  • 緩存 I/O
1.1、用戶空間與內(nèi)核空間

現(xiàn)在操作系統(tǒng)都是采用虛擬存儲(chǔ)器,那么對(duì)32位操作系統(tǒng)而言,它的尋址空間(虛擬存儲(chǔ)空間)為4G(2的32次方)。操作系統(tǒng)的核心是內(nèi)核,獨(dú)立于普通的應(yīng)用程序,可以訪問受保護(hù)的內(nèi)存空間,也有訪問底層硬件設(shè)備的所有權(quán)限。為了保證用戶進(jìn)程不能直接操作內(nèi)核(kernel),保證內(nèi)核的安全,操心系統(tǒng)將虛擬空間劃分為兩部分,一部分為內(nèi)核空間,一部分為用戶空間。針對(duì)linux操作系統(tǒng)而言,將最高的1G字節(jié)(從虛擬地址0xC0000000到0xFFFFFFFF),供內(nèi)核使用,稱為內(nèi)核空間,而將較低的3G字節(jié)(從虛擬地址0x00000000到0xBFFFFFFF),供各個(gè)進(jìn)程使用,稱為用戶空間。

1.2、進(jìn)程切換

為了控制進(jìn)程的執(zhí)行,內(nèi)核必須有能力掛起正在CPU上運(yùn)行的進(jìn)程,并恢復(fù)以前掛起的某個(gè)進(jìn)程的執(zhí)行。這種行為被稱為進(jìn)程切換。因此可以說,任何進(jìn)程都是在操作系統(tǒng)內(nèi)核的支持下運(yùn)行的,是與內(nèi)核緊密相關(guān)的。

從一個(gè)進(jìn)程的運(yùn)行轉(zhuǎn)到另一個(gè)進(jìn)程上運(yùn)行,這個(gè)過程中經(jīng)過下面這些變化:
1. 保存處理機(jī)上下文,包括程序計(jì)數(shù)器和其他寄存器。
2. 更新PCB信息。
3. 把進(jìn)程的PCB移入相應(yīng)的隊(duì)列,如就緒、在某事件阻塞等隊(duì)列。
4. 選擇另一個(gè)進(jìn)程執(zhí)行,并更新其PCB。
5. 更新內(nèi)存管理的數(shù)據(jù)結(jié)構(gòu)。
6. 恢復(fù)處理機(jī)上下文。

注:總而言之就是很耗資源,具體的可以參考這篇文章:進(jìn)程切換

1.3、進(jìn)程的阻塞

正在執(zhí)行的進(jìn)程,由于期待的某些事件未發(fā)生,如請(qǐng)求系統(tǒng)資源失敗、等待某種操作的完成、新數(shù)據(jù)尚未到達(dá)或無新工作做等,則由系統(tǒng)自動(dòng)執(zhí)行阻塞原語(Block),使自己由運(yùn)行狀態(tài)變?yōu)樽枞麪顟B(tài)??梢?,進(jìn)程的阻塞是進(jìn)程自身的一種主動(dòng)行為,也因此只有處于運(yùn)行態(tài)的進(jìn)程(獲得CPU),才可能將其轉(zhuǎn)為阻塞狀態(tài)。當(dāng)進(jìn)程進(jìn)入阻塞狀態(tài),是不占用CPU資源的。

1.4、文件描述符fd

文件描述符(File descriptor)是計(jì)算機(jī)科學(xué)中的一個(gè)術(shù)語,是一個(gè)用于表述指向文件的引用的抽象化概念。

文件描述符在形式上是一個(gè)非負(fù)整數(shù)。實(shí)際上,它是一個(gè)索引值,指向內(nèi)核為每一個(gè)進(jìn)程所維護(hù)的該進(jìn)程打開文件的記錄表。當(dāng)程序打開一個(gè)現(xiàn)有文件或者創(chuàng)建一個(gè)新文件時(shí),內(nèi)核向進(jìn)程返回一個(gè)文件描述符。在程序設(shè)計(jì)中,一些涉及底層的程序編寫往往會(huì)圍繞著文件描述符展開。但是文件描述符這一概念往往只適用于UNIX、Linux這樣的操作系統(tǒng)。

1.5、緩存 I/O

緩存 I/O 又被稱作標(biāo)準(zhǔn) I/O,大多數(shù)文件系統(tǒng)的默認(rèn) I/O 操作都是緩存 I/O。在 Linux 的緩存 I/O 機(jī)制中,操作系統(tǒng)會(huì)將 I/O 的數(shù)據(jù)緩存在文件系統(tǒng)的頁(yè)緩存( page cache )中,也就是說,數(shù)據(jù)會(huì)先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中,然后才會(huì)從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的地址空間。
緩存 I/O 的缺點(diǎn):
數(shù)據(jù)在傳輸過程中需要在應(yīng)用程序地址空間和內(nèi)核進(jìn)行多次數(shù)據(jù)拷貝操作,這些數(shù)據(jù)拷貝操作所帶來的 CPU 以及內(nèi)存開銷是非常大的。

二、 IO模式

剛才說了,對(duì)于一次IO訪問(以read舉例),數(shù)據(jù)會(huì)先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中,然后才會(huì)從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的地址空間。所以說,當(dāng)一個(gè)read操作發(fā)生時(shí),它會(huì)經(jīng)歷兩個(gè)階段:

  1. 等待數(shù)據(jù)準(zhǔn)備 (Waiting for the data to be ready)
  2. 將數(shù)據(jù)從內(nèi)核拷貝到進(jìn)程中 (Copying the data from the kernel to the process)
    正式因?yàn)檫@兩個(gè)階段,linux系統(tǒng)產(chǎn)生了下面五種網(wǎng)絡(luò)模式的方案。
  • 阻塞 I/O(blocking IO)
  • 非阻塞 I/O(nonblocking IO)
  • I/O 多路復(fù)用( IO multiplexing)
  • 信號(hào)驅(qū)動(dòng) I/O( signal driven IO)
  • 異步 I/O(asynchronous IO)
    注:由于signal driven IO在實(shí)際中并不常用,所以我這只提及剩下的四種IO Model。

三、 I/O 多路復(fù)用之select、poll、epoll詳解

select,poll,epoll都是IO多路復(fù)用的機(jī)制。I/O多路復(fù)用就是通過一種機(jī)制,一個(gè)進(jìn)程可以監(jiān)視多個(gè)描述符,一旦某個(gè)描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進(jìn)行相應(yīng)的讀寫操作。但select,poll,epoll本質(zhì)上都是同步I/O,因?yàn)樗麄兌夹枰谧x寫事件就緒后自己負(fù)責(zé)進(jìn)行讀寫,也就是說這個(gè)讀寫過程是阻塞的,而異步I/O則無需自己負(fù)責(zé)進(jìn)行讀寫,異步I/O的實(shí)現(xiàn)會(huì)負(fù)責(zé)把數(shù)據(jù)從內(nèi)核拷貝到用戶空間。(這里啰嗦下)

3.1、select

select是1983年的4.2BSD提出。系統(tǒng)在select用32*32=1024位來進(jìn)行查詢。返回的時(shí)候數(shù)組如readfds是已經(jīng)處理過的了,返回時(shí)只有準(zhǔn)備好事件的fd。所以需要輪訓(xùn)(要用FD_ISSET挨個(gè)比較)和重新賦值。FD_ISSET(fd,&readfds)

int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

select 函數(shù)監(jiān)視的文件描述符分3類,分別是writefds、readfds、和exceptfds。調(diào)用后select函數(shù)會(huì)阻塞,直到有描述符就緒(有數(shù)據(jù) 可讀、可寫、或者有except),或者超時(shí)(timeout指定等待時(shí)間,如果立即返回設(shè)為null即可),函數(shù)返回。當(dāng)select函數(shù)返回后,可以 通過遍歷fdset,來找到就緒的描述符。

使用方法總共分三步:

1.三個(gè)fd_set初始化,用FD_ZERO FD_SET
2.調(diào)用select
3.用fd遍歷每一個(gè)fd_set使用FD_ISSET。如果成功就處理。

select目前幾乎在所有的平臺(tái)上支持,其良好跨平臺(tái)支持也是它的一個(gè)優(yōu)點(diǎn)。select的一個(gè)缺點(diǎn)在于單個(gè)進(jìn)程能夠監(jiān)視的文件描述符的數(shù)量存在最大限制,在Linux上一般為1024,可以通過修改宏定義甚至重新編譯內(nèi)核的方式提升這一限制,但 是這樣也會(huì)造成效率的降低。

3.2、poll
int poll (struct pollfd *fds, unsigned int nfds, int timeout);

不同與select使用三個(gè)位圖來表示三個(gè)fdset的方式,poll使用一個(gè) pollfd的指針實(shí)現(xiàn)。

struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
};

pollfd結(jié)構(gòu)包含了要監(jiān)視的event和發(fā)生的event,不再使用select“參數(shù)-值”傳遞的方式。同時(shí),pollfd并沒有最大數(shù)量限制(但是數(shù)量過大后性能也是會(huì)下降)。 和select函數(shù)一樣,poll返回后,需要輪詢pollfd來獲取就緒的描述符。
從上面看,select和poll都需要在返回后,通過遍歷文件描述符來獲取已經(jīng)就緒的socket。事實(shí)上,同時(shí)連接的大量客戶端在一時(shí)刻可能只有很少的處于就緒狀態(tài),因此隨著監(jiān)視的描述符數(shù)量的增長(zhǎng),其效率也會(huì)線性下降。
也是分三步
1.pollfd初始化,綁定sock,設(shè)置事件event,revent。設(shè)置時(shí)間限制。
2.調(diào)用poll
3.遍歷看他的事件發(fā)生了么,如果發(fā)生了置0。

3.3、epoll

epoll:一次循環(huán)
epoll是在2.6內(nèi)核中提出的,是之前的select和poll的增強(qiáng)版本。而且只在linux下支持。相對(duì)于select和poll來說,epoll更加靈活,沒有描述符限制。epoll使用一個(gè)文件描述符管理多個(gè)描述符,將用戶關(guān)系的文件描述符的事件存放到內(nèi)核的一個(gè)事件表中,這樣在用戶空間和內(nèi)核空間的copy只需一次。
epoll是直接在內(nèi)核里的,用戶調(diào)用系統(tǒng)調(diào)用去注冊(cè),因此省去了每次的復(fù)制和輪詢的消耗。這兒用了三個(gè)系統(tǒng)調(diào)用,epollcreate只要每次調(diào)用開始調(diào)用一次創(chuàng)造一個(gè)epoll就可以了。然后用epoll_ctl來進(jìn)行添加事件,其實(shí)就是注冊(cè)到內(nèi)核管理的epoll里。然后直接epoll_wait就可以了。系統(tǒng)會(huì)返回系統(tǒng)調(diào)用的。
使用方法
1.準(zhǔn)備工作多了,很復(fù)雜,這個(gè)記錄數(shù)據(jù)在內(nèi)核里。
1)構(gòu)建epoll描述符,通過調(diào)用epoll_create
2)用需要的時(shí)間和上下文數(shù)據(jù)指針初始化。
3)調(diào)用epoll_ctl 添加文件描述符。
4)調(diào)用epoll_wait每次處理20個(gè)事件。這兒是接收一個(gè)空數(shù)組,然后填上東西。也就是有200個(gè)東西過來,我可能只填了一個(gè)。當(dāng)然如果50個(gè)完成了也是回復(fù)20.剩下的不會(huì)被漏掉,下次再來處理。
5)遍歷返回的數(shù)據(jù)。注意這兒返回的都是有用的東西。

3.3.1、 epoll操作過程

epoll操作過程需要三個(gè)接口,分別如下:

int epoll_create(int size);//創(chuàng)建一個(gè)epoll的句柄,size用來告訴內(nèi)核這個(gè)監(jiān)聽的數(shù)目一共有多大
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);
  1. int epoll_create(int size);
    創(chuàng)建一個(gè)epoll的句柄,size用來告訴內(nèi)核這個(gè)監(jiān)聽的數(shù)目一共有多大,這個(gè)參數(shù)不同于select()中的第一個(gè)參數(shù),給出最大監(jiān)聽的fd+1的值,參數(shù)size并不是限制了epoll所能監(jiān)聽的描述符最大個(gè)數(shù),只是對(duì)內(nèi)核初始分配內(nèi)部數(shù)據(jù)結(jié)構(gòu)的一個(gè)建議。
    當(dāng)創(chuàng)建好epoll句柄后,它就會(huì)占用一個(gè)fd值,在linux下如果查看/proc/進(jìn)程id/fd/,是能夠看到這個(gè)fd的,所以在使用完epoll后,必須調(diào)用close()關(guān)閉,否則可能導(dǎo)致fd被耗盡。

  2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    函數(shù)是對(duì)指定描述符fd執(zhí)行op操作。

  • epfd:是epoll_create()的返回值。
  • op:表示op操作,用三個(gè)宏來表示:添加EPOLL_CTL_ADD,刪除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分別添加、刪除和修改對(duì)fd的監(jiān)聽事件。
  • fd:是需要監(jiān)聽的fd(文件描述符)
  • epoll_event:是告訴內(nèi)核需要監(jiān)聽什么事,struct epoll_event結(jié)構(gòu)如下:

struct epoll_event {
__uint32_t events; /* Epoll events /
epoll_data_t data; /
User data variable */
};
//events可以是以下幾個(gè)宏的集合:
EPOLLIN :表示對(duì)應(yīng)的文件描述符可以讀(包括對(duì)端SOCKET正常關(guān)閉);
EPOLLOUT:表示對(duì)應(yīng)的文件描述符可以寫;
EPOLLPRI:表示對(duì)應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來);
EPOLLERR:表示對(duì)應(yīng)的文件描述符發(fā)生錯(cuò)誤;
EPOLLHUP:表示對(duì)應(yīng)的文件描述符被掛斷;
EPOLLET: 將EPOLL設(shè)為邊緣觸發(fā)(Edge Triggered)模式,這是相對(duì)于水平觸發(fā)(Level Triggered)來說的。
EPOLLONESHOT:只監(jiān)聽一次事件,當(dāng)監(jiān)聽完這次事件之后,如果還需要繼續(xù)監(jiān)聽這個(gè)socket的話,需要再次把這個(gè)socket加入到EPOLL隊(duì)列里

  1. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
    等待epfd上的io事件,最多返回maxevents個(gè)事件。
    參數(shù)events用來從內(nèi)核得到事件的集合,maxevents告之內(nèi)核這個(gè)events有多大,這個(gè)maxevents的值不能大于創(chuàng)建epoll_create()時(shí)的size,參數(shù)timeout是超時(shí)時(shí)間(毫秒,0會(huì)立即返回,-1將不確定,也有說法說是永久阻塞)。該函數(shù)返回需要處理的事件數(shù)目,如返回0表示已超時(shí)。
epoll操作代碼演示
#define IPADDRESS   "127.0.0.1"
#define PORT        8787
#define MAXSIZE     1024
#define LISTENQ     5
#define FDSIZE      1000
#define EPOLLEVENTS 100

listenfd = socket_bind(IPADDRESS,PORT);

struct epoll_event events[EPOLLEVENTS];

//創(chuàng)建一個(gè)描述符
epollfd = epoll_create(FDSIZE);

//添加監(jiān)聽描述符事件
add_event(epollfd,listenfd,EPOLLIN);

//循環(huán)等待
for ( ; ; ){
    //該函數(shù)返回已經(jīng)準(zhǔn)備好的描述符事件數(shù)目
    ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1);
    //處理接收到的連接
    handle_events(epollfd,events,ret,listenfd,buf);
}

//事件處理函數(shù)
static void handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf)
{
     int i;
     int fd;
     //進(jìn)行遍歷;這里只要遍歷已經(jīng)準(zhǔn)備好的io事件。num并不是當(dāng)初epoll_create時(shí)的FDSIZE。
     for (i = 0;i < num;i++)
     {
         fd = events[i].data.fd;
        //根據(jù)描述符的類型和事件類型進(jìn)行處理
         if ((fd == listenfd) &&(events[i].events & EPOLLIN))
            handle_accpet(epollfd,listenfd);
         else if (events[i].events & EPOLLIN)
            do_read(epollfd,fd,buf);
         else if (events[i].events & EPOLLOUT)
            do_write(epollfd,fd,buf);
     }
}

//添加事件
static void add_event(int epollfd,int fd,int state){
    struct epoll_event ev;
    ev.events = state;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);
}

//處理接收到的連接
static void handle_accpet(int epollfd,int listenfd){
     int clifd;     
     struct sockaddr_in cliaddr;     
     socklen_t  cliaddrlen;     
     clifd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddrlen);     
     if (clifd == -1)         
     perror("accpet error:");     
     else {         
         printf("accept a new client: %s:%d\n",inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port);                       //添加一個(gè)客戶描述符和事件         
         add_event(epollfd,clifd,EPOLLIN);     
     } 
}

//讀處理
static void do_read(int epollfd,int fd,char *buf){
    int nread;
    nread = read(fd,buf,MAXSIZE);
    if (nread == -1)     {         
        perror("read error:");         
        close(fd); //記住close fd        
        delete_event(epollfd,fd,EPOLLIN); //刪除監(jiān)聽 
    }
    else if (nread == 0)     {         
        fprintf(stderr,"client close.\n");
        close(fd); //記住close fd       
        delete_event(epollfd,fd,EPOLLIN); //刪除監(jiān)聽 
    }     
    else {         
        printf("read message is : %s",buf);        
        //修改描述符對(duì)應(yīng)的事件,由讀改為寫         
        modify_event(epollfd,fd,EPOLLOUT);     
    } 
}

//寫處理
static void do_write(int epollfd,int fd,char *buf) {     
    int nwrite;     
    nwrite = write(fd,buf,strlen(buf));     
    if (nwrite == -1){         
        perror("write error:");        
        close(fd);   //記住close fd       
        delete_event(epollfd,fd,EPOLLOUT);  //刪除監(jiān)聽    
    }else{
        modify_event(epollfd,fd,EPOLLIN); 
    }    
    memset(buf,0,MAXSIZE); 
}

//刪除事件
static void delete_event(int epollfd,int fd,int state) {
    struct epoll_event ev;
    ev.events = state;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev);
}

//修改事件
static void modify_event(int epollfd,int fd,int state){     
    struct epoll_event ev;
    ev.events = state;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev);
}

//注:另外一端我就省了

epoll總結(jié)

在 select/poll中,進(jìn)程只有在調(diào)用一定的方法后,內(nèi)核才對(duì)所有監(jiān)視的文件描述符進(jìn)行掃描,而epoll事先通過epoll_ctl()來注冊(cè)一 個(gè)文件描述符,一旦基于某個(gè)文件描述符就緒時(shí),內(nèi)核會(huì)采用類似callback的回調(diào)機(jī)制,迅速激活這個(gè)文件描述符,當(dāng)進(jìn)程調(diào)用epoll_wait() 時(shí)便得到通知。(此處去掉了遍歷文件描述符,而是通過監(jiān)聽回調(diào)的的機(jī)制。這正是epoll的魅力所在。)

epoll的優(yōu)點(diǎn)主要是一下幾個(gè)方面:

  • 監(jiān)視的描述符數(shù)量不受限制,它所支持的FD上限是最大可以打開文件的數(shù)目,這個(gè)數(shù)字一般遠(yuǎn)大于2048,舉個(gè)例子,在1GB內(nèi)存的機(jī)器上大約是10萬左 右,具體數(shù)目可以cat /proc/sys/fs/file-max察看,一般來說這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大。select的最大缺點(diǎn)就是進(jìn)程打開的fd是有數(shù)量限制的。這對(duì) 于連接數(shù)量比較大的服務(wù)器來說根本不能滿足。雖然也可以選擇多進(jìn)程的解決方案( Apache就是這樣實(shí)現(xiàn)的),不過雖然linux上面創(chuàng)建進(jìn)程的代價(jià)比較小,但仍舊是不可忽視的,加上進(jìn)程間數(shù)據(jù)同步遠(yuǎn)比不上線程間同步的高效,所以也不是一種完美的方案。
  • IO的效率不會(huì)隨著監(jiān)視fd的數(shù)量的增長(zhǎng)而下降。epoll不同于select和poll輪詢的方式,而是通過每個(gè)fd定義的回調(diào)函數(shù)來實(shí)現(xiàn)的。只有就緒的fd才會(huì)執(zhí)行回調(diào)函數(shù)。
  • 如果沒有大量的idle -connection或者dead-connection,epoll的效率并不會(huì)比select/poll高很多,但是當(dāng)遇到大量的idle- connection,就會(huì)發(fā)現(xiàn)epoll的效率大大高于select/poll。

select,poll,epoll區(qū)別

select的缺點(diǎn):
單個(gè)進(jìn)程能夠監(jiān)視的文件描述符的數(shù)量存在最大限制,通常是1024,當(dāng)然可以更改數(shù)量,但由于select采用輪詢的方式掃描文件描述符,文件描述符數(shù)量越多,性能越差;(在linux內(nèi)核頭文件中,有這樣的定義:#define __FD_SETSIZE 1024)
內(nèi)核 / 用戶空間內(nèi)存拷貝問題,select需要復(fù)制大量的句柄數(shù)據(jù)結(jié)構(gòu),產(chǎn)生巨大的開銷;
select返回的是含有整個(gè)句柄的數(shù)組,應(yīng)用程序需要遍歷整個(gè)數(shù)組才能發(fā)現(xiàn)哪些句柄發(fā)生了事件;
select的觸發(fā)方式是水平觸發(fā),應(yīng)用程序如果沒有完成對(duì)一個(gè)已經(jīng)就緒的文件描述符進(jìn)行IO操作,那么之后每次select調(diào)用還是會(huì)將這些文件描述符通知進(jìn)程。
poll:
優(yōu)勢(shì):
1.無上限1024。
2.由于它不修改pollfd里的數(shù)據(jù),所以它可以不用每次都填寫了。
3.方便的知道遠(yuǎn)程的狀態(tài)比如宕機(jī)
缺點(diǎn):
1、還要輪巡
2、不能動(dòng)態(tài)修改set。
其實(shí)大多數(shù)client不用考慮這個(gè),除非p2p應(yīng)用。一些server端用不用考慮這個(gè)問題。
大多時(shí)候他都比select更好。甚至如下場(chǎng)景比epoll還好:
你要跨平臺(tái),因?yàn)閑poll只支持linux。
socket數(shù)目少于1000個(gè)。
大于1000但是是socket壽命比較短。
沒有其他線程干擾的時(shí)候。
相比select模型,poll使用鏈表保存文件描述符,因此沒有了監(jiān)視文件數(shù)量的限制,但select三個(gè)缺點(diǎn)依然存在。

拿select模型為例,假設(shè)我們的服務(wù)器需要支持100萬的并發(fā)連接,則在__FD_SETSIZE 為1024的情況下,則我們至少需要開辟1k個(gè)進(jìn)程才能實(shí)現(xiàn)100萬的并發(fā)連接。除了進(jìn)程間上下文切換的時(shí)間消耗外,從內(nèi)核/用戶空間大量的無腦內(nèi)存拷貝、數(shù)組輪詢等,是系統(tǒng)難以承受的。因此,基于select模型的服務(wù)器程序,要達(dá)到10萬級(jí)別的并發(fā)訪問,是一個(gè)很難完成的任務(wù)。
epoll:
優(yōu)點(diǎn):
1.只返回觸發(fā)的事件。少了拷貝消耗,迭代輪訓(xùn)消耗。
2.可以綁定更多上下文,不僅僅是socket。
3.任何時(shí)間處理socket。這些問題都是有內(nèi)核來處理。了。這個(gè)還需要繼續(xù)學(xué)習(xí)啊。
4.可以邊緣觸發(fā)。
5.多線程可以在同一個(gè)epoll wait里等待。
缺點(diǎn):
1.讀寫狀態(tài)變更之類的就要麻煩些,在poll里只要改一個(gè)bit就可以了。在這里面則需要改更多的位數(shù)。并且都是system call。
2.創(chuàng)建socket也需要兩次系統(tǒng)調(diào)用,麻煩。
3.只有l(wèi)inux下可以使用
4.復(fù)雜難調(diào)試
適合場(chǎng)景
1.多線程,多連接。在單線程還不如poll
2.大量線程監(jiān)控1000上,
3.相對(duì)長(zhǎng)壽命的連接。系統(tǒng)調(diào)用會(huì)很耗時(shí)。
4.linux依賴的事情。

epoll IO多路復(fù)用模型實(shí)現(xiàn)機(jī)制

由于epoll的實(shí)現(xiàn)機(jī)制與select/poll機(jī)制完全不同,上面所說的 select的缺點(diǎn)在epoll上不復(fù)存在。
設(shè)想一下如下場(chǎng)景:有100萬個(gè)客戶端同時(shí)與一個(gè)服務(wù)器進(jìn)程保持著TCP連接。而每一時(shí)刻,通常只有幾百上千個(gè)TCP連接是活躍的(事實(shí)上大部分場(chǎng)景都是這種情況)。如何實(shí)現(xiàn)這樣的高并發(fā)?
select/poll時(shí)代,服務(wù)器進(jìn)程每次都把這100萬個(gè)連接告訴操作系統(tǒng)(從用戶態(tài)復(fù)制句柄數(shù)據(jù)結(jié)構(gòu)到內(nèi)核態(tài)),讓操作系統(tǒng)內(nèi)核去查詢這些套接字上是否有事件發(fā)生,輪詢完后,再將句柄數(shù)據(jù)復(fù)制到用戶態(tài),讓服務(wù)器應(yīng)用程序輪詢處理已發(fā)生的網(wǎng)絡(luò)事件,這一過程資源消耗較大,因此,select/poll一般只能處理幾千的并發(fā)連接。
epoll的設(shè)計(jì)和實(shí)現(xiàn)與select完全不同。epoll通過在Linux內(nèi)核中申請(qǐng)一個(gè)簡(jiǎn)易的文件系統(tǒng)(文件系統(tǒng)一般用什么數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)?B+樹)。把原先的select/poll調(diào)用分成了3個(gè)部分:
1)調(diào)用epoll_create()建立一個(gè)epoll對(duì)象(在epoll文件系統(tǒng)中為這個(gè)句柄對(duì)象分配資源)
2)調(diào)用epoll_ctl向epoll對(duì)象中添加這100萬個(gè)連接的套接字
3)調(diào)用epoll_wait收集發(fā)生的事件的連接
如此一來,要實(shí)現(xiàn)上面說是的場(chǎng)景,只需要在進(jìn)程啟動(dòng)時(shí)建立一個(gè)epoll對(duì)象,然后在需要的時(shí)候向這個(gè)epoll對(duì)象中添加或者刪除連接。同時(shí),epoll_wait的效率也非常高,因?yàn)檎{(diào)用epoll_wait時(shí),并沒有一股腦的向操作系統(tǒng)復(fù)制這100萬個(gè)連接的句柄數(shù)據(jù),內(nèi)核也不需要去遍歷全部的連接。
下面來看看Linux內(nèi)核具體的epoll機(jī)制實(shí)現(xiàn)思路
當(dāng)某一進(jìn)程調(diào)用epoll_create方法時(shí),Linux內(nèi)核會(huì)創(chuàng)建一個(gè)eventpoll結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體中有兩個(gè)成員與epoll的使用方式密切相關(guān)。eventpoll結(jié)構(gòu)體如下所示:

struct eventpoll{  
    ....  
    /*紅黑樹的根節(jié)點(diǎn),這顆樹中存儲(chǔ)著所有添加到epoll中的需要監(jiān)控的事件*/  
    struct rb_root  rbr;  
    /*雙鏈表中則存放著將要通過epoll_wait返回給用戶的滿足條件的事件*/  
    struct list_head rdlist;  
    ....  
};  

每一個(gè)epoll對(duì)象都有一個(gè)獨(dú)立的eventpoll結(jié)構(gòu)體,用于存放通過epoll_ctl方法向epoll對(duì)象中添加進(jìn)來的事件。這些事件都會(huì)掛載在紅黑樹中,如此,重復(fù)添加的事件就可以通過紅黑樹而高效的識(shí)別出來(紅黑樹的插入時(shí)間效率是lgn,其中n為樹的高度)。
而所有添加到epoll中的事件都會(huì)與設(shè)備(網(wǎng)卡)驅(qū)動(dòng)程序建立回調(diào)關(guān)系,也就是說,當(dāng)相應(yīng)的事件發(fā)生時(shí)會(huì)調(diào)用這個(gè)回調(diào)方法。這個(gè)回調(diào)方法在內(nèi)核中叫ep_poll_callback,它會(huì)將發(fā)生的事件添加到rdlist雙鏈表中。
在epoll中,對(duì)于每一個(gè)事件,都會(huì)建立一個(gè)epitem結(jié)構(gòu)體,如下所示:

struct epitem{  
    struct rb_node  rbn;//紅黑樹節(jié)點(diǎn)  
    struct list_head    rdllink;//雙向鏈表節(jié)點(diǎn)  
    struct epoll_filefd  ffd;  //事件句柄信息  
    struct eventpoll *ep;    //指向其所屬的eventpoll對(duì)象  
    struct epoll_event event; //期待發(fā)生的事件類型  
}

當(dāng)調(diào)用epoll_wait檢查是否有事件發(fā)生時(shí),只需要檢查eventpoll對(duì)象中的rdlist雙鏈表中是否有epitem元素即可。如果rdlist不為空,則把發(fā)生的事件復(fù)制到用戶態(tài),同時(shí)將事件數(shù)量返回給用戶。


image.png

從上面的講解可知:通過紅黑樹和雙鏈表數(shù)據(jù)結(jié)構(gòu),并結(jié)合回調(diào)機(jī)制,造就了epoll的高效。

OK,講解完了Epoll的機(jī)理,我們便能很容易掌握epoll的用法了。一句話描述就是:三步曲。

第一步:epoll_create()系統(tǒng)調(diào)用。此調(diào)用返回一個(gè)句柄,之后所有的使用都依靠這個(gè)句柄來標(biāo)識(shí)。

第二步:epoll_ctl()系統(tǒng)調(diào)用。通過此調(diào)用向epoll對(duì)象中添加、刪除、修改感興趣的事件,返回0標(biāo)識(shí)成功,返回-1表示失敗。

第三部:epoll_wait()系統(tǒng)調(diào)用。通過此調(diào)用收集收集在epoll監(jiān)控中已經(jīng)發(fā)生的事件。

select仍然在現(xiàn)實(shí)保留的原因
1.歷史遺留問題,因?yàn)閟elect發(fā)展了很久的時(shí)間,額可以肯定大多的平臺(tái)都支持他了,因?yàn)槟銦o法保證新的平臺(tái)都支持poll或者epoll。放心,我們說的不是enaic那種元祖機(jī)子,你聽說過xp嗎?你知道他在全中國(guó)全世界知道今天2016/9/10仍然占據(jù)多少比例么。oh no,它只支持iselect。
2.時(shí)間高精度,因?yàn)閟elect可以精確到ns級(jí)別。而后二者只能精確到ms級(jí)別。當(dāng)然你會(huì)說很多系統(tǒng)調(diào)用都沒有那么高精度的。但是對(duì)于實(shí)時(shí)操作系統(tǒng),也就是類似工業(yè)控制的高精領(lǐng)域,或者說比如核電站,核反應(yīng)堆,oh,no這兒用select不止是讓系統(tǒng)更安全,讓你不被老板炒魷魚,更是關(guān)系到我們大眾安全的問題,請(qǐng)你一定不要忘了這一點(diǎn)。
3,當(dāng)然如果是簡(jiǎn)單應(yīng)用場(chǎng)景,比如低于200個(gè)socket,那么你用什么其實(shí)問題都不大,更多的問題是在與程序員的編程水平了。

轉(zhuǎn)自:https://segmentfault.com/a/1190000003063859

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容