poll與epoll

poll


poll函數接口

#include <poll.h>

int poll(struct pollfd *fds,nfds_t nfds,int timeout);
//參數1 結構體指針(結構體數組的首地址)  
//參數2 數組長度
//參數3 poll函數的超時時間(同select)
//返回值小於0,出錯,等於0,poll函數等待超時
//大於0,表示poll由於監聽的文件描述符就緒返回。

struct pollfd{
    int fd;//文件描述符
    short events;//輸入參數 監聽的事件集合(常用事件 POLLIN POLLOUT)
    short revents;//輸出參數 返回的事件集合
}; 


poll的優點(同select比較)

不同與select使⽤三個位圖來表⽰三個fdset的⽅式,poll使⽤⼀個pollfd的指針實現

  • pollfd結構包含了要監視的event和發⽣的event,不再使⽤select“參數-值”傳遞的⽅式. 接⼝使⽤⽐select更⽅便.
  • poll並沒有最⼤數量限制 (但是數量過⼤後性能也是會下降)

poll的缺點

  • 和select函數⼀樣,poll返回後,需要輪詢pollfd來獲取就緒的描述符.
  • 每次調⽤poll都需要把⼤量的pollfd結構從⽤戶態拷⻉到內核中.
  • 同時連接的⼤量客戶端在⼀時刻可能只有很少的處於就緒狀態, 因此隨着監視的描述符數量的增⻓,其效率也會線性下降

//使用epoll監控標準輸入

#include <stdio.h>
#include <unistd.h>
#include <poll.h>

int main(){

    struct pollfd fds;
    fds.fd = 0;//0標準輸入
    fds.events = POLLIN;

    while(1){
        int ret = poll(&fds,1,-1);//-1 永久阻塞
        if(ret<0){
            perror("poll");
            return 1;
        }
        char buf[1024] = {0};
        ssize_t read_size = read(0,buf,sizeof(buf)-1);
        if(read_size < 0){
            perror("read");
            return 1;
        }
        if(read_size == 0){
            printf("read done\n");
            return 0;
        }
        buf[read_size] = '\0';

        printf("rep = %s\n",buf);
    }

    return 0;
}

epoll


epoll的相關函數


創建一個epoll的句柄(驗證其爲文件描述符的方式:連接服務器時,第一個client爲5,不是4),既然是文件描述符,用完要關閉。

int epoll_create(int size);
//size爲任意值,無意義

epoll的事件註冊函數

int epoll_ctl (int epfd,int op,int fd,struct epoll_event *event);
//參數1 epoll_create() 的返回值(句柄)
//參數2 表示處理事件的方式(3種)
//參數3 需要監聽的文件描述符
//參數4結構體指針(操作方式)

參數2的三種方法

  • EPOLL_CTL_ADD 註冊新的fd到epfd;
  • EPOLL_CTL_MOD 修改已經註冊的fd的監聽事件;
  • EPOLL_CTL_DEL 從epfd中刪除一個fd;

    struct epoll_event
    這裏寫圖片描述

epoll_event結構體裏邊有兩個參數,一個events(相當於位圖)其常用操作有EPOLLIN,EPOLLOUT。第二個參數爲一個聯合,其能更好的操作一個未知甚至更復雜的結構(含有一個void*的指針)。

events常見操作:

  • EPOLLIN : 表⽰對應的⽂件描述符可以讀 (包括對端SOCKET正常關閉);
  • EPOLLOUT : 表⽰對應的⽂件描述符可以寫;
  • EPOLLPRI : 表⽰對應的⽂件描述符有緊急的數據可讀 (這⾥應該表⽰有帶外數據到來);
  • EPOLLERR : 表⽰對應的⽂件描述符發⽣錯誤;
  • EPOLLHUP : 表⽰對應的⽂件描述符被掛斷;
  • EPOLLET : 將EPOLL設爲邊緣觸發(Edge Triggered)模式, 這是相對於⽔平觸發(Level Triggered)來說的.(默認爲LT模式1)
  • EPOLLONESHOT:只監聽⼀次事件, 當監聽完這次事件之後, 如果還需要繼續監聽這個socket的話, 需要再次把這個socket加⼊到EPOLL隊列⾥


int epoll_wait(int epfd,struct epoll_event *event,int maxevents,int timeout);

//參數1 epoll句柄
//參數2 結構體數組首地址(同epoll_ctl的 *event)
//參數3 數組的大小
//參數4 超時時間

參數events是分配好的epoll_event結構體數組.
epoll將會把發⽣的事件賦值到events數組中 (events不可以是空指針,內核只負責把數據複製到這個events數組中,不會去幫助我們在⽤戶態中分配內存).
maxevents告之內核這個events有多⼤,這個 maxevents的值不能⼤於創建epoll_create()時的size.
參數timeout是超時時間 (毫秒,0會⽴即返回,-1是永久阻塞).
如果函數調⽤成功,返回對應I/O上已準備好的⽂件描述符數目,如返回0表⽰已超時, 返回⼩於0表⽰函數失敗


epoll的使用場景

對於多連接, 且多連接中只有⼀部分連接⽐較活躍時, ⽐較適合使⽤epoll


epoll的工作原理(底層紅黑樹存儲,就緒存放於鏈表)

  • 當某⼀進程調⽤epoll_create⽅法時,Linux內核會創建⼀個eventpoll結構體,這個結構體中有兩個成員與epoll的使⽤⽅式密切相關
  • 每⼀個epoll對象都有⼀個獨⽴的eventpoll結構體,⽤於存放通過epoll_ctl⽅法向epoll對象中添加進來的事件.
  • 這些事件都會掛載在紅⿊樹中,如此,重複添加的事件就可以通過紅⿊樹⽽⾼效的識別出來(紅⿊樹的插⼊時間效率是lgn,其中n爲樹的⾼度).
  • ⽽所有添加到epoll中的事件都會與設備(網卡)驅動程序建⽴回調關係,也就是說,當響應的事件發⽣時會調⽤這個回調⽅法.
  • 這個回調⽅法在內核中叫eppollcallback,它會將發⽣的事件添加到rdlist雙鏈表中.
    在epoll中,對於每⼀個事件,都會建⽴⼀個epitem結構體

  • 當調⽤epoll_wait檢查是否有事件發⽣時,只需要檢查eventpoll對象中的rdlist雙鏈表中是否有epitem元素即可.

  • 如果rdlist不爲空,則把發⽣的事件複製到⽤戶態,同時將事件數量返回給⽤戶. 這個操作的時間複雜度是O(1)

epoll的工作方式

  • 假如有一個文件描述符就緒,攜帶1K的數據,epoll_wait返回,假設開始讀數據,一次只能讀0.5K的數據,那麼緩衝區還留下0.5K,那麼此時epoll_wait立刻再次返回,告訴服務器該文件描述符就緒,讓其讀完剩餘的0.5K數據,這樣的操作爲水平觸發。
  • 邊緣觸發則是如上例如果1K的字節只讀了0.5K,epoll_wait不會立刻返回,而是等到下次這個文件就緒後,返回,此時,服務器纔有可能讀取上次的緩存。故ET模式強制我們將緩衝區的數據一次性全部讀取。所以只能是非阻塞讀寫。

水平觸發(LT)

  • 支持阻塞和非阻塞讀寫

邊緣觸發(ET)(效率高)

  • 只支持非阻塞讀寫(ET(邊緣觸發)數據就緒只會通知⼀次,也就是說,如果要使⽤ET模式,當數據就緒時,需要⼀直read,直到出錯或完成爲⽌.但倘若當前fd爲阻塞(默認),那麼在當讀完緩衝區的數據時,如果對端並沒有關閉寫端,那麼該read函數會⼀直阻塞。)(想象僵持狀態)

