多路轉接之poll與epoll

poll

相比於select函數,poll的跨平臺移植性不如select,因爲poll函數只能在linux環境中使用,也是採用輪詢遍歷的方式。

與select函數的不同之處:

  1. poll不會限制文件描述符的個數
  2. 文件描述符對應一個事件結構,這個結構中有兩個事件,一個是要監控的文件描述符,另一個是這個文件描述符所對應的事件

函數接口

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • fds:事件結構數組
  • nfds:fds數組中有效的元素個數
  • timeout:超時時間

在這裏插入圖片描述
struct pollfd 結構體

struct pollfd      
  {      
    int fd;         /* File descriptor to poll.  */      
    short int events;       /* Types of events poller cares about.  */      
    short int revents;      /* Types of events that actually occurred.  */      
  };

其中:

  • fd:該結構體所關心的文件描述符數值
  • events:所關心這個文件描述符的那些事件,事件與事件之間採用按位或的方式連接
  • revents:當關心的文件描述符產生對應的關心的事件時,返回給調用者發生的事件(每次監控的時候,就會被初始化爲空)

在這裏插入圖片描述
events 和 revents的取值 :

事件 描述 是否可以作爲輸入 是否可以作爲輸出
POLLIN 普通或優先級帶數據可讀
POLLRDNORM 普通數據可讀
POLLRDBAND 優先級帶數據可讀(linux不支持)
POLLPRI 高優先級數據可讀,比如TCP帶外數據
POLLOUT 數據可寫(普通數據和優先數據)
POLLWRNORM 普通數據可寫
POLLWRBAND 優先級帶數據可寫
POLLERR 發生錯誤
POLLHUP 發生掛起,管道的寫端關閉後,讀端描述符將受到POLLHUP事件
POLLNVAL 描述字不是一個打開的文件

編程實例

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <poll.h>

using namespace std;

int main()
{
	//創建一個struct pollfd結構體,關心0號文件描述符,標準輸入
	//監視可讀事件
    struct pollfd poll_fd;
    poll_fd.fd = 0;
    poll_fd.events = POLLIN;
	
	//輪詢遍歷
    while(1)
    {
        int ret = poll(&poll_fd,1,1000);
        if(ret < 0)//poll出錯
        {
            perror("poll");
            sleep(3);
            continue;
        }
        else if(ret == 0)//監控超時
        {
            cout<<"poll timeout"<<endl;
            continue;
        }
        else if(poll_fd.revents == POLLIN)//當發送的事件時可讀事件時
        {
			//讀取數據
            char buf[1024] = {'\0'};
            read(0,buf,sizeof(buf) - 1);
            cout<<"input : "<<buf<<endl;
        }
    }

    return 0;
}

在這裏插入圖片描述
如果我們一次所傳入的數據超過了一次可以接受的數據大小時,就會別分批進行接收

poll 的優缺點:
優點:

  1. poll採用了事件結構的方式,簡化了程序,並且他不限制文件描述符的個數
  2. 不需要我們重新添加文件描述符到事件的結構數組當中

缺點:

  1. poll也是需要輪詢遍歷事件結構數組,那麼當文件描述符增多的時候,性能可能不會那麼好了
  2. poll不支持跨平臺操作
  3. poll不會告訴我們哪一個文件描述符就緒了,想要找到這個就緒的文件描述符,就需要我們進行遍歷操作
  4. poll在操作的過程中,也是需要時間將結構數組從用戶空間拷貝到內核空間中,再將內核空間拷貝到用戶空間

epoll

man手冊中,對於epoll的描述是:

The  epoll API performs a similar task to poll(2): monitoring multiple file descriptors to see if
       I/O is possible on any of them. 

也就是:

epoll API執行與poll(2)類似的任務:監視多個文件描述符,看看是否存在
I/O是可能的任何一個

epoll是爲了處理大量的文件描述符而改進的poll函數

函數接口

創建epoll操作句柄:

#include <sys/epoll.h>

int epoll_create(int size);
  • size:定義的是epoll中最大可以監控的文件描述符個數,但是在linux2.6.8之後,size參數是被忽略的,變成了可以擴容的方式。(size 不可以傳入一個負數)

在使用完之後,必須使用close函數進行關閉

epoll 的操作:

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, 
		struct epoll_event *event);
  • epfd:之前使用epoll_create函數返回的epoll操作句柄
  • op:想讓epoll_stl函數所做的哪些事

