為什么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模式沒有驗證


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM