主頁 > 軟體設計 > 五種高階IO模型以及多路轉接技術(select、poll和epoll)及其代碼驗證

五種高階IO模型以及多路轉接技術(select、poll和epoll)及其代碼驗證

2021-08-12 08:03:25 軟體設計

目錄

  • 1. 五種高階IO模型
    • 1.1 阻塞IO
    • 1.2 非阻塞IO
    • 1.3 信號驅動IO
    • 1.4 多路轉接IO
    • 1.5 異步IO
  • 2. 多路轉接技術(select、poll、epoll)
    • 2.1 select函式
      • 2.1.1 select 函式詳解
      • 2.1.2 select函式優缺點
    • 2.2 poll函式
      • 2.2.1 poll函式詳解
      • 2.2.2 poll函式優缺點
    • 2.3 epoll函式
      • 2.3.1 epoll相關函式詳解
      • 2.3.2 epoll作業原理
      • 2.3.3 epoll的優點
      • 2.3.4 LT模式和ET模式
  • 3. select、poll、epoll的代碼驗證
    • 3.1 select代碼驗證
      • 3.1.1 select 簡單驗證
      • 3.1.2 使用select函式解決TCP單行程阻塞問題
      • 3.1.3 在多執行緒下呼叫select函式
    • 3.2 poll代碼驗證
    • 3.3 epoll代碼驗證
      • 3.3.1 epoll的簡單驗證
      • 3.3.2 使用epoll函式解決TCP單行程的阻塞問題
      • 3.3.3 epoll函式的ET模式的驗證和解決


1. 五種高階IO模型

首先我們要明確的是在任何IO操作中,均包含兩個步驟,等待和拷貝,而在實際的業務中,等待所消耗的時間往往大于拷貝的時間,因此,讓IO操作更高效,核心的方法就是將等待的時間縮短,

低階IO:是指類似于將用戶輸入的內容讀取到某個變數中,將變數中的值列印在螢屏上等等,簡單來說就是對C庫自己所維護的緩沖區進行I/O操作,

高階IO:通常應用于網路Socket編程,對UDP(TCP)所維護的發送緩沖區和接識訓沖區進行I/O操作,并且高階IO分為同步IO異步IO,同步IO又分為阻塞IO、非阻塞IO、信號驅動IO和多路轉接IO

1.1 阻塞IO

阻塞IO:在內核將資料準備好之前(等待+拷貝),系統呼叫會一直進行等待,并且所有的套接字默認均是阻塞方式,

在這里插入圖片描述

1.2 非阻塞IO

非阻塞IO:若當前內核沒有將資料準備好,則系統呼叫會直接回傳,并回傳一個EWOULDBLOCK錯誤碼,
非阻塞IO一般都是搭配回圈來使用的(也叫輪詢),這對系統資源是較大的浪費,一般都是在特定的場景下使用,

在這里插入圖片描述

擴展:將一個檔案描述符設定為非阻塞屬性,

int fcntl(int fd, int cmd, ... /* arg */ ),處于#include <fcntl.h>

  • 首先通過給cmd傳入F_GETFL宏來獲取到當前檔案描述符的屬性
  • 其次再通過獲得的屬性按位或上O_NONBLOCK屬性,將檔案描述符設定為非阻塞屬性

代碼如下:

int f1 = fcntl(fd,F_GETFL);
if(f1 < 0)
{
	perror("fcntl");
	return ;
}
fcntl(fd,F_SETFL, f1 | O_NONBLOCK);

1.3 信號驅動IO

信號驅動IO:當內核將資料準備好之后或者說告訴應用行程何時才可以開始拷貝資料,會給應用行程發送一個SIGIO的信號,通知其進行IO操作,當應用程式接收到該信號之后,證明資料已經準備好了,接下來就會呼叫系統呼叫函式對其進行相應的IO操作

在這里插入圖片描述

1.4 多路轉接IO

多路轉接IO:在流程上與阻塞IO類似,但是其本質上最核心的地方就是可以一次性等待多個檔案描述符的就緒狀態

在這里插入圖片描述

1.5 異步IO

異步IO:當內核將資料拷貝好之后,通知應用行程進行相關的IO操作,需要注意的是他和信號驅動IO很像,但是信號驅動IO是內核將資料準備好后才通知,而異步IO是內核將資料拷貝好后才通知

在這里插入圖片描述

注:為了性能和效率的優先,C++默認采用的是異步IO的方式,

2. 多路轉接技術(select、poll、epoll)

首先我們要知道多路復用函式的作用是什么,其本質上就是讓內核幫助程式員監控多個檔案描述符的IO事件,一旦監控的某個檔案描述符對應的事件產生(IO就緒),就會通知呼叫者,也就是說可以并行的處理多條客戶端的請求,換句話說就是實作了高并發,

2.1 select函式

作用:監控多個檔案描述符,就緒之后,通知呼叫者,

2.1.1 select 函式詳解

在這里插入圖片描述

  • nfds:select監控事件集合(fd_set)的范圍,范圍是從[0,1023]之間去進行選擇范圍,nfds的取值為:監控的最大檔案描述符數值+1
  • fd_set:事件集合型別

readfds:可讀事件結合
writefds:可寫事件集合
exceptfds:例外事件集合
在這里插入圖片描述
在這里插入圖片描述
在這里插入圖片描述
并且內核在使用該陣列的時候采用的是位圖的方式,因此總共有1024個位元位
在這里插入圖片描述

  • timeout:
    在這里插入圖片描述

NULL:阻塞,
0 :非阻塞,
傳遞一個struct timeval:代表著帶有超時時間的方式,

回傳值:

  • 監控成功:回傳就緒的檔案描述符個數

監控成功會回傳就緒的檔案描述符個數,并且在回傳的時候,會將事件集合中未就緒的檔案描述符去除掉

  • 監控失敗:回傳-1

引數含義補充:

  • 如果關心某個檔案描述符對應的某種事件,則將檔案描述符添加到對應的事件集合當中去,例如:關心3號檔案描述符的可讀事件,則將3號檔案描述符添加到對應的readfds事件中,
  • 添加檔案描述符到事件集合的時候,就是將檔案描述符對應的位元位設定為1,
  • 如果一個檔案描述符關心多種事件(可讀、可寫、例外),則將檔案描述符添加到不同的事件集合當中去,
  • select的監控效率會隨著檔案描述符的增多而下降,本質原因就是由于監控輪詢的范圍變大了,
  • 對相應事件集合的操作:
    在這里插入圖片描述

2.1.2 select函式優缺點

優點:

  • According to POSIX.1-2001,select遵循的是POISX標準,說明select函式是一個跨平臺的函式,既可以在linux中運行,也可以在win中運行,
  • select在帶有超時時間監控的時候,超時時間單位可以是微秒,

缺點:

  • 監控檔案描述符的個數最多為1024個
  • 隨著監控檔案描述符的增多,監控的效率在逐漸的下降(本質上是select在輪詢的進行監控)
  • 可讀、可寫、例外這些事件需要添加到單獨的添加到不同的事件集合中
  • 當select監控成功之后,會從事件集合當中去除未就緒的檔案描述符,這表明程式在下一次運行的時候,需要重新添加檔案描述符,
  • 在每次select進行監控的時候,都會將準備好的事件集合拷貝到內核空間,select回傳的時候,都會相應的事件集合從內核空間再拷貝到用戶空間,

2.2 poll函式

前提:均是監控多個檔案描述符,就緒之后,然后通知呼叫者,與select相比,不支持跨平臺,與epoll相比,效率沒有epoll高,

2.2.1 poll函式詳解

在這里插入圖片描述

  • struct pollfd*: 事件集合結構

在這里插入圖片描述
想讓poll監控幾個元素,只需要在定義事件結構資料的時候,多傳遞幾個元素,

  • nfds:事件結構陣列中的有效元素的個數
  • timeout

>0:帶有超時事件,單位秒
==:非阻塞
<0:阻塞

回傳值:回傳就緒的檔案描述符個數

2.2.2 poll函式優缺點

優點:

  • 提出了事件結構的方式,在給poll函式傳遞引數的時候,不需要分別添加到事件集合當中,
  • 事件結構陣列的大小可以根據程式員自己進行定義,并沒有上限的要求,
  • 不用再監控到就緒之后,重新添加檔案描述符,

缺點:

  • 不支持跨平臺,
  • 內核對事件結構陣列進行監控的時候也采用的是輪詢遍布的方式,

2.3 epoll函式

epoll函式是目前世界上公認在Linux下,多路轉接監控效率最高的模型

2.3.1 epoll相關函式詳解

① 創建epoll操作句柄

在這里插入圖片描述

  • size:自從Liunx2.6.8之后,size引數是被忽略的,但是不要傳遞一個小于0的數字,

回傳值:回傳epoll的操作句柄,

② 注冊待要監控的檔案描述符

在這里插入圖片描述

  • epfd:epoll操作句柄
  • op

在這里插入圖片描述

  • fd:待處理(添加、修改、洗掉)的檔案描述符
  • event:檔案描述符對應的事件結構

在這里插入圖片描述

回傳值:
在這里插入圖片描述

③ epoll的等待介面

在這里插入圖片描述

  • epfd:epoll的操作句柄
  • events:型別同 epoll_ctl 中一樣,只不過這里的是事件集合陣列,從epoll當中獲取就緒的事件結構
  • maxevents:最多一次獲取多少個事件結構
  • timeout

>0:帶有超時事件,單位秒
==:非阻塞
<0:阻塞

回傳值:就緒的檔案描述符個數

2.3.2 epoll作業原理

當某一個行程呼叫epoll_create函式時,LInux內核會創建一個eventpoll的結構體,這個結構體中有兩個成員與epoll的使用方式密切相關,

在這里插入圖片描述

簡單概述一下:

每當呼叫epoll_create函式的時候,內核就會創建一個eventpoll的結構體,在該結構體中有兩個成員,型別分別為紅黑樹和雙向鏈表,當呼叫epoll_ctl函式進行添加、修改或洗掉時,本質上就是對該紅黑樹進行添加、修改和洗掉,而每一個添加進來的紅黑樹結點均會和設備(網卡)驅動程式建立一個回呼關系(這個回呼函式為ep_poll_callback),當某個檔案描述符就緒之后,他會呼叫這個回呼函式將該就緒的事件結構添加到雙向鏈表當中,而當呼叫epoll_wait進行監控的時候,如果雙向鏈表為空,則表明當前沒有就緒的事件發生,如果不為空,則將雙向鏈表中的內容復制到用戶態,并回傳將事件數量回傳給用戶

注意:這里的雙向鏈表其實實作的是一個佇列,雖然是一個雙向鏈表,但是他只支持先進先出(FIFO),是佇列的特性,

2.3.3 epoll的優點

  • 沒有數量的限制,檔案描述符的數量為內核所支持的最大上限(65536)
  • 事件回呼機制,當檔案描述符就緒之后,會呼叫回呼函式將事件結構復制到雙向鏈表中,呼叫epoll_wait回傳時,直接訪問就緒佇列就可以知道有多少檔案描述符就緒,這個操作的時間復雜度為O(1),即使檔案運算子很多,也不會受到影響,
  • 資料拷貝是輕量的,只有在合適的時候呼叫EPOLL_CTL_ADD將對應的檔案描述符介面拷貝到內核中,這個操作并不頻繁(select/poll每次都要回圈的進行拷貝),

select、poll、epoll對比

在這里插入圖片描述

2.3.4 LT模式和ET模式

舉個例子:當你在中午飯點玩游戲的時候,如果這個時候飯剛好做好了,
LT:家里人第一次通知的時候,你沒有管,那他們還會通知第二次、第三次…
ET:家里人在第一次通知的時候,你沒有管,那么他們就不會在通知你了,

