天天看點

為什麼epoll會那麼高效

參考(原文簡直超贊): https://zhidao.baidu.com/question/687563051895364284.html

下面是我結合原文寫的,為了便于自己了解:

關于阻塞和非阻塞的了解可以看這個:http://www.cnblogs.com/xcywt/p/8146123.html

1.舉例子說明

假設你在讀大學,有個朋友F來找你,你住在A棟。但是不知道具體是哪個房間。于是你們約好在A棟門口見面。

如果用阻塞IO模型來處理這個問題,你就相當于一直在A棟門口等着,這個時候你不能做别的事情,效率比較低,如果F一直不來你就得一直在那等着。

接着來看用非阻塞模型來處理這個問題,主要有兩種select/poll(這兩個可以看成一種)和epoll:

select大媽做的事情是這樣:當朋友F到了樓下時,她帶着F一個個房間了輪詢的去找你。

epoll大媽就比較進階了:大媽拿本子記錄下你的房間号,當朋友F來的時候告訴F你的房間号。這樣就不用整棟樓去跑了。

在大并發伺服器中,輪詢IO是一件比較費時的操作,就跟select大媽一樣。

epoll大媽多用了一個本子,就有點用空間去換取時間的意思。

2.select/poll為什麼慢:

1)select/poll 是周遊所有添加進fd_set的fd。并且需要将所有使用者态的fd拷貝到核心态。數量巨大時這個效率比較慢

2)并且傳回之後,還要輪詢将所有集合查詢一次

3)核心空間的資料需要拷貝到使用者空間

3.epoll的實作原理:

具體使用方法可以參考:http://www.cnblogs.com/xcywt/p/8146094.html

先說幾個函數的作用

       int epoll_create(int size); // 建立一個epoll對象,size是核心保證能夠正确處理的最大句柄數。

       int epoll_create1(int flags);// 上面的加強版本,參數隻能是EPOLL_CLOEXEC

       int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // 操作epoll對象

       int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);// 在給定時間内,監控的所有句柄中有時間發生就傳回

下面我們來看具體做了什麼:

  epoll在核心初始化的時候向核心注冊了一個檔案系統,用于存儲上述被監控的socket,同時還會開辟出epoll自己的核心高速cache區,用于安置需要監控的fd。這些fd以紅黑樹的形式儲存在核心cache裡,以支援快速的查找、插入、删除。這個核心高速cache區,就是建立連續的實體記憶體頁,然後在之上建立slab層,簡單的說就是實體上配置設定好你想要的大小的記憶體對象,每次使用時都是使用空閑的已配置設定好的對象。

  每次調用epoll_create時,會在這個虛拟的epoll檔案系統裡建立一個file節點,在核心cache中建立個紅黑樹來存儲通過epoll_ctl添加進來的fd。這些fd其實已經在核心态了,當你再次調用epoll_wait時,不需要再拷貝進核心态(select需要再全部拷貝到核心态)。

  同時還會建立一個list連結清單,用來存儲已經就緒的事件。被epoll_wait調用時,就去看這個list連結清單是不是為空,若不為空就傳回,為空就等待指定的事件再傳回。

  list連結清單是如何維護的呢:當我們執行epoll_ctl時,會把對應fd放到紅黑樹中,還會給核心終端處理程式注冊一個回調函數。如果這個句柄的中斷到了,就把它放在list連結清單中去。

  總結一下:一棵紅黑樹和一個list連結清單就解決大并發的問題。epoll_create時建立紅黑樹和就緒連結清單,epoll_ctl時添加到紅黑樹中(若存在則不添加)并向核心注冊回調函數。epoll_wait時傳回list就緒連結清單裡面的資料就可以了。

4.epoll的兩個工作模式:

LT:隻要一個句柄上的事件一次沒有處理完,接着調用epoll_wait時仍然會傳回這個句柄。

ET:盡在空閑狀态->就緒狀态傳回一次。

這件事是怎麼做到的呢:當有fd'發生事件時,就放到list就緒連結清單中去了。然後epoll_wait傳回,再然後清空準備list就緒連結清單。

最後如果是LT模式,并且仍有未處理的事件,就把這個fd重新放回到list就緒連結清單中。

如果是ET,就不管了,不管有沒有事件未處理完都不再添加到list就緒連結清單中。

就有點像下面的流程:

wait傳回 -> 清空list就緒連結清單
if(LT模式)
{
  if(存在未處理完的事件)
  {
    重新添加進list就緒連結清單中
  }
}
else // ET 模式
{

}      

關于觸發模式詳解,這裡面也講的比較詳細:

http://blog.csdn.net/weiyuefei/article/details/52242778

5.ET模式被喚醒的條件:

對于讀取操作:

1)buffer由不可讀,變為可讀的時候。

2)buffer資料變多的時候,有新的資料到來

3)當buffer不為空(有資料可讀),且使用者對相應fd進行epoll_mod  IN 事件時。(待會用代碼示範)

對于寫操作:

1)由不可寫,變成可寫

2)buffer是資料變少的時候,也就是被讀走了一部分3)buffer有可寫空間,且使用者對相應fd進行epoll_mod OUT 事件時。

對于LT模式:

讀操作:隻要緩沖區中有資料,且讀完一部分之後還不空的時候,就會傳回

寫操作:當發送緩沖區沒滿,寫了一下還不滿的時候,epoll_wait傳回讀事件。

補充一個例子1:驗證ET模式的讀取傳回的前2個:

#include<unistd.h>
#include<iostream>
#include<sys/epoll.h>
using namespace  std;
int main()
{
    int epfd, ret;
    struct epoll_event ev, events[5];
    epfd = epoll_create(1);
    ev.data.fd = STDIN_FILENO;
    ev.events = EPOLLIN|EPOLLET; // 标記A,這裡是ET模式
    //ev.events = EPOLLIN; // 标記B。表示預設是LT模式
    char buf[1024] = {0};
    epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev); //添加标準輸入
    while(1)
    {
        ret = epoll_wait(epfd, events, 5, -1);
        for(int i=0; i < ret; i++)
        {
            if(events[i].data.fd == STDIN_FILENO)
            {
                //read(STDIN_FILENO, buf, sizeof(buf)); // 标記C
                cout << "hello world, recv:" << buf << endl;
            }
        } 
    }
    return 0;
}      

分三種情況讨論:

1)打開标記A,注釋B和C:這種情況運作,雖然輸入緩沖區裡面還有資料,但是“hello world”也不會一直列印。

因為邊沿觸發,一定要等到下一次事件到來 wait才會傳回。

2)打開B,注釋A和C:切換成了LT模式,隻要緩沖區裡面還有資料嗎,wait會一直傳回。是以helloworld會一直列印

3)打開B和C,注釋A:LT模式,但是每次wait之後把緩沖區裡面的資料讀完了,相當于處理完了這個事件。wait就不會傳回了。除非标準輸入中再輸入資料。

例子2:驗證ET模式的讀取傳回的第3個:

#include<unistd.h>
#include<iostream>
#include<sys/epoll.h>
using namespace  std;
int main()
{
    int epfd, ret;
    struct epoll_event ev, events[5];
    epfd = epoll_create(1);
    ev.data.fd = STDIN_FILENO;
    ev.events = EPOLLIN|EPOLLET; 
    char buf[1024] = {0};
    epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev);
    while(1)
    {
        ret = epoll_wait(epfd, events, 5, -1);
        for(int i=0; i < ret; i++)
        {
            if(events[i].data.fd == STDIN_FILENO)
            {
                cout << "hello world << endl;
                ev.data.fd = STDIN_FILENO;
                ev.events = EPOLLIN|EPOLLET;
                epoll_ctl(epfd, EPOLL_CTL_MOD, STDIN_FILENO, &ev); // 這裡對fd進行epoll_mod  IN 事件
            }
        } 
    }
    return 0;
}      

可以看到當輸入一次之後,依然會有死循環列印helloworld。

例子3:驗證ET模式的寫傳回,前2個

#include<unistd.h>
#include<iostream>
#include<sys/epoll.h>
using namespace  std;
int main()
{
    int epfd, ret;
    struct epoll_event ev, events[5];
    epfd = epoll_create(1);
    ev.data.fd = STDIN_FILENO;
    ev.events = EPOLLOUT|EPOLLET; 
    char buf[1024] = {0};
    epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev);
    while(1)
    {
        ret = epoll_wait(epfd, events, 5, -1);
        for(int i=0; i < ret; i++)
        {
            if(events[i].data.fd == STDIN_FILENO)
            {
                //cout << "hello world" << endl; // 标記A
                cout << "hello world"; // 标記B
            }
        } 
    }
    return 0;
}      

對于ET模式。

1)打開标記A,注釋标記B:可以看到會死循環,因為這裡有 endl 。标準輸出為控制台的時候緩沖的“行緩沖”,是以換行符号導緻buffer中的内容被清空。就相當于上面條件中的第二個,有資料發送走了。是以會一直循環

2)打開B,注釋A:不發送endl,就相當于buffer中一直有資料存在,是以wait不會一直傳回。

例子4,ET模式的寫傳回第三個條件。

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

int main()
{
    int epfd, ret;
    struct epoll_event ev, events[5];
    epfd = epoll_create(1);
    ev.data.fd = STDIN_FILENO;
    ev.events = EPOLLOUT|EPOLLET;

    char buf[1024] = {0};
    epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev);
    while(1)
    {
        ret = epoll_wait(epfd, events, 5, -1);
        for(int i=0; i < ret; i++)
        {
            if(events[i].data.fd == STDIN_FILENO)
            {
                cout << "hello world";
                ev.data.fd = STDIN_FILENO;
                ev.events = EPOLLOUT;
                epoll_ctl(epfd, EPOLL_CTL_MOD, STDIN_FILENO, &ev); // 這裡對fd進行epoll_mod  OUT 事件
            }
        } 
    }

    return 0;
}      

每次輸出helloworld後重新MOD OUT 事件。也會一直循環列印。

注意:LT模式沒有驗證