前言
衆所周知,libevent支持多種I/O多路複用,如select、poll、epoll、kqueue等。那麼其中是如何實現的呢?
主要就是結構體eventop
,它內部成員有幾個函數指針,統一了每種I/O多路複用的接口,也就是說,要想libevent支持某種I/O多路複用,就必須實現這幾種接口。結構體eventop
位於event-internal.h
中。
eventop
struct eventop
{
const char *name;
void *(*init)(struct event_base *); //初始化
int (*add)(void *, struct event *); //添加事件
int (*del)(void *, struct event *); //刪除事件
int (*dispatch)(struct event_base *, void *, struct timeval *); //分發事件
void (*dealloc)(struct event_base *, void *); //釋放資源
int need_reinit;
};
這5個函數指針分別指向某個具體的多路I/O複用機制的初始化、添加事件、刪除事件、分發事件、釋放資源這5個操作上。這樣,libevent就可以支持該多路I/O複用機制了。
但是其實我們使用的時候並沒有選擇用哪一種,說明是有默認選項的。
在event.c
文件中,有一個靜態全局的eventops數組,並且按照優先級選擇用哪一種多路I/O複用機制。定義如下:
static const struct eventop *eventops[] = {
#ifdef HAVE_EVENT_PORTS
&evportops,
#endif
#ifdef HAVE_WORKING_KQUEUE
&kqops,
#endif
#ifdef HAVE_EPOLL
&epollops,
#endif
#ifdef HAVE_DEVPOLL
&devpollops,
#endif
#ifdef HAVE_POLL
&pollops,
#endif
#ifdef HAVE_SELECT
&selectops,
#endif
#ifdef WIN32
&win32ops,
#endif
NULL
};
這裏主要運用了條件編譯,在config.h
中define了可以支持的多路I/O複用,由於只是簡單的define,並且代碼比較多,就不在這裏列舉了,可以自己看一看。
epollops
接下來,我們以epoll爲例來看一看epollops
的真面目。
在epoll.c
文件中:
const struct eventop epollops = {
"epoll",
epoll_init,
epoll_add,
epoll_del,
epoll_dispatch,
epoll_dealloc,
1 /* need reinit */
};
那麼只用實現epoll_init
、epoll_add
、epoll_del
、epoll_dispatch
、epoll_dealloc
這幾個函數就行了。
首先先看一下這幾個函數要用到的結構體
//對應讀和寫事件
struct evepoll {
struct event *evread;
struct event *evwrite;
};
struct epollop {
//每個fd可對應讀/寫事件
struct evepoll *fds;
//fd的數量
int nfds;
//epoll事件
struct epoll_event *events;
//事件的數量
int nevents;
//epoll專用文件描述符
int epfd;
};
epoll_init
接下來是epoll_init
:
static void *
epoll_init(struct event_base *base)
{
int epfd;
struct epollop *epollop;
/* Disable epollueue when this environment variable is set */
if (evutil_getenv("EVENT_NOEPOLL"))
return (NULL);
/* Initalize the kernel queue */
//創建epoll句柄
if ((epfd = epoll_create(32000)) == -1) {
if (errno != ENOSYS)
event_warn("epoll_create");
return (NULL);
}
//這是爲了防止在使用多進程時,子進程繼承父進程打開的文件描述符及權限。所以設置FD_CLOEXEC標誌。
FD_CLOSEONEXEC(epfd);
//給epollop申請空間
if (!(epollop = calloc(1, sizeof(struct epollop))))
return (NULL);
epollop->epfd = epfd;
/* Initalize fields */
epollop->events = malloc(INITIAL_NEVENTS * sizeof(struct epoll_event));
//當申請空間失敗時,把之前申請的也釋放了,然後return
if (epollop->events == NULL) {
free(epollop);
return (NULL);
}
epollop->nevents = INITIAL_NEVENTS;
epollop->fds = calloc(INITIAL_NFILES, sizeof(struct evepoll));
//同理
if (epollop->fds == NULL) {
free(epollop->events);
free(epollop);
return (NULL);
}
epollop->nfds = INITIAL_NFILES;
//由於libevent爲了將signal也集成到事件主循環中,所以使用了套結字對(socket pair)。這個函數就用於創建socket pair和初始化evsignal_info
evsignal_init(base);
return (epollop);
}
epoll_add
epoll_add:
static int
epoll_add(void *arg, struct event *ev)
{
struct epollop *epollop = arg;
struct epoll_event epev = {0, {0}};
struct evepoll *evep;
int fd, op, events;
//如果是signal事件,直接調用evsignal_add來添加就行了
if (ev->ev_events & EV_SIGNAL)
return (evsignal_add(ev));
fd = ev->ev_fd;
//當前的文件描述符大於nfds時,需要重新擴展。(這點是利用了linux系統優先分配空閒的最小值fd)
if (fd >= epollop->nfds) {
/* Extent the file descriptor array as necessary */
if (epoll_recalc(ev->ev_base, epollop, fd) == -1)
return (-1);
}
//evep是當前fd(需要add的)對應的struct evepoll(裏面是讀/寫事件)
evep = &epollop->fds[fd];
//對應的默認操作是添加操作
op = EPOLL_CTL_ADD;
events = 0;
//如果已經指向了一個讀事件,證明該fd已經在epoll監聽中了,所以應該將操作改爲EPOLL_CTL_MOD,但是爲了防止以前的監聽讀事件標誌被覆蓋,所以重新加上。
if (evep->evread != NULL) {
//監聽讀事件
events |= EPOLLIN;
op = EPOLL_CTL_MOD;
}
//同理
if (evep->evwrite != NULL) {
events |= EPOLLOUT;
op = EPOLL_CTL_MOD;
}
//如果設置了EV_READ標誌,說明是讀事件
if (ev->ev_events & EV_READ)
events |= EPOLLIN;
//如果設置了EV_WRITE標誌,說明是寫事件
if (ev->ev_events & EV_WRITE)
events |= EPOLLOUT;
//設置struct epoll_event
epev.data.fd = fd;
epev.events = events;
//修改/增加fd到監聽的epollop->epfd中去
if (epoll_ctl(epollop->epfd, op, ev->ev_fd, &epev) == -1)
return (-1);
/* Update events responsible */
//如果是讀事件,那麼讓evread指向該event
if (ev->ev_events & EV_READ)
evep->evread = ev;
//如果是寫事件,那麼讓evwrite指向該event
if (ev->ev_events & EV_WRITE)
evep->evwrite = ev;
return (0);
}
epoll_dispatch
接下來便是最複雜的epoll_dispatch
了:
static int
epoll_dispatch(struct event_base *base, void *arg, struct timeval *tv)
{
struct epollop *epollop = arg;
struct epoll_event *events = epollop->events;
struct evepoll *evep;
int i, res, timeout = -1;
//如果設置了超時等待時間,那麼就將這時間具體多少ms算出來
if (tv != NULL)
timeout = tv->tv_sec * 1000 + (tv->tv_usec + 999) / 1000;
//如果該等待時間大於了最長的等待時間,那就直接設置爲最長等待時間(該最長等待時間是epoll.c裏面設定的)
//#define MAX_EPOLL_TIMEOUT_MSEC (35*60*1000)
//即最久等35分鐘.....不過可以自己修改
if (timeout > MAX_EPOLL_TIMEOUT_MSEC) {
/* Linux kernels can wait forever if the timeout is too big;
* see comment on MAX_EPOLL_TIMEOUT_MSEC. */
//linux內核是可以無限等的,但是關鍵還是看MAX_EPOLL_TIMEOUT_MSEC
timeout = MAX_EPOLL_TIMEOUT_MSEC;
}
//epoll_wait函數我相信你應該懂的,返回值是觸發了的事件總數
res = epoll_wait(epollop->epfd, events, epollop->nevents, timeout);
//返回-1代表出錯了
if (res == -1) {
//如果不是被信號中斷的,那麼直接報錯了
if (errno != EINTR) {
event_warn("epoll_wait");
return (-1);
}
//處理signal事件
evsignal_process(base);
return (0);
} else if
//查看是否有信號的標誌,如若發生,則處理signal事件
(base->sig.evsignal_caught) {
evsignal_process(base);
}
event_debug(("%s: epoll_wait reports %d", __func__, res));
//依次處理被觸發的事件
for (i = 0; i < res; i++) {
//what:記錄什麼類型的事件
int what = events[i].events;
struct event *evread = NULL, *evwrite = NULL;
int fd = events[i].data.fd;
if (fd < 0 || fd >= epollop->nfds)
continue;
evep = &epollop->fds[fd];
//如果是被掛斷或者出錯導致被觸發
if (what & (EPOLLHUP|EPOLLERR)) {
evread = evep->evread;
evwrite = evep->evwrite;
} else {
if (what & EPOLLIN) {
evread = evep->evread;
}
if (what & EPOLLOUT) {
evwrite = evep->evwrite;
}
}
//如果讀/寫事件都沒有,直接結束本次循環
if (!(evread||evwrite))
continue;
//手動激活讀/寫事件
if (evread != NULL)
event_active(evread, EV_READ, 1);
if (evwrite != NULL)
event_active(evwrite, EV_WRITE, 1);
}
//當nevents不夠用的時候,重新分配
if (res == epollop->nevents && epollop->nevents < MAX_NEVENTS) {
/* We used all of the event space this time. We should
be ready for more events next time. */
int new_nevents = epollop->nevents * 2;
struct epoll_event *new_events;
new_events = realloc(epollop->events,
new_nevents * sizeof(struct epoll_event));
if (new_events) {
epollop->events = new_events;
epollop->nevents = new_nevents;
}
}
return (0);
}
注意epoll_dispatch
函數中並沒有調用處理事件的業務,而是在event_base_loop
中由event_process_actice
調用。相當於它的工作是隻負責把事件添加到激活隊列中,然後由event_process_actice
處理。
接下來只剩epoll_del
和epoll_dealloc
還有epoll_recalc
了,epoll_del
的邏輯和epoll_add
大致相似,就不在這裏列出了。而epoll_recalc
無非就是重新分配內存,也沒什麼需要注意的,最後就來個epoll_dealloc
收尾吧。
epoll_dealloc
epoll_dealloc:
static void
epoll_dealloc(struct event_base *base, void *arg)
{
struct epollop *epollop = arg;
evsignal_dealloc(base);
if (epollop->fds)
free(epollop->fds);
if (epollop->events)
free(epollop->events);
if (epollop->epfd >= 0)
close(epollop->epfd);
//釋放了空間之後,別忘了將指針指向的地方賦爲null,不然指向的是已經釋放了的空間,造成野指針
memset(epollop, 0, sizeof(struct epollop));
free(epollop);
}
這裏我們來關注一下void *arg
,前面已經出現過多次,可能你會對其產生疑惑,而在event_init
中的函數類型也是void *
,而返回的是struct epollop *
,在event_add
或者event_base_loop
中,調用的都是evsel->add(evbase, ev)
或者evsel->dispatch(base, evbase, tv_p)
這樣的,這說明每一個多路I/O複用都對應有它自己的struct xxxop *
。之所以返回值是void *
,是爲了將不同的struct xxxop *
轉換成統一的指針類型。
小結
在本節中,我們終於知道了神祕的libevent如何同時支持了那麼多種多路I/O複用機制,再配合上前面講的主循環以及event還有event_base以及信號、定時事件。你應該可以跟着事件的主循環,在腦海中浮現出一個一個的事件是如何被添加到事件鏈表,以及被激活,等待調度,調度之後被銷燬等等的場景了。
接下來,我們將研究libevent庫中緩衝區的部分。