① LT(Level Triggered) 水平觸發作業模式

在LT模式下,當epoll檢測到事件就緒的時候,可以不處理或處理一部分,但是可以連續多次呼叫epoll_wait對事件進行處理,簡單點來說的話就是如果事件來了,不管來了幾個,只要仍然有未處理的事件,epoll都會通知你

② ET(Edge Triggered) 邊緣觸發作業模式

在ET模式下,當epoll檢測到事件就緒的時候,會立即進行處理,并且只會處理一次,換句話說就是檔案描述符上的事件就緒之后,只有一次處理機會, 簡單來說就是如果事件來了,不管來了幾個,你若不處理或者沒有處理完,除非下一個事件到來,否則epoll將不會再通知你,

LT模式存在的問題:

如果可讀或者可寫事件未進行處理,會頻繁反復的激活未處理事件

解決:

在不想處理某個事件的時候就將它從epoll中移除,需要時再添加上

ET模式存在的問題:

如果可讀或者可寫事件沒有全部處理,會有老資料殘留,需要等待新資料的到來才會被處理

解決:

  • 回圈讀取或者寫入資料,直至回傳值未EAGAIN或者EWOULDBLOCK(回圈呼叫)
  • 讀取或寫入資料后,通過epoll_ctl設定EPOLL_CTL_MOD,激活未處理事件(相當于將當前未處理事件設定未新事件)

3. select、poll、epoll的代碼驗證

3.1 select代碼驗證

3.1.1 select 簡單驗證

先舉個簡單的例子,利用select函式對系統的0號檔案描述符(讀緩沖區)進行監控,一旦監控到讀的事件,則將其讀入的內容列印到螢屏上,

#include <unistd.h>
#include <sys/select.h>
#include <iostream>

using namespace std;

#define nfds 1

int main()
{
    fd_set readfds;
    FD_ZERO(&readfds);
    FD_SET(0,&readfds);
    while(1)
    {
        fd_set tmp = readfds;
        int ret = select(nfds,&tmp,NULL,NULL,NULL);
        if(ret < 0)
        {
            cout << "select failed" << endl;
            return 0;
        }
        for(int i = 0; i < nfds; ++i){
            if(FD_ISSET(0,&tmp))
            {
                char buf[1024] = {0};
                read(0,buf,sizeof(buf)-1);
                cout << buf << endl;
            }
        }
    }
    return 0;
}

結果驗證

3.1.2 使用select函式解決TCP單行程阻塞問題

在TCP單行程的條件下,存在于將accept函式放到while回圈內部或外面所造成的問題

內部:每次接收都是新的連接,和每個客戶端只能聊一次(accept函式阻塞)
外部:只能接收一個客戶端的連接(recv函式阻塞)

更為詳細的請看:Linux:TCP Socket編程(代碼實戰),

解決代碼如下:
為了更方便使用,我們將selecct函式的相關操作封裝為一個類

#include <sys/select.h>
#include <stdio.h>
#include <iostream>
#include <vector>

using namespace std;

class SocketSelect{
    public:
        SocketSelect()
        {
            FD_ZERO(&readfds_);
            nfds_ = 0;
        }

        void SetFd(int fd)
        {
            FD_SET(fd,&readfds_);

            if(fd > nfds_)
                nfds_ = fd;
        }

        void RmFd(int fd)
        {
            FD_CLR(fd,&readfds_);

            for(int i = nfds_; i >= 0; ++i)
            {
                if(FD_ISSET(i,&readfds_))
                {
                    nfds_ = i;
                    break;
                }
            }
        }

        int SelectWait(vector<int>& iv)
        {
            fd_set tmp = readfds_;
            int ret = select(nfds_+1,&tmp,NULL,NULL,NULL);
            if(ret < 0)
            {
                printf("select failed\n");
                return -1;
            }

            for(int i = 0; i <= nfds_; ++i)
            {
                if(FD_ISSET(i,&tmp))
                    iv.push_back(i);
            }

            return ret;
        }

        ~SocketSelect()
        {
            FD_ZERO(&readfds_);
        }
    private:
        int nfds_;
        fd_set readfds_;
};

服務端代碼:

#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#include "ssocket.hpp"

using namespace std;

int main()
{
    int sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
    if(sockfd < 0)
    {
        cout << "socket failed" << endl;
        return 0;
    }
    
    sockaddr_in addr;
    addr.sin_port = htons(18989);
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr("0.0.0.0");
    int ret = bind(sockfd,(struct sockaddr *)&addr,sizeof(addr));
    if(ret < 0)
    {
        close(sockfd);
        cout << "bind failed" << endl;
    }

    ret = listen(sockfd,5);
    if(ret < 0)
    {
        close(sockfd);
        cout << "listen failed" << endl;
    }

    SocketSelect ss;
    ss.SetFd(sockfd);

    while(1)
    {
        vector<int> iv;
        ret = ss.SelectWait(iv);
        if(ret < 0)
        {
            continue;
        }

        for(size_t i = 0; i < iv.size(); ++i)
        {
            if(iv[i] == sockfd)
            {
                //new con
                int newsockfd = accept(iv[i],NULL,NULL);
                if(newsockfd < 0)
                {
                    cout << "accept failed" << endl;
                    continue;
                }

                ss.SetFd(newsockfd);
            }
            else
            {
                //recv
                char buf[1024] = {0};
                ssize_t recv_size = recv(iv[i],buf,sizeof(buf)-1,0);
                if(recv_size < 0)
                {
                    cout << "recv failed" << endl;
                    continue;
                }
                else if(recv_size == 0)
                {
                    cout << "peer shutdown" << endl;
                    ss.RmFd(iv[i]);
                    close(iv[i]);
                    continue;
                }
                else
                {
                    cout << buf << endl;
                }
            }
        }
    }
    
    close(sockfd);
    return 0;
}

客戶端代碼:

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

#include <iostream>

using namespace std;

int main()
{
    int sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
    if(sockfd < 0)
    {
        cout << "socket failed" << endl;
        return 0;
    }

    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(18989);
    addr.sin_addr.s_addr = inet_addr("118.89.67.215");
    int ret = connect(sockfd,(struct sockaddr *)&addr,sizeof(addr));
    if(ret < 0)
    {
        cout << "connect failed" << endl; 
    }

    while(1)
    {
     	//char buf[1024] = "i am client1\n";
        char buf[1024] = "i am clientxxxxx\n";
        ssize_t send_size = send(sockfd,buf,strlen(buf),0);
        if(send_size < 0)
        {
            cout << "send failed" << endl;
            continue;
        }
        sleep(1);
    }
    return 0;
}

運行結果:

在這里插入圖片描述
完美解決了單行程下兩個阻塞的問題,

3.1.3 在多執行緒下呼叫select函式

在多執行緒下呼叫select函式(是多個執行緒共同偵聽同一個可讀事件集合)

#include <unistd.h>
#include <sys/select.h>
#include <pthread.h>

#include <iostream>

using namespace std;

void* PthreadEntry(void* arg)
{
    pthread_detach(pthread_self());
    fd_set *readfds = (fd_set*) arg;
    
    while(1)
    {
        int ret = select(1,readfds,NULL,NULL,NULL);
        if(ret < 0)
        {
           cout << "select failed" << endl;
           continue;
        }

        if(FD_ISSET(0,readfds))
        {
            char buf[1024] = {0};
            read(0,buf,sizeof(buf)-1);
            cout << buf << endl;
        }
    }

    return NULL;
}

int main()
{
    pthread_t pid;
    
    fd_set readfds;
    FD_ZERO(&readfds);
    FD_SET(0,&readfds);

    for(int i = 0; i < 2; ++i)
    {
        int ret = pthread_create(&pid,NULL,PthreadEntry,(void *)&readfds);
        if(ret < 0)
        {
            cout << "pthread_create failed" << endl;
            continue;
        }
    }

    while(1)
    {
        sleep(1);
    }
    return 0;
}

