參考(原文簡直超贊): 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/522427785.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模式沒有驗證