epoll的優點(同select比較)

  • ⽂件描述符數目⽆上限: 通過epoll_ctl()來註冊⼀個⽂件描述符, 內核中使⽤紅⿊樹的數據結構來管理所有需要監控的⽂件描述符.
  • 基於事件的就緒通知⽅式: ⼀旦被監聽的某個⽂件描述符就緒, 內核會採⽤類似於callback的回調機制, 迅速激活這個⽂件描述符. 這樣隨着⽂件描述符數量的增加, 也不會影響判定就緒的性能;
  • 維護就緒隊列: 當⽂件描述符就緒, 就會被放到內核中的⼀個就緒隊列中. 這樣調⽤epoll_wait獲取就緒⽂件描述符的時候, 只要取隊列中的元素即可, 操作的時間複雜度是O(1);
  • 從接口使用的角度上講:不必每次都重新設置要監控的文件描述符,使接口使用方便,也能夠避免用戶態和內核態之間,來回的拷貝文件描述符結構。

LT

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>

typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
typedef struct epoll_event epoll_event;


void ProcessListenScok(int epoll_fd, int listen_sock){
    sockaddr_in peer;
    socklen_t len;
    int new_sock = accept(listen_sock,(sockaddr*)&peer,&len);
    if(new_sock <0 ){
        perror("accept");
        return;
    }
    //把 new_sock 加入到epoll之中

    epoll_event event;
    event.events = EPOLLIN;
    event.data.fd = new_sock;
    int ret = epoll_ctl(epoll_fd,EPOLL_CTL_ADD,new_sock,&event);
    if(ret < 0){
        perror("epoll_ctl ADD");
        return;
    }
    printf("[client %d] connected\n",new_sock);
    return;
}

int ServerInit(const char* ip,short port){
    int listen_sock = socket(AF_INET,SOCK_STREAM,0);
    if(listen_sock < 0){
        perror("socket");
        return -1;
    }
    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr(ip);
    addr.sin_port = htons(port);
    int ret = bind(listen_sock,(sockaddr*)&addr,sizeof(addr));
    if(ret < 0 ){
        perror("bind");
        return -1;
    }
    ret = listen(listen_sock,10);
    if(ret < 0){
        perror("listen");
        return -1;
    }
    return listen_sock;
}


void ProcessNewSock(int epoll_fd,int new_sock){
    char buf[1024] = {0};
    ssize_t read_size = read(new_sock,buf,sizeof(buf)-1);
    if(read_size < 0){
        perror("read");
        return;
    }
    //讀到返回值爲0,對端關閉了文件描述符。
    //本端也應該關閉文件描述符,並且把文件描述符從epoll之中刪除掉
    if(read_size == 0){
        close(new_sock);
        epoll_ctl(epoll_fd,EPOLL_CTL_DEL,new_sock,NULL);
        printf("[client %d] disconnected\n",new_sock);
        return;
    }
    buf[read_size] = '\0';
    printf("[client %d]say:%s\n",new_sock,buf);
    write(new_sock,buf,strlen(buf));
    return;
}



int main(int argc,char* argv[]){
    if(argc != 3){
        printf("Usage: ./server_epoll  [ip] [port]\n");
        return 1;
    }
    int listen_sock = ServerInit(argv[1],atoi(argv[2]));
    if( listen_sock < 0){
        perror("ServerInit");
        return 1;
    }

    //創建並初始化epoll;
    int epoll_fd = epoll_create(9);
    if(epoll_fd < 0){
        perror("epoll_create");
        return 1;
    }
    //把listen_sock放置到epoll
    epoll_event event;
    event.events = EPOLLIN;
    event.data.fd = listen_sock;
    int ret = epoll_ctl(epoll_fd,EPOLL_CTL_ADD,listen_sock,&event);
    if(ret < 0){
        perror("epoll_ctl");
        return -1;
    }
    printf("ServerInit OK!\n");

    while(1){
        epoll_event output_event[100];
        int nfds = epoll_wait(epoll_fd,output_event,100,-1);
        if(nfds < 0){
            perror("epoll_wait");
            continue;
        }
        //output_event[]裏邊保存着準備好的文件描述符
        int i = 0;
        for(;i < nfds;++i ){
            if(listen_sock == output_event[i].data.fd){//讀
                ProcessListenScok(epoll_fd,listen_sock);
            }
            else{
                ProcessNewSock(epoll_fd,output_event[i].data.fd);//增加
            }
        }//for
    }//while


    return 0;
}

ET

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章