在這里插入圖片描述
結果說明,沒有必要,因為不管是阻塞還是非阻塞,當前緩沖區只能由一個執行緒進行讀,在讀完之后,由于檔案描述符默認是阻塞屬性,因此就會阻塞,其實這是一種驚群效應,當一個fd事件就緒的時候,所有等待這個fd的執行緒都會被喚醒,但是最終只會有一個執行緒會讀到資料,這就是驚群效應,它會造成極大的性能和資源的浪費,

3.2 poll代碼驗證

利用poll函式對系統的0號檔案描述符(讀緩沖區)進行監控,一旦監控到讀的事件,則將其讀入的內容列印到螢屏上,

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

#include <iostream>

using namespace std;


int main()
{
    struct pollfd pollfd[1];
    pollfd[0].fd = 0;
    pollfd[0].events = POLLIN;
    
    while(1)
    {
        int res = poll(pollfd,1,-1);
        if(res < 0)
        {
            cout << "poll failed" << endl;
            return 0;
        }

        for(int i = 0; i < 1; ++i)
        {
            if(pollfd[i].revents == POLLIN)
            {
                char buf[1024] = {0};
                read(pollfd[i].fd,buf,sizeof(buf)-1);
                cout << buf << endl;
            }
        }
    }


    return 0;
}

運行結果:

在這里插入圖片描述

3.3 epoll代碼驗證

3.3.1 epoll的簡單驗證

對0號檔案描述符進行監控,當發生讀事件的時候,則從緩沖區中讀資料并列印到螢屏上,

#include <unistd.h>
#include <sys/epoll.h>

#include <iostream>

#define MAXSIZE 1

using namespace std;

int main()
{
    int handle = epoll_create(5);
    if(handle < 0)
    {
        cout << "epoll_create failed" << endl;
        return 0;
    }
    
    struct epoll_event ee;
    ee.events = EPOLLIN;
    ee.data.fd = 0;

    int res = epoll_ctl(handle,EPOLL_CTL_ADD,0,&ee);
    if(res < 0)
    {
        cout << "epoll_ctl failed" << endl;
        return 0;
    }

    while(1)
    {
        struct epoll_event arr[MAXSIZE];
        res = epoll_wait(handle,arr,MAXSIZE,-1);
        if(res < 0)
        {
            cout << "epoll_wait failed" << endl;
            continue;
        }
        
        for(int i = 0; i < MAXSIZE; ++i)
        {
            if(arr[i].events == EPOLLIN)
            {
                char buf[1024] = {0};
                read(arr[i].data.fd,buf,sizeof(buf)-1);
                cout << buf << endl;
            }
        }
    }

    return 0;
}

結果驗證:

在這里插入圖片描述

3.3.2 使用epoll函式解決TCP單行程的阻塞問題

#include <iostream>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;

#define MAXSIZE 10
#define Judge(x) if(x<0) {cout << "failed" << endl; return 0;}


int main()
{
    int lisnfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
    if(lisnfd < 0)
    {
        cout << "socket failed" << endl;
        return 0;
    }

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(18989);
    addr.sin_addr.s_addr = inet_addr("0.0.0.0");

    int ret = bind(lisnfd,(struct sockaddr *)&addr,sizeof(addr));
    Judge(ret);

    ret = listen(lisnfd,5);
    Judge(ret);

    int epoll_handle = epoll_create(2);
    Judge(epoll_handle);

    struct epoll_event ee;
    ee.events = EPOLLIN;
    ee.data.fd = lisnfd;
    ret = epoll_ctl(epoll_handle,EPOLL_CTL_ADD,lisnfd,&ee);
    Judge(ret);

    while(1)
    {
        struct epoll_event arr[MAXSIZE];
        ret = epoll_wait(epoll_handle,arr,MAXSIZE,-1);
        if(ret < 0)
        {
            cout << "epoll_wait failed" << endl;
            return 0;
        }

        for(int i = 0; i < MAXSIZE; ++i)
        {
            if(arr[i].data.fd == lisnfd)
            {
                int sockfd = accept(lisnfd,NULL,NULL);
                if(sockfd < 0)
                {
                    cout << "accept failed" << endl;
                    continue;
                }
                struct epoll_event tmp;
                tmp.events = EPOLLIN;
                tmp.data.fd = sockfd;
                ret = epoll_ctl(epoll_handle,EPOLL_CTL_ADD,sockfd,&tmp);
                if(ret < 0)
                {
                    cout << "epoll_ctl failed" << endl;
                    continue;
                }
            }
            else
            {
                char buf[1024] = {0};
                ssize_t recv_size = recv(arr[i].data.fd,buf,sizeof(buf)-1,0);
                if(recv_size < 0)
                {
                    cout << "recv failed" << endl;
                    continue;
                }
                else if(recv_size == 0)
                {
                    cout << "Peer Shutdown" << endl;
                    epoll_ctl(epoll_handle,EPOLL_CTL_DEL,arr[i].data.fd,&arr[i]);
                    close(arr[i].data.fd);
                    continue;
                }
                else
                    cout << buf << endl;
            }
        }

    }

    close(lisnfd);
    return 0;
}

客戶端代碼和上面select一樣

運行結果:

在這里插入圖片描述

3.3.3 epoll函式的ET模式的驗證和解決

首先,我們需要知道的是將epoll設定為ET模式,只需要在epoll_ctl的時候,將其對應的events設定為EPOLLET即可,

我們使用epoll函式監控0號檔案描述符,當要進行讀取資料的時候,若epoll監控到該檔案描述符就緒之后,則對緩沖區中的內容進行讀操作,我們在這里規定一次讀2個Byte的資料,而我們輸入的資料則要大于2Byte,

首先來看LT模式下,它的運行情況:

#include <unistd.h>
#include <sys/epoll.h>

#include <iostream>

using namespace std;

int main()
{
    int epoll_handle = epoll_create(2);
    if(epoll_handle < 0)
    {
        cout << "epoll_create failed" << endl;
        return 0;
    }
    struct epoll_event ee;
    ee.events = EPOLLIN; 
    ee.data.fd = 0;
    int ret = epoll_ctl(epoll_handle,EPOLL_CTL_ADD,0,&ee);
    if(ret < 0)
    {
        cout << "epoll_ctl failed" << endl;
        return 0;
    }

    while(1)
    {
        struct epoll_event arr[2];
        ret = epoll_wait(epoll_handle,arr,2,-1);
        if(ret < 0)
        {
            cout << "epoll_wait failed" << endl;
            return 0;
        }

        for(int i = 0; i < 2; ++i)
        {
            if(arr[i].data.fd == 0)
            {
                char buf[3] = {0};
                read(arr[i].data.fd,buf,sizeof(buf)-1);
                cout << buf << endl;
            }
        }
    }
    return 0;
}

運行結果:

在這里插入圖片描述
一次只讀兩個位元組,當發現沒有讀完,則會立即重復的進行讀,說明這里的epoll_wait會一直的進行通知,這也就說明在LT模式下,只要當前事件未處理完,則epoll函式會一直進行通知,

ET模式下的運行情況

#include <unistd.h>
#include <sys/epoll.h>
#include <iostream>
using namespace std;
int main()
{
    int epoll_handle = epoll_create(2);
    if(epoll_handle < 0)
    {
        cout << "epoll_create failed" << endl;
        return 0;
    }
    
    struct epoll_event ee;
    //設定對當前檔案描述符的監控為可讀事件和ET模式
    ee.events = EPOLLIN | EPOLLET;
    ee.data.fd = 0;
    int ret = epoll_ctl(epoll_handle,EPOLL_CTL_ADD,0,&ee);
    if(ret < 0)
    {
        cout << "epoll_ctl failed" << endl;
        return 0;
    }

    while(1)
    {
        struct epoll_event arr[2];
        ret = epoll_wait(epoll_handle,arr,2,-1);
        if(ret < 0)
        {
            cout << "epoll_wait failed" << endl;
            return 0;
        }

        for(int i = 0; i < 2; ++i)
        {
            if(arr[i].data.fd == 0)
            {
                char buf[3] = {0};
                read(arr[i].data.fd,buf,sizeof(buf)-1);
                cout << buf << endl;
            }
        }
    }
    return 0;
}

