epoll源碼剖析
文章目錄
1.前言
好久好久沒有更新博客了,最近一直在實(shí)習(xí),刷算法找工作,忙里偷閑簡(jiǎn)單研究了一下epoll的源碼。也是由于面試的時(shí)候經(jīng)常被問到,我只會(huì)說那一套,什么epoll_create創(chuàng)建紅黑樹,以O(shè)(1)的方式去讀取數(shù)據(jù),它和poll與select的區(qū)別等等。本篇將從epoll的源碼層面重新學(xué)習(xí)epoll。
2.應(yīng)用層的體現(xiàn)
多路轉(zhuǎn)接(epoll)實(shí)現(xiàn)我在這篇文章中實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的epoll網(wǎng)絡(luò)server。感興趣的同學(xué)可以簡(jiǎn)單閱讀一下,我只挑其中關(guān)鍵的代碼來講一下應(yīng)用層是如何使用epoll的:
#include"Sock.hpp"
using namespace ns_Sock;
#define NUM 128
#include<sys/epoll.h>
#include <cstdlib>
void Usage(char* proc)
{
cout<<"Usage \n\t"<<proc<<" port"<<endl;
}
int main(int argc,char* argv[])
{
if(argc!=2)
{
Usage(argv[0]);
exit(-1);
}
uint16_t port=(uint16_t)atoi(argv[1]);
int listen_sock=Sock::Socket();
Sock::Bind(listen_sock,port);
Sock::Listen(listen_sock);
//建立epoll模型,獲得epfd
int epfd=epoll_create(128);
//先添加listen_sock和它所關(guān)心的事件到內(nèi)核中
struct epoll_event ev;
ev.events=EPOLLIN;
ev.data.fd=listen_sock;//雖然epoll_ctl有文件描述符,但是revs數(shù)組中的元素是epoll_event沒有fd,因此需要將fd添加都epoll_event的data字段中
epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev);
//事件循環(huán)
volatile bool quit=false;
struct epoll_event revs[NUM];//由于epoll_wait的數(shù)組是輸出型參數(shù),因此需要接收
while(!quit)
{
int timeout=1000;
int n=epoll_wait(epfd,revs,NUM,-1);//epoll_wait會(huì)將epfd中就緒事件的epoll_event結(jié)構(gòu)體放在revs數(shù)組中,返回值表示數(shù)組大小
switch(n)
{
case 0:
cout<<"timeout....."<<endl;
break;
case -1:
cerr<<"epoll error"<<endl;
break;
default:
cout<<"有事件就緒了"<<endl;
//處理就緒事件
for(int i=0;i<n;i++)
{
int sock=revs[i].data.fd;//暫時(shí)方案
cout<<"文件描述符"<<sock<<"有數(shù)據(jù)就緒了"<<endl;
if(revs[i].events&EPOLLIN)//讀事件就緒
{
cout<<"文件描述符"<<sock<<"讀事件就緒了"<<endl;
if(sock==listen_sock)
{
int fd=Sock::Accept(listen_sock);
if(fd>=0)
{
cout<<"獲取新鏈接成功了"<<endl;//此時(shí)還不能讀需要添加到epfd的空間中
struct epoll_event _ev;
_ev.events=EPOLLIN;
_ev.data.fd=fd;
epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&_ev);
cout<<"已經(jīng)把"<<fd<<"添加到epfd空間中了"<<endl;
}
}
//正常的讀處理
else
{
cout<<"文件描述符"<<sock<<"正常數(shù)據(jù)準(zhǔn)備就緒"<<endl;
char buffer[1024];
ssize_t s=read(sock,buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s]=0;
cout<<"client["<<sock<<"]#"<<buffer<<endl;
}
else if(s==0)
{
cout<<"client quit "<<sock<<"將被關(guān)閉"<<endl;
close(sock);
epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr);//將該套接字從epoll空間關(guān)注的位置刪除
cout<<"Sock:"<<sock<<"delete from epoll success"<<endl;
}
else
{
cout<<"recv error"<<endl;
close(sock);
epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr);
cout<<"delete sock"<<sock<<endl;
}
}
}
}
}
}
}
這是我那篇博客的服務(wù)器端的代碼,使用telnet是可以直接訪問的,通過這段代碼我們可以發(fā)現(xiàn)調(diào)用epoll的過程以及一些細(xì)節(jié)。首先就是眾所周知的:
- epoll_create創(chuàng)建一個(gè)epoll空間。
- 接著調(diào)用epoll_ctl將一個(gè)文件描述符以及對(duì)該文件描述符需要關(guān)心的事件放進(jìn)epoll空間中。
- 然后調(diào)用epoll_wait進(jìn)行等待就好了。事件就緒會(huì)使用epoll_wait這個(gè)函數(shù)來通知我們。
但仔細(xì)看代碼還是會(huì)發(fā)現(xiàn)一些細(xì)節(jié),在epoll空間建立完成后,添加的第一個(gè)文件描述符就是listen_sock,并且關(guān)心它的讀事件。
在epoll_wait成功的時(shí)候,會(huì)返回一個(gè)就緒事件的數(shù)組,通過遍歷這個(gè)數(shù)組去對(duì)數(shù)據(jù)進(jìn)行操作,但當(dāng)該數(shù)組元素表示的是listen_sock事件就緒的時(shí)候,需要在listen_sock中將新監(jiān)聽到的鏈接accept上來。這是對(duì)監(jiān)聽套接字獨(dú)有的一種處理方式,也說明epoll的回調(diào)函數(shù)不止一個(gè)觸發(fā)條件(數(shù)據(jù)就緒orl鏈接就緒)。
其中這里還涉及到了一個(gè)重要的結(jié)構(gòu)體epoll_event,這個(gè)結(jié)構(gòu)體在內(nèi)核源碼中也經(jīng)常使用到,下面給出它在linux系統(tǒng)中的結(jié)構(gòu):
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event
{
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
} __EPOLL_PACKED;
其中它的events表示的是事件,data又是一個(gè)結(jié)構(gòu)體,它其中的一個(gè)重要的東西就是fd。它表示該epoll_event是哪個(gè)套接字的。
3.兩個(gè)重要結(jié)構(gòu)
(1)eventpoll
其中一個(gè)結(jié)構(gòu)名為eventpoll,當(dāng)調(diào)用epoll_create的時(shí)候內(nèi)核會(huì)自動(dòng)創(chuàng)建它,所以其實(shí)所謂的epoll空間僅僅是一個(gè)結(jié)構(gòu)體而已。
struct eventpoll {
/*
struct ep_rb_tree {
struct epitem *rbh_root;
}
*/
ep_rb_tree rbr; //rbr指向紅黑樹的根節(jié)點(diǎn)
int rbcnt; //紅黑樹中節(jié)點(diǎn)的數(shù)量(也就是添加了多少個(gè)TCP連接事件)
LIST_HEAD( ,epitem) rdlist; //rdlist指向雙向鏈表的頭節(jié)點(diǎn);
/* 這個(gè)LIST_HEAD等價(jià)于
struct {
struct epitem *lh_first;
}rdlist;
*/
int rdnum; //雙向鏈表中節(jié)點(diǎn)的數(shù)量(也就是有多少個(gè)TCP連接來事件了)
// ...略...
};
它包含了幾個(gè)內(nèi)容,有我們熟知的紅黑樹根節(jié)點(diǎn),還有所謂的就緒隊(duì)列(是一個(gè)雙鏈表),以及紅黑樹與就緒隊(duì)列的節(jié)點(diǎn)數(shù)量。
(2)epitem
這個(gè)是比較關(guān)鍵的一個(gè)結(jié)構(gòu)體,epoll_ctl將文件描述符以及所關(guān)心的事件插入到epoll空間中,插入的就是這么個(gè)玩意。
struct epitem {
RB_ENTRY(epitem) rbn;
/* RB_ENTRY(epitem) rbn等價(jià)于
struct {
struct type *rbe_left; //指向左子樹
struct type *rbe_right; //指向右子樹
struct type *rbe_parent; //指向父節(jié)點(diǎn)
int rbe_color; //該節(jié)點(diǎn)的顏色
} rbn
*/
LIST_ENTRY(epitem) rdlink;
/* LIST_ENTRY(epitem) rdlink等價(jià)于
struct {
struct type *le_next; //指向下個(gè)元素
struct type **le_prev; //前一個(gè)元素的地址
}*/
int rdy; //判斷該節(jié)點(diǎn)是否同時(shí)存在與紅黑樹和雙向鏈表中
int sockfd; //socket句柄
struct epoll_event event; //存放用戶填充的事件
};
首先它包含了一個(gè)紅黑樹節(jié)點(diǎn)的結(jié)構(gòu),其次他又包含了雙向鏈表的結(jié)構(gòu),到這里我們就可以發(fā)現(xiàn),紅黑樹和雙向鏈表中插入的是同一個(gè)結(jié)構(gòu)體epitem。也很容易想到epoll_ctl的本質(zhì)其實(shí)就是使用傳入的參數(shù)來構(gòu)造一個(gè)epitem。
因此它包含的參數(shù)顯而易見:sockfd表示該epitem對(duì)應(yīng)的套接字,epoll_event表示需要關(guān)心的該套接字的事件。
4.四個(gè)函數(shù)
(1)epoll_create源碼
int epoll_create(int size) {
//size沒有什么卵用
if (size <= 0) return -1;
//tcp服務(wù),我也不懂,目前不是重點(diǎn)
nty_tcp_manager *tcp = nty_get_tcp_manager();
if (!tcp) return -1;
struct _nty_socket *epsocket = nty_socket_allocate(NTY_TCP_SOCK_EPOLL);
if (epsocket == NULL) {
nty_trace_epoll("malloc failed\n");
return -1;
}
//1.建立一個(gè)eventpoll
struct eventpoll *ep = (struct eventpoll*)calloc(1, sizeof(struct eventpoll));
if (!ep) {
nty_free_socket(epsocket->id, 0);
return -1;
}
//2.初始化紅黑樹指針為空,節(jié)點(diǎn)數(shù)為0
ep->rbcnt = 0;
RB_INIT(&ep->rbr);
//3.雙向鏈表頭指向空
LIST_INIT(&ep->rdlist);
//線程操作,暫時(shí)不用關(guān)心
if (pthread_mutex_init(&ep->mtx, NULL)) {
free(ep);
nty_free_socket(epsocket->id, 0);
return -2;
}
if (pthread_mutex_init(&ep->cdmtx, NULL)) {
pthread_mutex_destroy(&ep->mtx);
free(ep);
nty_free_socket(epsocket->id, 0);
return -2;
}
if (pthread_cond_init(&ep->cond, NULL)) {
pthread_mutex_destroy(&ep->cdmtx);
pthread_mutex_destroy(&ep->mtx);
free(ep);
nty_free_socket(epsocket->id, 0);
return -2;
}
if (pthread_spin_init(&ep->lock, PTHREAD_PROCESS_SHARED)) {
pthread_cond_destroy(&ep->cond);
pthread_mutex_destroy(&ep->cdmtx);
pthread_mutex_destroy(&ep->mtx);
free(ep);
nty_free_socket(epsocket->id, 0);
return -2;
}
//4.保存ep對(duì)象,通過epid可以在系統(tǒng)中找到eventpoll結(jié)構(gòu)
tcp->ep = (void*)ep;
epsocket->ep = (void*)ep;
return epsocket->id;
}
我們發(fā)現(xiàn)在epoll_create的時(shí)候會(huì)傳入一個(gè)參數(shù),但是這個(gè)參數(shù)似乎沒有意義,從這里就可以很簡(jiǎn)單的看出了,這個(gè)size除了判斷一下是不是大于0之外,之后什么都沒做。因此這個(gè)參數(shù)是沒有意義的。
它的主要功能我在代碼中標(biāo)注了,大致如下:
- 1.建立一個(gè)eventpoll對(duì)象,并為其分配空間。
- 2.將該對(duì)象中紅黑樹根節(jié)點(diǎn)指向空,節(jié)點(diǎn)個(gè)數(shù)設(shè)為0。
- 3.將該對(duì)象中雙向鏈表頭節(jié)點(diǎn)指向空。
- 4.將eventpoll對(duì)象保存起來。以后可以通過epid找到它,并返回這個(gè)epid。
(2)epoll_ctl源碼
下面來看看epoll_ctl都干了些什么,在講解這個(gè)函數(shù)之前,我們還是先回頭看一下我們是怎么向它傳參的:
epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev);
這是我們將listen_sock加入到epoll空間中的代碼,可以看到epfd用于尋找eventpoll結(jié)構(gòu)體,EPOLL_CTL_ADD表示我是要加入一個(gè)epoll節(jié)點(diǎn),listen_sock表示我加入的這個(gè)節(jié)點(diǎn)的文件描述符是listen_sock,&ev表示我關(guān)心的事件(使用地址傳參為了節(jié)省空間),這一切是不是很明確了呢?下面我們來閱讀這部分的源碼。
int epoll_ctl(int epid, int op, int sockid, struct epoll_event *event) {
//tcp部分,暫時(shí)不管
nty_tcp_manager *tcp = nty_get_tcp_manager();
if (!tcp) return -1;
nty_trace_epoll(" epoll_ctl --> 1111111:%d, sockid:%d\n", epid, sockid);
//1.通過傳入進(jìn)來的epid找到對(duì)應(yīng)的eventpoll結(jié)構(gòu)體
struct _nty_socket *epsocket = tcp->fdtable->sockfds[epid];
//struct _nty_socket *socket = tcp->fdtable->sockfds[sockid];
//nty_trace_epoll(" epoll_ctl --> 1111111:%d, sockid:%d\n", epsocket->id, sockid);
if (epsocket->socktype == NTY_TCP_SOCK_UNUSED) {
errno = -EBADF;
return -1;
}
if (epsocket->socktype != NTY_TCP_SOCK_EPOLL) {
errno = -EINVAL;
return -1;
}
nty_trace_epoll(" epoll_ctl --> eventpoll\n");
struct eventpoll *ep = (struct eventpoll*)epsocket->ep;
if (!ep || (!event && op != EPOLL_CTL_DEL)) {
errno = -EINVAL;
return -1;
}
//2.判斷如果是增加節(jié)點(diǎn)
if (op == EPOLL_CTL_ADD) {
pthread_mutex_lock(&ep->mtx);
//在棧區(qū)先創(chuàng)建一個(gè)對(duì)象,接收sockid
struct epitem tmp;
tmp.sockfd = sockid;
//查找這個(gè)節(jié)點(diǎn)是否在紅黑樹中,其實(shí)也就是根據(jù)epollfd查找的,這查找傳入tmp,可能表示的是紅黑樹節(jié)點(diǎn)類型,這是一個(gè)小細(xì)節(jié)
struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
if (epi) {
nty_trace_epoll("rbtree is exist\n");
pthread_mutex_unlock(&ep->mtx);
return -1;
}
//如果不存在才為結(jié)構(gòu)體分配完整空間
epi = (struct epitem*)calloc(1, sizeof(struct epitem));
if (!epi) {
pthread_mutex_unlock(&ep->mtx);
errno = -ENOMEM;
return -1;
}
//使用參數(shù)構(gòu)建這個(gè)epitem節(jié)點(diǎn)
epi->sockfd = sockid;
memcpy(&epi->event, event, sizeof(struct epoll_event));
epi = RB_INSERT(_epoll_rb_socket, &ep->rbr, epi);
assert(epi == NULL);
ep->rbcnt ++;
pthread_mutex_unlock(&ep->mtx);
} else if (op == EPOLL_CTL_DEL) {
pthread_mutex_lock(&ep->mtx);
struct epitem tmp;
//先把fd分配給sockfd
tmp.sockfd = sockid;
//查找該節(jié)點(diǎn)是否已經(jīng)存在,這也說明是根據(jù)sockfd查找的
struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
if (!epi) {
nty_trace_epoll("rbtree no exist\n");
pthread_mutex_unlock(&ep->mtx);
return -1;
}
//存在則刪除即可
epi = RB_REMOVE(_epoll_rb_socket, &ep->rbr, epi);
if (!epi) {
nty_trace_epoll("rbtree is no exist\n");
pthread_mutex_unlock(&ep->mtx);
return -1;
}
ep->rbcnt --;
free(epi);
pthread_mutex_unlock(&ep->mtx);
} else if (op == EPOLL_CTL_MOD) {
//修改該節(jié)點(diǎn)
struct epitem tmp;
tmp.sockfd = sockid;
struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
//找到了就將該節(jié)點(diǎn)的事件進(jìn)行修改,這里還有一個(gè)細(xì)節(jié),修改之后會(huì)默認(rèn)加入EPOLLERR | EPOLLHUP這兩個(gè)事件監(jiān)聽,一個(gè)表示套接字發(fā)生錯(cuò)誤,一個(gè)表示套接字被掛斷。
if (epi) {
epi->event.events = event->events;
epi->event.events |= EPOLLERR | EPOLLHUP;
} else {
errno = -ENOENT;
return -1;
}
} else {
nty_trace_epoll("op is no exist\n");
assert(0);
}
return 0;
}
每根據(jù)一個(gè)不同的選擇就創(chuàng)建一個(gè)結(jié)構(gòu),并查找它是不是在紅黑樹中可能有點(diǎn)冗余,感覺直接在上面創(chuàng)建一個(gè)即可。
這段代碼看起來不難,其實(shí)細(xì)節(jié)有很多的,首先先來說一下大體的步驟:
- 根據(jù)傳入的epfd找到對(duì)應(yīng)的eventpoll結(jié)構(gòu)。
- 根據(jù)不同的選擇類型進(jìn)行if-else判斷
1.首先所有操作,都要先在棧區(qū)建立一個(gè)epitem的結(jié)構(gòu),然后將sock傳入其中。并使用紅黑樹查找函數(shù)傳入該結(jié)構(gòu)查找。
2.根據(jù)有沒有來進(jìn)行構(gòu)造刪除,或者修改。
以插入為例,沒有則在堆區(qū)構(gòu)建結(jié)構(gòu)體對(duì)象,然后向紅黑樹中插入,使用memcpy來加入事件。
其中的兩個(gè)細(xì)節(jié)包括:
都是先建立一個(gè)棧區(qū)對(duì)象,然后紅黑樹中進(jìn)行查找的,這里可以優(yōu)化一下。
在修改的時(shí)候,會(huì)加入監(jiān)聽事件:文件描述符出錯(cuò),套接字被掛斷。
(3)epoll_wait的源碼
這個(gè)函數(shù)就是用于等待事件就緒,然后將他插入就緒隊(duì)列中的,其中這里的epoll_event是一個(gè)輸出型參數(shù),它通常表示一個(gè)數(shù)組的首地址。這里可以再回顧一下它是怎么進(jìn)行傳參的:
int n=epoll_wait(epfd,revs,NUM,-1);
其中epfd顯然還是去找eventpoll的,revs是一個(gè)數(shù)組首元素地址(我們建立一個(gè)數(shù)組,傳入數(shù)組名其實(shí)就可以了),NUM是一個(gè)整數(shù),表示多少個(gè)套接字就緒了就可以返回了,-1表示的是只要沒有文件描述符就緒,就永久阻塞。
值得注意的是,這里我們沒有回調(diào)函數(shù)的實(shí)現(xiàn),也就是說暫時(shí)沒有套接字就緒了將它插入等待隊(duì)列中的操作,它的實(shí)現(xiàn)在后面講。
int epoll_wait(int epid, struct epoll_event *events, int maxevents, int timeout) {
//tcp相關(guān),可惡我寫完這篇文章一定看看這是個(gè)啥東西
nty_tcp_manager *tcp = nty_get_tcp_manager();
if (!tcp) return -1;
//nty_socket_map *epsocket = &tcp->smap[epid];
//找到eventpoll
struct _nty_socket *epsocket = tcp->fdtable->sockfds[epid];
if (epsocket == NULL) return -1;
if (epsocket->socktype == NTY_TCP_SOCK_UNUSED) {
errno = -EBADF;
return -1;
}
if (epsocket->socktype != NTY_TCP_SOCK_EPOLL) {
errno = -EINVAL;
return -1;
}
struct eventpoll *ep = (struct eventpoll*)epsocket->ep;
if (!ep || !events || maxevents <= 0) {
errno = -EINVAL;
return -1;
}
//線程相關(guān),先不研究
if (pthread_mutex_lock(&ep->cdmtx)) {
if (errno == EDEADLK) {
nty_trace_epoll("epoll lock blocked\n");
}
assert(0);
}
//如果rdnum為空,并且等待時(shí)間不為0的時(shí)候會(huì)等待一段時(shí)間
while (ep->rdnum == 0 && timeout != 0) {
ep->waiting = 1;
if (timeout > 0) {
struct timespec deadline;
clock_gettime(CLOCK_REALTIME, &deadline);
if (timeout >= 1000) {
int sec;
sec = timeout / 1000;
deadline.tv_sec += sec;
timeout -= sec * 1000;
}
deadline.tv_nsec += timeout * 1000000;
if (deadline.tv_nsec >= 1000000000) {
deadline.tv_sec++;
deadline.tv_nsec -= 1000000000;
}
int ret = pthread_cond_timedwait(&ep->cond, &ep->cdmtx, &deadline);
if (ret && ret != ETIMEDOUT) {
nty_trace_epoll("pthread_cond_timewait\n");
pthread_mutex_unlock(&ep->cdmtx);
return -1;
}
timeout = 0;
} else if (timeout < 0) {
int ret = pthread_cond_wait(&ep->cond, &ep->cdmtx);
if (ret) {
nty_trace_epoll("pthread_cond_wait\n");
pthread_mutex_unlock(&ep->cdmtx);
return -1;
}
}
ep->waiting = 0;
}
pthread_mutex_unlock(&ep->cdmtx);
pthread_spin_lock(&ep->lock);
int cnt = 0;
//哪個(gè)少將哪個(gè)作為事件的數(shù)量
int num = (ep->rdnum > maxevents ? maxevents : ep->rdnum);
int i = 0;
while (num != 0 && !LIST_EMPTY(&ep->rdlist)) { //EPOLLET
//每次從雙向鏈表的頭節(jié)點(diǎn)取得一個(gè)一個(gè)的節(jié)點(diǎn)
struct epitem *epi = LIST_FIRST(&ep->rdlist);
//把這個(gè)節(jié)點(diǎn)從雙向鏈表中刪除
LIST_REMOVE(epi, rdlink);
//標(biāo)記這個(gè)節(jié)點(diǎn)不在雙向鏈表中,但是在紅黑樹中
epi->rdy = 0;//只有當(dāng)該節(jié)點(diǎn)再次被放入雙向鏈表中的時(shí)候,才會(huì)置為1
//把標(biāo)記的信息拷貝出來,拷貝到提供的events參數(shù)中
memcpy(&events[i++], &epi->event, sizeof(struct epoll_event));
//--,++的操作
num --;
cnt ++;
ep->rdnum --;
}
pthread_spin_unlock(&ep->lock);
return cnt;
}
(4)epoll_event_callback()
1.客戶端connect()連入,服務(wù)器處于SYN_RCVD狀態(tài)時(shí)
2.三路握手完成,服務(wù)器處于ESTABLISHED狀態(tài)時(shí)
3.客戶端close()斷開連接,服務(wù)器處于FIN_WAIT_1和FIN_WAIT_2狀態(tài)時(shí)
4.客戶端send/write()數(shù)據(jù),服務(wù)器可讀時(shí)
5.服務(wù)器可以發(fā)送數(shù)據(jù)時(shí)
它的作用是向雙向鏈表中添加一個(gè)紅黑樹的節(jié)點(diǎn)。它的參數(shù)有一個(gè)eventpoll類型,注意這里沒有傳epid,有一個(gè)sockid,有一個(gè)event。
int epoll_event_callback(struct eventpoll *ep, int sockid, uint32_t event) {
struct epitem tmp;
tmp.sockfd = sockid;
//首先根據(jù)sockid找到紅黑樹中的節(jié)點(diǎn)
struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
if (!epi) {
nty_trace_epoll("rbtree not exist\n");
assert(0);
}
//根據(jù)epi->rdy判斷該節(jié)點(diǎn)是否在雙向鏈表里
if (epi->rdy) {
//如果在就將事件填入events中
epi->event.events |= event;
return 1;
}
//如果不在,要做的就是將這個(gè)節(jié)點(diǎn)添加到雙向鏈表中的表頭位置
nty_trace_epoll("epoll_event_callback --> %d\n", epi->sockfd);
pthread_spin_lock(&ep->lock);
epi->rdy = 1;
LIST_INSERT_HEAD(&ep->rdlist, epi, rdlink);
ep->rdnum ++;
pthread_spin_unlock(&ep->lock);
pthread_mutex_lock(&ep->cdmtx);
pthread_cond_signal(&ep->cond);
pthread_mutex_unlock(&ep->cdmtx);
return 0;
}
5.水平觸發(fā)和邊緣觸發(fā)
1.狀態(tài)變化
要講明白水平觸發(fā)和邊緣觸發(fā)就需要知道都有哪些狀態(tài)會(huì)觸發(fā),無非也就這四種:
- 可讀:socket上有數(shù)據(jù)
- 不可讀:socket上沒有數(shù)據(jù)了
- 可寫:socket上有空間可寫
- 不可寫:socket上無空間可寫
對(duì)于水平觸發(fā)模式,一個(gè)事件只要有,就會(huì)一直被觸發(fā)。對(duì)于邊緣觸發(fā)模式,一個(gè)事件只要從無到有就會(huì)被觸發(fā)。
2.LT模式
讀:只要接收緩沖區(qū)上有未讀完的數(shù)據(jù),就會(huì)一直被觸發(fā)。
寫:只要發(fā)送緩沖區(qū)上還有空間,就會(huì)一直被觸發(fā)。如果程序依賴于可寫事件觸發(fā)去發(fā)送數(shù)據(jù),要移除可寫事件,避免無意義的觸發(fā)。
在LT模式下,讀事件觸發(fā)后,可以按需收取想要的字節(jié)數(shù),不用把本次接收到的數(shù)據(jù)收取干凈。
3.ET模式
讀:只有接收緩沖區(qū)上的數(shù)據(jù)從無到有,就會(huì)被觸發(fā)一次。
寫:只有發(fā)送緩沖區(qū)上由不可寫到可寫,就會(huì)觸發(fā),(由滿到不滿)
在ET模式下,當(dāng)讀事件觸發(fā)后,需要將數(shù)據(jù)一次性讀干凈。
6.epoll中的鎖
通過閱讀上面的代碼,我們發(fā)現(xiàn)訪問紅黑樹和訪問雙向鏈表的時(shí)候會(huì)加鎖。
紅黑樹:互斥鎖,因?yàn)榧t黑樹是一個(gè)自平衡的二叉搜索樹,多線程訪問的時(shí)候可能改變樹的結(jié)構(gòu),因此加上互斥鎖。
雙向鏈表:自旋鎖,是一個(gè)更輕量級(jí)的鎖,因?yàn)殡p向鏈表的結(jié)構(gòu)是不會(huì)改變的,通過自旋等待的方式獲取鎖,避免了切換上下文的開銷。
7.epoll的致命缺點(diǎn)
(1)多線程擴(kuò)展性
場(chǎng)景一:同一個(gè) listen fd 在多個(gè) CPU 上調(diào)用 accept 系統(tǒng)調(diào)用
水平觸發(fā):
1.內(nèi)核收到了一個(gè)鏈接請(qǐng)求。同時(shí)喚醒了A和B兩個(gè)在epoll_wait上等待的線程。
2.線程A epoll_wait成功,而線程B epoll_wait失敗。
3.線程B其實(shí)不需要喚醒,造成驚群效應(yīng),消耗資源。
邊緣觸發(fā):
1.內(nèi)核收到了一個(gè)鏈接請(qǐng)求,由于是邊緣觸發(fā)所以只會(huì)喚醒一個(gè)線程,假設(shè)線程A被喚醒。
2.A正在accept的時(shí)候,突然又來了一個(gè)鏈接,此時(shí)由于由于監(jiān)聽套接字處于就緒狀態(tài),沒有產(chǎn)生新事件,所以就不會(huì)發(fā)起通知。
3.由于不會(huì)發(fā)起通知,所以必須由A再去處理該鏈接,這樣就造成了線程饑餓的問題。
(2)epoll 所注冊(cè)的 fd(file descriptor) 和實(shí)際內(nèi)核中控制的結(jié)構(gòu) file description 擁有不同的生命周期

epoll混淆了上圖中的進(jìn)程的文件描述符和系統(tǒng)中的文件描述符表。當(dāng)進(jìn)行EPOLLADD后,epoll其實(shí)監(jiān)聽的是系統(tǒng)級(jí)的文件描述符,所以當(dāng)close(fd)的時(shí)候,如果對(duì)應(yīng)的description只有它一個(gè)descriptor的時(shí)候,正常關(guān)閉。
但如果它有多個(gè)descriptor與之對(duì)應(yīng)的話,就會(huì)發(fā)生即使將該文件描述符關(guān)閉了,但是還是可以接收到事件的情況。更糟糕的是,一旦你 close() 了這個(gè) fd,再也沒有機(jī)會(huì)把這個(gè)死掉的 fd 從 epoll 上摘除了。所以在刪除之前一定要進(jìn)行EPOLLDELETE。

浙公網(wǎng)安備 33010602011771號(hào)