在這裏插入圖片描述

  • fd:告訴epoll函數,我們所關心的文件描述符
  • event:他是一個struct epoll_event類型的結構體,表示的是epoll事件的結構
typedef union epoll_data    
{    
  void *ptr;    
  int fd;    
  uint32_t u32;    
  uint64_t u64;    
} epoll_data_t;    
    
struct epoll_event    
{    
  uint32_t events;  /* Epoll events */    
  epoll_data_t data;    /* User data variable */    
} __EPOLL_PACKED;  

struct epoll_event :

  1. 第一個參數:uint32_t events,是我們想讓文件描述符關心的事件集合
    其中,EPOLLIN,是可讀事件;EPOLLOUT,是可寫事件

  2. 第二個參數 epoll_data_t data,他是一個epoll_data類型的聯合結構體

typedef union epoll_data :

  1. void* ptr:這裏可以傳遞的信息,就是當epoll監控的文件描述符就緒的時候,等到函數返回,我們可以通過ptr拿到這些信息;其在使用的時候,傳入一個結構體,(自定義結構體)struct my_epoll_data{int fd},必須在結構體中包含文件描述符
  2. int fd:我們所關心的文件描述符,可以當做文件描述符中的事件就緒之後,返回給我們時查看;其取值爲文件描述符的數值
  3. ptr和fd,兩者在使用的時候,只能選其中的一個。

在這裏插入圖片描述

監控:

 #include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event *events,
         int maxevents, int timeout);
  1. epfd:epoll操作句柄
  2. events:epoll事件結構數組。作爲出參,他返回的是就緒的事件結構(每一個事件結構都對應一個文件描述符)
  3. maxevents:最大可以拷貝的事件結構數量
  4. timeout:超時時間

在這裏插入圖片描述
epoll 的工作原理:

在這裏插入圖片描述

  1. 當某一進程調用epoll_create方法的時候,linux內核會創建一個eventpoll結構體,這個結構體中有兩個成員與epoll使用方式相關,分別是紅黑樹和雙向鏈表
  2. 那些沒有就緒的事件,用於存放通過epoll_ctl方法向epoll對象添加進來的事件,都會被掛載在紅黑樹中(這樣就可以識別出重複的事件,並且還讓查找效率提高)
  3. 當紅黑樹中某個事件就緒後,就會被拷貝到雙向鏈表中(這是一個內核行爲)。所以說,雙向鏈表中的數據都是就緒的
  4. 當調用epoll_wait函數的時候,雙向鏈表向參數中進行拷貝的方式,是通過改變頁表,直接指向物理內存中的雙向鏈表中就緒事件所佔用的物理內存

在這裏插入圖片描述

epoll的兩種工作方式

水平觸發 LT模式

在這種模式下,當epoll中檢測到了等待觸發的事件就緒後,可以不立即進行處理,而是隻處理一部分。等到第二次調用epoll_wait函數的時候,可以接着操作剛纔沒有處理完的數據。他支持阻塞讀寫與非阻塞讀寫

這是epoll函數的默認工作方式,另外select 和 poll都是水平觸發

  • 對於可讀事件,只要接收緩衝區中的存在數據,就會一直觸發該接收該可讀事件,直到沒有數據可讀

  • 對於可寫事件,只要發送緩衝區中存在數據,就會一直觸發該可寫事件就緒,直到發送緩衝區中沒有數據

函數模擬

/*================================================================
*   Copyright (C) 2020 Sangfor Ltd. All rights reserved.
*   
*   文件名稱:test.cpp
*   創 建 者:dcl
*   創建日期:2020年06月20日
*   描    述:
*
================================================================*/

#include <unistd.h>
#include <cstdio>
#include <sys/epoll.h>
#include <iostream>
using namespace std;

int main()
{
    int epollfd = epoll_create(10);
    if(epollfd < 0)
    {
        perror("epoll_create");
        return 0;
    }

    struct epoll_event ev;
    ev.events = EPOLLIN;//可讀事件
    ev.data.fd = 0;
    //int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    //添加一個文件描述符到紅黑樹中
    int ret = epoll_ctl(epollfd,EPOLL_CTL_ADD,0,&ev);
    if(ret < 0)
    {
        perror("epoll_ctl");
        return 0;
    }

    while(1)
    {
        struct epoll_event fd_arr[10];
        //int epoll_wait(int epfd, struct epoll_event *events,
        //                      int maxevents, int timeout);
        int ret = epoll_wait(epollfd,fd_arr,sizeof(fd_arr) / sizeof(fd_arr[0]),3000);
        if(ret < 0)
        {
            perror("epoll_wait");
            continue;
        }
        else if(ret == 0)
        {
            cout<<"timeout"<<endl;
            sleep(1);
            continue;
        }
        cout<<"---"<<ret<<endl;
        //文件描述符被觸發
        for(int i = 0; i < ret; i++)
        {
            if(fd_arr[i].data.fd == 0)
            {
                char buf[3] = {'\0'};
                read(fd_arr[i].data.fd,buf,sizeof(buf) - 1);
                cout<<"input : "<<buf<<endl;
            }
        }
    }
    

    return 0;
}