運行情況:

在這里插入圖片描述
我們可以清晰的看到,ET模式下當對應事件就緒之后,不管有沒有處理完,均只會通知一次,除非等待下一個事件進來才會再次進行通知,也就是說如果一次未處理完,在下一次處理的時候,要首先處理上次遺留下來的老資料,

ET模式問題的解決
ET模式所帶來的問題就是如果不能一次將未處理的事件處理完,則會遺留老資料未進行處理,因此,我們只需要對代碼進行控制,使其能夠一次性將其處理完即可,

解決思路:

當第一次監控到檔案描述符就緒之后,我們可以用一個回圈來不停的讀取緩沖區當中的值,直到讀完為止,又因為檔案描述符的讀寫默認屬性是阻塞屬性,因此,我們要將檔案描述符的屬性設定為非阻塞屬性(呼叫fcntl函式)
這里也有需要注意的地方,當呼叫fcntl函式將檔案描述符的屬性設定為非阻塞屬性時,每當進行讀操作的時候,都會回傳一個值,該值代表了讀取的位元組數,當它回傳一個小于0的數時,不一定是讀取失敗了,而是有可能將資料讀完了,這時候就看它回傳的錯誤碼,如果回傳的錯誤是EAGAIN或者EWOULDBLOCK,則代表著將資料讀完了,

代碼如下:

#include <unistd.h>
#include <sys/epoll.h>
#include <fcntl.h>

#include <iostream>

using namespace std;

int main()
{

    //設定檔案描述符的讀寫屬性為非阻塞屬性
    int flag = fcntl(0,F_GETFL);
    if(flag < 0)
    {
        cout << "fcntl failed" << endl;
        return -1;
    }
    
    //設定非阻塞屬性
    fcntl(0,F_SETFL,flag | O_NONBLOCK);

    int epoll_handle = epoll_create(2);
    if(epoll_handle < 0)
    {
        cout << "epoll_create failed" << endl;
        return 0;
    }
    
    struct epoll_event ee;
    //設定對當前檔案描述符的監控為可讀事件和ET模式
    ee.events = EPOLLIN | EPOLLET;
    ee.data.fd = 0;
    int ret = epoll_ctl(epoll_handle,EPOLL_CTL_ADD,0,&ee);
    if(ret < 0)
    {
        cout << "epoll_ctl failed" << endl;
        return 0;
    }

    while(1)
    {
        struct epoll_event arr[2];
        ret = epoll_wait(epoll_handle,arr,2,-1);
        if(ret < 0)
        {
            cout << "epoll_wait failed" << endl;
            return 0;
        }

        for(int i = 0; i < 2; ++i)
        {
            if(arr[i].data.fd == 0)
            {
                string res = "";
                while(1)
                {
                    char buf[3] = {0};
                    int t = read(arr[i].data.fd,buf,sizeof(buf)-1);
                    if(t < 0)
                    {   //注意:這里當回傳值小于0的時候,不一定是發生了錯誤
                        //由于檔案描述符是非阻塞屬性,因此,每次讀的時候都會回傳一個值
                        //若回傳的錯誤碼是EAGAIN或者EWOULDBLOCK的時候,說明此時已經將緩沖區中的內容讀完了
                        if(errno == EAGAIN || errno == EWOULDBLOCK)
                            break;
                    }
                    res += buf;
                }
                cout << res << endl; 
            }
        }
    }

    return 0;
}

結果驗證:

在這里插入圖片描述

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/293218.html

標籤:其他

上一篇:計算機專業畢業生人數穩居前十,你該怎么脫穎而出?

下一篇:詳細分析 —— 函式堆疊幀的創建與銷毀(C語言進階)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more