前面介紹了select
函數和poll
函數, select
我們知道能夠支持的套接字個數太少了, 但是poll
函數已經很好了也沒有什麼缺點啊爲啥還要介紹epoll
呢? 接下來我們就來談談poll
和select
函數的其他問題.
poll和select的問題
-
poll
函數一般使用都是將它設置爲輪詢的方式, 這樣是很佔用CPU的; 當然可以將其設置爲阻塞也就能避免這個問題. -
select
能夠監聽的描述符太少. -
poll
和select
的底層實現. poll函數其實和select底層實現類似.當有監聽的事件到來時, select會將該事件的描述符和事件的狀態從內核複製到用戶空間中. poll會將到來事件的描述符狀態複製到用戶空間.
最後用戶都要重新遍歷所有的描述符判斷是哪一個描述符狀態改變.
epoll的優點
poll和select最大的問題都是實現上需要佔用CPU大量的時間, 那麼epoll又是怎樣避免這樣的問題呢.
- epoll函數也要設置監聽描述符並將其複製到內核中爲每一個監聽描述符註冊一個回調函數.
- 當監聽描述符的時候到來時直接返回該描述符的回調函數不需要在進行復制.
可以看出來epoll只有在設置監聽時才複製一次描述符, 事件到來時不需要複製並且也不需要遍歷判斷. 大大的減少了佔用CPU的時間.
如果對於底層實現感興趣可以看一下以前總結的epoll源碼分析和select源碼分析.
函數原型
epoll可是有三個函數哦. 我們一個個來介紹並探討他們的底層實現的功能.
1. epoll_create
#include <sys/epoll.h>
int epoll_create(int size);
成功 : 返回一個文件描述符.
失敗 : 返回-1.
功能 : 在cache中申請創建的紅黑樹大小.
參數
size
: 該參數 原本是指定在cache中分配的內存大小, 在linux2.6以上已經沒有用了.
2. epoll_ctl
int epoll_ctl(int epfd, int opt, int fd, struct epoll_event *event);
成功 : 返回0
失敗 : 返回-1
功能 : 註冊要監聽的事件類型.
參數 :
-
epfd
: epoll_create函數返回的文件描述符. -
opt
: 功能選項, 用三個宏定義表示不同的功能opt值 描述 EPOLL_CTL_ADD 註冊fd的回調函數到epfd中 EPOLL_CTL_DEL 刪除fd註冊的回調函數 EPOLL_CTL_MOD 修改已註冊的fd的監聽事件 -
fd
: 需要監聽的文件描述符 -
event
: 內核需要監聽的事件, 與poll的事件類似.event值 描述 EPOLLIN 監聽是描述符是否可讀 EPOLLOUT 監聽是描述符是否可寫 EPOLLERR 發生錯誤, 對端異常斷開 EPOLLRDHUP 對端掛斷, 或其中一端關閉了 EPOLLET [1] 設置爲邊沿觸發模式 EPOLLONESHOT [2] 設置關聯文件描述符的一次性行爲
3. epoll_wait
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
成功 : 返回0
失敗 : 返回-1
功能 : 從就緒事件的鏈表中返回監聽的回調函數.
參數
epfd
: epoll_create函數返回的文件描述符.events
: 內核返回的監聽事件maxevents
: events數組的大小timeout
: 超時時間- timeout == INFTIM(負數) : select函數永遠阻塞等待監視文件描述符集合中某個文件描述符發生變化爲止.
- timeout == 0 : 函數爲非阻塞函數, 不管有無等待的文件描述符發生變化都會返回
- timeout > 0 : 等待的超時時間, 即函數在timeout時間內阻塞, 超時時間之內有事件到來就返回了, 否則在超時後不管怎樣一定返回.
函數調用
看了前面的三個函數估計第一次接觸可能會產生牴觸, 太麻煩了. 但是麻煩歸麻煩, 好用高效率纔是王道. 比如在百萬連接時, epoll_wait都可以只需要常量時間就能精確返回觸發事件的套接字.
說了那麼多, 還是練習能最快掌握, 完整代碼 epoll_service.c
void doService(int servicefd)
{
int clientfd, epfd;
char buf[1024];
// 設置監聽套接字
struct epoll_event event, evts[EPOLL_MAX];
event.events = EPOLLIN;
event.data.fd = servicefd; // 這裏需要設置監聽的套接字
epfd = epoll_create(1);
epoll_ctl(epfd, EPOLL_CTL_ADD, servicefd, &event); // 這裏也需要設置監聽的套接字
int n, eventNum;
while(1)
{
// 設置爲永久阻塞
eventNum = epoll_wait(epfd, evts, EPOLL_MAX, -1);
if(eventNum == 0)
continue;
else if(eventNum < 0)
EXIT("epoll_wait");
// 遍歷狀態改變的套接字
for(int i = 0; i < eventNum; i++)
{
if(evts[i].data.fd == servicefd && (evts[i].events &EPOLLIN))
{
clientfd = Accept(servicefd, NULL, NULL);
event.events = EPOLLIN;
event.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &event);
}
else if(evts[i].events & EPOLLIN)
{
n = recv(evts[i].data.fd, buf, sizeof(buf), 0);
if(n <= 0)
{
// 刪除關閉的套接字
epoll_ctl(epfd, EPOLL_CTL_DEL, evts[i].data.fd, NULL);
close(evts[i].data.fd);
fprintf(stderr, "peer close\n");
}
send(evts[i].data.fd, buf, n, 0);
}
}
}
}
具體現象與select
是一樣, 這裏就不再驗證了.
總結
本節介紹了epoll的三個函數, 希望能夠掌握並且自己修改服務端的函數. 接下來我們還要繼續介紹epoll的功能.
- 掌握epoll
- epoll 的優點
- poll 和 select 的缺點