EPOLL使用的簡單總結
EPOLL使用簡單總結
0. 為什麼要用epoll
既然用到epoll,一定對select和poll有一定的瞭解。
Select需要與fd_set結構體配合使用,並在使用者空間維護一個客戶端描述符,且管理控制代碼時有數目的限制。
Poll解決了控制代碼數目的限制(連結串列實現),同時維護一個pollfd結構體的客戶端事件的集合。
這來倆效能侷限點為:
Select和POLL都會遍歷整個集合來確定活躍描述符
與核心互動時會把所有控制代碼拷貝到核心
注意的是:
伺服器效能四大殺手:
1.資料拷貝-> 快取方案
2.環境切換(執行緒切換)->單核單執行緒,多核多執行緒
3.記憶體分配->記憶體池
4.鎖競爭->減少鎖的使用
Poll每次需要從使用者態將所有的控制代碼複製到核心態,如果以萬計的控制代碼會導致每次都要copy幾十幾百KB的記憶體到核心態,非常低效。使用epoll時你只需要呼叫epoll_ctl事先新增到對應紅黑樹,真正用epoll_wait時不用傳遞socket控制代碼給核心,節省了拷貝開銷。
以上此段出自阿里雲《epoll全面講解:從實現到應用》https://www.aliyun.com/jiaocheng/122174.html
Epoll在核心的實現使用了mmap共享記憶體,紅黑樹和鎖,所以在一定條件下提升機器的效能:
大量連結的/不是所有的控制代碼都很活躍 條件下使用epoll
1. 為什麼要使用非阻塞模式
ET模式需要非阻塞。
為此我們需要知道什麼是阻塞模式,非阻塞模式,IO複用模型。
此外,在伺服器程式中發生阻塞一般是讀寫資料和accept等待連結的時候。
以下圖和思想,來源於《Unix網路程式設計 第二版》第一卷 第二部分 第六章 第二節
阻塞模式:
正如原文所說,一開始寫的網路程式設計程式碼都是阻塞模式,直觀一點的意思就是沒有用到select/poll,直接使用socket-> sockaddr_in ->bind->listen->while(1)->accept模型的簡單回射伺服器就是阻塞IO模型應用。此模型的侷限是一個執行緒或者程序只能同時處理一個描述符。
非阻塞模式:
也就是應用層一直檢查核心是否準備好資料,直到完成。可以做個簡單的實驗,就上面說過的回射伺服器,直接設定成非租塞,accept會一直返回-1。原因是一直在等待連結,當連結到來讀寫完資料,再次瘋狂返回-1。
IO複用模式:
如圖,IO複用其實就是select/poll/epoll這類的函式,它們們幫我們完成了核心的監控,並可以監控多個,當核心某個IO準備好後通知我們,我們在呼叫。與上面的非阻塞模式配合使用就不會反會-1的錯誤(當資料準備好後再accept,舉例select也就是if (pollfds[0].revents & POLLIN){… accept …})。
我在使用第一次使用epoll時候(就是寫這完文件的前一天)使用的是<非阻塞+IO複用+LT模式>,其實LT模式下非阻塞效能不高,但是好寫。
之後會改ET。
先放個圖。
圖片來源圖片來源CSDN《epoll EPOLLL、EPOLLET模式與阻塞、非阻塞》https://blog.csdn.net/zxm342698145/article/details/80524331
2. epoll使用(c++,面向過程)
先說一下用epoll和不用IO複用網路伺服器程式設計的區別
首先是阻塞的程式設計流程(個人總結不是很嚴謹):
然後就是epoll 的IO複用程式設計模型:
程式碼中的體現如下:
int main()
{
/*Socket(AF_INET, SOCK_STREAM, 0),我這裡設定了非阻塞模式,下面的accept4也是。*/
int listenfd;
listenfd = Socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); // fei zu se IO fu yong
/*設定伺服器的sockaddr_in結構體,IPv4,當前地址,8000埠*/
struct sockaddr_in serveraddr;
bzero(&serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(8000);
/*重連處理*/
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
/*Bind繫結描述符和伺服器結構體*/
Bind(listenfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
/*Listen監聽描述符*/
Listen(listenfd, 20);
/*準備客戶端sockaddr_in結構體,以及accept的返回值*/
struct sockaddr_in clientaddr;
socklen_t clientlen;
int connfd;
/*準備epoll的epoll_event結構體集,用的是c++的向量,為了方便*/
typedef std::vector<struct epoll_event> EpollList;
/*epoll_create1(EPOLL_CLOEXEC)生成用於處理accept的epoll專用的檔案描述符,建立一個epoll的控制代碼*/
int epollfd;
epollfd = epoll_create1(EPOLL_CLOEXEC);
//Creates a handle to epoll, the size of which tells the kernel how many listeners there are.
/*設定epoll_event結構體監聽事件,epoll_event結構體的變數,epfd用於註冊事件*/
struct epoll_event epfd;
epfd.data.fd = listenfd;
epfd.events = EPOLLIN/*| EPOLLET */;
/*epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &epfd);
epollfd為epoll_create1返回,epfd為epoll_event結構體監聽事件的結構體
epoll的事件註冊函式,它不同與select()是在監聽事件時(epoll使用epoll_wait監聽)告訴核心要監聽什麼型別的事件,而是在這裡先註冊要監聽的事件型別*/
epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &epfd);
/*設定epoll_event結構體集的大小*/
EpollList events(16);//You can listen for 16 at first
int nready;//活躍描述符個數
while(1)
{
/*nready = epoll_wait(epollfd, &*events.begin(), static_cast<int>(events.size()), -1);
等待事件的產生,類似於select()呼叫。引數events用來從核心得到事件的集合,maxevents告之核心這個events有多大,
這個 maxevents的值不能大於建立epoll_create()時的size,引數timeout是超時時間(毫秒,0會立即返回,-1是永久阻塞)。
該函式返回需要處理的事件數目,如返回0表示已超時。*/
nready = epoll_wait(epollfd, &*events.begin(), static_cast<int>(events.size()), -1);
if (nready == -1)//出錯處理
{
if(errno == EINTR)
continue;
perror("epoll_wait");
}
if(nready == 0) //如果沒有活躍的重來
continue;
if ((size_t)nready == events.size())//如果結構體集不夠用了,倍增
{
events.resize(events.size() * 2);
}
/*遍歷返回的活躍描述符for(int i=0; i < nready; ++i)*/
for(int i=0; i < nready; ++i)
{
/*if (events[i].data.fd == listenfd)監聽活躍*/
if (events[i].data.fd == listenfd)
{
/*Accept客戶端結構體和監聽描述符,返回一個客戶描述符,accept4比accept定義一個引數*/
clientlen = sizeof(clientaddr);
connfd = Accept4(listenfd, (struct sockaddr*)&clientaddr, &clientlen,
SOCK_NONBLOCK | SOCK_CLOEXEC);// fei zu se IO fu yong
std::cout << connfd << "is come!" << std::endl;
/*有客戶訪問到來,修改結構體事件,把監聽描述符改為客戶連結描述符,寫入核心*/
epfd.data.fd = connfd;
epfd.events = EPOLLIN/* | EPOLLET*/;
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &epfd);
}
/*if (events[i].events & EPOLLIN)客戶描述符活躍,客戶連結描述符有可讀事件*/
else if (events[i].events & EPOLLIN)
{
connfd = events[i].data.fd;//取出連結描述符使用
if (connfd < 0)
{
continue;
}
/*用於讀寫的準備*/
char buf[100];
bzero(buf, sizeof(buf));
int n;
if ((n = read(connfd, buf, 100)) > 0)
{
std::cout << "::" << connfd <<" Date: ["<< buf <<"]" << std::endl;
write(connfd, buf, n);
}
/*關閉描述符,就是客戶斷開連線後處理*/
else if (n == 0)
{
std::cout << connfd << "is go" << std::endl;
close(connfd);
epfd = events[i];
epoll_ctl(epollfd, EPOLL_CTL_DEL, connfd, &epfd);
}
}
}
}
return 0;
}
3. epoll介面和結構體
Epoll的標頭檔案:
#include <sys/epoll.h>
Epoll的函式介面:
Int epoll_create(int size);
引數size為設定可以連線的多少,老的create函式,例項epoll,現在引數size被忽略,大小取決於核心的處理能力。
Int epoll_create1(int flags);
推薦使用的新版本, flags引數的值為EPOLL_CLOEXEC ;
Int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
事件註冊函式
-
epfd:epoll_create返回的例項;
-
op:表示動作
EPOLL_CTL_ADD:註冊新的fd到epfd中;
EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;
EPOLL_CTL_DEL:從epfd中刪除一個fd; -
fd:監聽的檔案描述符;
-
event:通知核心的結構體下頁說明
Int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
相當於select函式等待事件產生maxevents:通知核心event的大小;timeout:超時時間,-1為永遠等待;
EPOLL結構體:
Typedef union epoll_date{
void *ptr;
int fd;
unit32_t u32;
unit64_t u64;
}epoll_data_t
聯合體,使用者資料變數,一般使用fd檔案描述符
Struct epoll_event{
unit32_t events;
epoll_data_t date;
}
events可以是以下幾個巨集的集合:
**EPOLLIN **:表示對應的檔案描述符可以讀(包括對端SOCKET正常關閉);
EPOLLOUT:表示對應的檔案描述符可以寫;
EPOLLPRI:表示對應的檔案描述符有緊急的資料可讀(這裡應該表示有帶外資料到來);
EPOLLERR:表示對應的檔案描述符發生錯誤;
EPOLLHUP:表示對應的檔案描述符被結束通話;
EPOLLET: 將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的。
EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個 socket加入到EPOLL佇列裡
參考部落格
博主:lvyilong316
http://blog.chinaunix.net/uid/28541347.html
epoll專欄,共10篇