redis原始碼解析----epoll的使用

TuxedoLinux發表於2018-06-03

redis原始碼解析----epoll的使用

平時做專案,涉及到網路層的都是epoll,前幾年發現redis的epoll實現起來非常的精簡,好用。因為提供的介面簡單,愛並實現的很高效。於是,我就提取出來,直接使用。

今天又開啟該檔案詳細的看看他的實現細節。

首先簡單介紹epoll,它是linux核心下的一個高效的處理大批量的檔案操作符的一個實現。不僅限於socket fd。

他在超時時間內會喚醒有事件的操作符。其中有兩種模式 1、水平觸發(Level Triggered)2、邊緣觸發(Edge Triggered)

簡單概括這兩種,水平觸發是是預設的工作方式,並且同時支援block和no-block socket.在這種做法中,核心告訴你一個檔案描述符是否就緒了,然後你可以對這個就緒的fd進行IO操作。如果你不作任何操作,核心還是會繼續通知你的,所以,這種模式程式設計出錯誤可能性要小一點。傳統的select/poll都是這種模型的代表。

而邊緣模式是 有讀寫等事件,只會通知你一次,直到下一次事件再一次觸發。所以,使用該模式的時候,一般情況下比較複雜,要對操作符讀取資料到完全為空。才能保證資料不會丟失

epoll 提供了三個介面,

首先通過epoll_create(int maxfds)來建立一個epoll的控制程式碼

之後在你的網路主迴圈裡面,每一幀的呼叫epoll_wait(int epfd, epoll_event *events, int max events, int timeout)來查詢所有的網路介面,看哪一個可以讀,哪一個可以寫了。

epoll_ctl用來新增/修改/刪除需要偵聽的檔案描述符及其事件。

好了,當我們瞭解瞭如何使用這三個函式後,redis ae 做得就是如何友好的使用這三個函式了,並給我們提供方面的介面,讓我們只關注 資料包的處理。

首先了解一下ae的結構體eventloop

/* State of an event based program */
typedef struct aeEventLoop {
int maxfd; /* highest file descriptor currently registered */
int setsize; /* max number of file descriptors tracked */
long long timeEventNextId;
time_t lastTime; /* Used to detect system clock skew */
aeFileEvent *events; /* Registered events */
aeFiredEvent *fired; /* Fired events */
aeTimeEvent *timeEventHead;
int stop;
void *apidata; /* This is used for polling API specific data */
aeBeforeSleepProc *beforesleep;
} aeEventLoop;

我們首先只關注epoll相關,maxfd,表示能夠註冊的最大操作符數,也就是aeFileEvent *events的最大陣列,

int setsize; /* max number of file descriptors tracked */

同上,能夠分配的最大陣列的數量。events 成員儲存了我們要註冊到epoll裡的操作符,以及對該操作符事件到來的時候進行的操作的相關函式,具體看一下起結構體我們就明白了。

/* File event structure */
typedef struct aeFileEvent {
int mask; /* one of AE_(READABLE|WRITABLE) */
aeFileProc *rfileProc;
aeFileProc *wfileProc;
void *clientData;
} aeFileEvent;
mask表示 我們對改操作符所要關心的時間,比如可讀,可寫時間的掩碼。rfileProc為當我們有可讀事件的時候,進行對其回撥,wfileProc表示當有可寫的事件的時候,進行回撥。clientData為函式引數。
 一般都是以fd作為aeFileEvent的陣列下標,當有fd有事件時候,我們可以直接用fd定位到相應的位置,直接呼叫相應的函式。
redis 通過提供int aeCreateFileEvent(aeEventLoop *eventLoopint fdint mask,
        aeFileProc *procvoid *clientData) 該方法,將fd註冊進入。  

eventloop中的fired 用來臨時儲存epoll_wait中要有事件觸發的操作符。
相應的結構體為
/* A fired event */
typedef struct aeFiredEvent {
int fd;
int mask;
} aeFiredEvent;
有了這個結構體,我們就可以根據fd 找到相應的struct aeFileEvent相對應的的陣列元素了。
那麼最終是什麼時候被填充呢,下面我們就要看epoll_wait函式的呼叫了。
retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);

首先關注一下,第二個引數,struct epoll_event * events
這個結構體是epoll的引數,它是什麼樣子呢?
//儲存觸發事件的某個檔案描述符相關的資料(與具體使用方式有關)
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 */
};

而redis將該結構體放到了,
typedef struct aeApiState {
int epfd;
struct epoll_event *events;
} aeApiState;
內,epfd是epoll_create的返回控制程式碼,events用來儲存epoll_wait的的第二個引數結果。能夠儲存的數目也就是我們之前提到的setSize大小了。

好,當我們呼叫epoll_wait後,就會有相應的epoll_event填充到state內,那麼,我們就要對這些fd進行操作了。

請看程式碼。
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
aeApiState *state = eventLoop->apidata;
int retval, numevents = 0;
retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);
if (retval > 0) {
int j;
numevents = retval;
for (j = 0; j < numevents; j++) {
int mask = 0;
struct epoll_event *e = state->events+j;
if (e->events & EPOLLIN) mask |= AE_READABLE;
if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
if (e->events & EPOLLERR) mask |= AE_WRITABLE;
if (e->events & EPOLLHUP) mask |= AE_WRITABLE;
eventLoop->fired[j].fd = e->data.fd;
eventLoop->fired[j].mask = mask;
}
}
return numevents;
}

我們可以清晰的認識到,epoll_wait返回值是本次觸發的時間數量,然後將其便利,相應的事件放入到fired中,

緊接著,對fired進行遍歷操作

numevents = aeApiPoll(eventLoop, tvp);
for (j = 0; j < numevents; j++) {
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
int mask = eventLoop->fired[j].mask;
int fd = eventLoop->fired[j].fd;
int rfired = 0;
/* note the fe->mask & mask & ... code: maybe an already processed
* event removed an element that fired and we still didn't
* processed, so we check if the event is still valid. */
if (fe->mask & mask & AE_READABLE) {
rfired = 1;
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
}
if (fe->mask & mask & AE_WRITABLE) {
if (!rfired || fe->wfileProc != fe->rfileProc)
fe->wfileProc(eventLoop,fd,fe->clientData,mask);
}
processed++;
}
}
這就完成了一次對操作符的操作實現
那麼為什麼中間還弄了一個fired的臨時儲存fd的成員呢,多了一次迴圈操作,我想應該是為了實現kqueue,select,epoll的提供共了一個通用的結構。

是不是很簡單?

因此,我在專案中是直接拿來使用的。非常好用方便。
http://blog.chinaunix.net/uid-24517549-id-4051156.html 這篇文章對epoll的使用有很詳細的講解。

更多文章,歡迎訪問
http://blog.csdn.net/wallwind


相關文章