在設置接收數據的大小的時候,我們給數組設置的比較小,當我們發送大量的數據,就會出現下面這種情況
在這裏插入圖片描述

邊緣觸發 ET模式

在ET模式下,如果我們一次只是處理了部分數據,那麼剩下的數據在下一次觸發的時候纔會被處理的。

也就是說,ET模式下,當文件描述符就緒後,就只有一次處理文件中數據的機會,所以我們需要使用循環的方式進行接收數據。他支持非阻塞讀寫

程序模擬:

/*================================================================
*   Copyright (C) 2020 Sangfor Ltd. All rights reserved.
*   
*   文件名稱:test.cpp
*   創 建 者:dcl
*   創建日期:2020年06月20日
*   描    述:
*
================================================================*/

#include <errno.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <cstdio>
#include <iostream>
#include <string>

using namespace std;

inline void slove(struct epoll_event* fd_arr,int ret)
{
    for(int i = 0; i < ret; i++)
        if(fd_arr[i].data.fd == 0)
        {
            string ans = "";
            while(1)
            {
                char buf[3] = {'\0'};
                ssize_t readsize = read(0,buf,sizeof(buf) - 1);
                if(readsize < 0)
                {
                    //說明數據讀完了
                    if(errno == EAGAIN || errno == EWOULDBLOCK)
                        break;
                    
                    perror("read");
                    break;
                }

                ans += buf;
                break;
                if(readsize < (ssize_t)sizeof(buf) - 1)
                   break; //數據讀完了
            }

            if(ans != "")   cout<<"read data: "<<ans<<endl;
        }

}

int main()
{
    //設置文件描述符屬性爲非阻塞
    //int fcntl(int fd, int cmd, ... /* arg */ );
    int flag = fcntl(0,F_GETFL);
    fcntl(0,F_SETFL,flag | O_NONBLOCK);

    //創建epoll操作句柄
    int epoll_fd = epoll_create(10);
    if(epoll_fd < 0)
    {
        perror("epoll_create");
        return 0;
    }

    //添加文件描述符
    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLET;
    ev.data.fd = 0;
    int ret = epoll_ctl(epoll_fd,EPOLL_CTL_ADD,0,&ev);
    if(ret < 0)
    {
        perror("epoll_ctl");
        return 0;
    }

    while(1)
    {
        struct epoll_event fd_arr[10];
        ret = epoll_wait(epoll_fd,fd_arr,sizeof(fd_arr) / sizeof(fd_arr[0]),-1);
        if(ret < 0)
        {
            perror("epoll_wait");
            continue;
        }

        slove(fd_arr,ret);

    }


    return 0;
}


沒有循環接收數據時的情況
在這裏插入圖片描述
數據的接收過程:
在這裏插入圖片描述
在循環接收的過程中,當某一次我們接收的數據的大小小於我們的最大接收能力的時候,就說明數據讀取完了

還有一種意外情況,就是當我們所發送的數據正好是我們最大接收能力的整數倍時,比如說:我們發送一個 123\r\n,我們一次只能接收兩個數據,那麼當最後一次接收到數據的時候,其實數據讀取完畢了,但是read函數還是處於一個等待數據的階段,就會卡死,造成一個飢餓的情況

  • ET模式,在加上循環後很容易造成飢餓狀態

解決方法:將文件描述符設置爲非阻塞狀態,我們可以調用fcntl函數

int fcntl(int fd, int cmd, ... /* arg */ );
  • fd:所要操作的哪一個文件描述符
  • cmd:fcntl函數需要做的事情
    F_GETFL,獲取文件描述符屬性,文件描述符通過返回值返回給我們,arg不需要設置
    F_SETFD,設置文件描述符屬性,arg就是文件描述符需要設置的屬性,類型爲int

在這裏插入圖片描述

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