?追根究底?Libevent內(nèi)部實(shí)現(xiàn)原理初探
Libevent確實(shí)方便了開發(fā)人員食呻,對(duì)于定時(shí)器、信號(hào)處理、關(guān)心的文件或者套接字搁进,只需要掛載到event_base上面浪感,設(shè)置好對(duì)應(yīng)的回調(diào)函數(shù)和參數(shù)就可以了,當(dāng)對(duì)應(yīng)的事件發(fā)生時(shí)饼问,Libevent會(huì)自動(dòng)調(diào)度相應(yīng)的回調(diào)函數(shù)進(jìn)行處理。
本文就按照之前在sshinner(https://github.com/taozhijiang/sshinner)中使用Libevent的過程揭斧,以這些接口函數(shù)作為突破點(diǎn)莱革,沿著代碼走了一朝,嘗試探究一下Libevent的內(nèi)部工作流程是怎樣的讹开。由于本人能力有限盅视,有些東西可能不夠詳盡或者準(zhǔn)確,還望不吝指出旦万。
一闹击、創(chuàng)建event_base
struct event_base * main_base = event_base_new(void);
主要是根據(jù)參數(shù),創(chuàng)建event_base結(jié)構(gòu)體成艘,然后初始化一些數(shù)據(jù)赏半,如果對(duì)默認(rèn)的參數(shù)不滿意需要個(gè)性化配置的話,可以先創(chuàng)建event_config淆两,然后調(diào)用event_base_new_with_config來創(chuàng)建断箫。其中在eventops這個(gè)變量中,按照優(yōu)先級(jí)順序排序羅列了常見的IO復(fù)用模型秋冰,比如kqueue仲义、epoll、poll剑勾、select等埃撵,由于Libevent是跨平臺(tái)的,這些IO復(fù)用在有些平臺(tái)可能是不可用的虽另,同時(shí)你還可以在event_config中選擇過濾某些不想要的模型暂刘。
當(dāng)選定了某個(gè)IO復(fù)用模型之后,其操作結(jié)構(gòu)eventop就被添加到base->evsel中洲赵,然后調(diào)用其特定的init初始化函數(shù)鸳惯。這些操作跟文件系統(tǒng)file_operations結(jié)構(gòu)極為的類似。
那我們接著跟下去叠萍,看看大名鼎鼎的epoll類提供了哪些操作吧:
const struct eventop epollops = {
"epoll",
epoll_init,
epoll_nochangelist_add,
epoll_nochangelist_del,
epoll_dispatch,
epoll_dealloc,
1, /* need reinit */
EV_FEATURE_ET|EV_FEATURE_O1|EV_FEATURE_EARLY_CLOSE,
0
};
在初始化函數(shù)epoll_init當(dāng)中芝发,基本就類似epoll使用時(shí)候標(biāo)準(zhǔn)化的準(zhǔn)備工作:首先調(diào)用epoll_create創(chuàng)建epfd,然后預(yù)先創(chuàng)建INITIAL_NEVENT(32)個(gè)空間用于存放epoll_event苛谷,如果使用了timerfd辅鲸,則再調(diào)用timerfd_create創(chuàng)建對(duì)應(yīng)的timerfd。最后這些fd以及epoll_event都存放在struct epollop當(dāng)中腹殿,然后作為epoll_init函數(shù)的返回保存在base->evbase上独悴。
struct epollop {
struct epoll_event *events; ? ?//數(shù)組
int nevents;
int epfd;
#ifdef USING_TIMERFD
int timerfd;
#endif
};
在創(chuàng)建event_base的最后例书,還調(diào)用了event_base_priority_init進(jìn)行了一個(gè)初始化操作,如果有多個(gè)優(yōu)先級(jí)刻炒,就有對(duì)應(yīng)的多個(gè)等待隊(duì)列掛靠在base->activequeues上面决采,而base->nactivequeues記錄了優(yōu)先級(jí)的數(shù)目。
二坟奥、創(chuàng)建listen套接字树瞭,并建立connect事件偵聽
2.1 基本過程
listener = evconnlistener_new_bind(srvopt.main_base, accept_conn_cb, NULL,
LEV_OPT_CLOSE_ON_FREE|LEV_OPT_REUSEABLE, -1/*backlog*/,
(struct sockaddr*)&sin, sizeof(sin));
這個(gè)算是個(gè)簡(jiǎn)化版的函數(shù),你可以自己先手動(dòng)建立和綁定socket爱谁,然后再調(diào)用evconnlistener_new建立connect事件偵聽晒喷。這個(gè)函數(shù)給socket設(shè)置了一個(gè)高大上的符號(hào)SO_KEEPALIVE(SO_KEEPALIVE 保持連接檢測(cè)對(duì)方主機(jī)是否崩潰,避免服務(wù)器永遠(yuǎn)阻塞于TCP連接的輸入)访敌,SOCKET開發(fā)還是有很多參數(shù)的凉敲,比如之前sshinner網(wǎng)絡(luò)一直出問題,消息不能及時(shí)的被發(fā)送接收寺旺,最后跟蹤shadowsockets-libev發(fā)現(xiàn)爷抓,是要給socket添加TCP_NODELAY參數(shù),問題才得以解決迅涮。
在evconnlistener_new函數(shù)中废赞,首先調(diào)用listen,然后分配evconnlistener_event這個(gè)數(shù)據(jù)結(jié)構(gòu)叮姑,base作為struct evconnlistener類型傳遞給用戶空間唉地,而listener主要作為內(nèi)部隱藏的數(shù)據(jù)結(jié)構(gòu),為通用的struct event數(shù)據(jù)類型传透。
struct evconnlistener_event {
struct evconnlistener base;
struct event listener;
};
struct evconnlistener {
const struct evconnlistener_ops *ops;
void *lock;
evconnlistener_cb cb;
evconnlistener_errorcb errorcb;
void *user_data;
unsigned flags;
short refcnt;
int accept4_flags;
unsigned enabled : 1;
};
struct event {
struct event_callback ev_evcallback;
/* for managing timeouts */
union {
TAILQ_ENTRY(event) ev_next_with_common_timeout;
int min_heap_idx;
} ev_timeout_pos;
evutil_socket_t ev_fd;
struct event_base *ev_base;
union {
/* used for io events */
struct {
LIST_ENTRY (event) ev_io_next;
struct timeval ev_timeout;
} ev_io;
/* used by signal events */
struct {
LIST_ENTRY (event) ev_signal_next;
short ev_ncalls;
/* Allows deletes in callback */
short *ev_pncalls;
} ev_signal;
} ev_;
short ev_events;
short ev_res; ? ? ? ?/* result passed to event callback */
struct timeval ev_timeout;
};
通過上面的數(shù)據(jù)結(jié)構(gòu)可以清晰的發(fā)現(xiàn)耘沼,調(diào)用evconnlistener_new_bind函數(shù)作為參數(shù)提供的回調(diào)函數(shù)和參數(shù),都被賦值給了evconnlistener_event->base.cb和base.user_data上面朱盐。接下來調(diào)用了兩個(gè)比較重要的函數(shù):
event_assign(&lev->listener, base, fd, EV_READ|EV_PERSIST,
listener_read_cb, lev);
evconnlistener_enable(&lev->base);
evconnlistener_enable(&lev->base)通過追根述源是調(diào)用了event_listener_enable群嗤,最后調(diào)用了event_add(&lev_e->listener, NULL)。而event_assign和event_add都是比較重要的函數(shù)兵琳,event_assign類似于event_new的作用狂秘,只不過參數(shù)是一個(gè)已經(jīng)初始化了的struct event,而event_add則是把event由initialized狀態(tài)變成pending狀態(tài)躯肌,以便開始接收事件者春,其實(shí)后面可以發(fā)現(xiàn),bufferevent_enable等接口清女,底層也是調(diào)用的event_add實(shí)現(xiàn)的钱烟。
接下來把上面這兩個(gè)函數(shù)慢慢品讀。
2.2 event_assign調(diào)用
event_assign(&lev->listener, base, fd, EV_READ|EV_PERSIST,
listener_read_cb, lev);
初看上面比較奇怪,在evconnlistener_new這個(gè)函數(shù)的上半部分已經(jīng)設(shè)置了一個(gè)base.cb和base.user_data了拴袭,怎么下面又調(diào)用一個(gè)event_assign來設(shè)置一個(gè)listener_read_cb回調(diào)呢读第?其實(shí)上面是用戶提供的callback和args,但是這并沒有直接跟某個(gè)事件相關(guān)聯(lián)拥刻,而下面的event_assign卻是設(shè)置了&lev->listener(標(biāo)準(zhǔn)的struct event類型)為固定的listener_read_cb回調(diào)函數(shù)怜瞒,當(dāng)發(fā)生了EV_READ就會(huì)被自動(dòng)調(diào)用。然后在listener_read_cb中泰佳,我們發(fā)現(xiàn):
cb = lev->cb; ? ?//
user_data = lev->user_data; ? ?//
cb(lev, new_fd, (struct sockaddr*)&ss, (int)socklen, ?user_data);
errorcb(lev, user_data);
所以說盼砍,其實(shí)Libevent內(nèi)部根本沒有什么諸如LISTEN的事件,還是用的標(biāo)準(zhǔn)EV_READ(因?yàn)樽钭畹讓拥膃poll異步只能監(jiān)聽read/write/except事件)逝她,只是做了個(gè)封裝,當(dāng)連接之后激活EV_READ(為什么呢睬捶?為什么呢黔宛?)的回調(diào)函數(shù),而出錯(cuò)了就調(diào)用err_callback函數(shù)擒贸,所以accept_conn_cb實(shí)際是被手動(dòng)調(diào)用的臀晃。
2.3 event_add調(diào)用
evconnlistener_enable的調(diào)用被翻譯到event_add函數(shù),其實(shí)不光光是這里介劫,后面最常用的bufferevent_enable這類函數(shù)徽惋,其實(shí)也是翻譯到底層的event_add函數(shù)(event_add_nolock_)上面。
struct evconnlistener_event *lev_e =
EVUTIL_UPCAST(lev, struct evconnlistener_event, base); ? ?//這個(gè)宏比較好看
return event_add(&lev_e->listener, NULL);
#define EVUTIL_UPCAST(ptr, type, field) ? ? ? ? ? ? ? ?\
((type *)(((char*)(ptr)) - evutil_offsetof(type, field)))
event_add有兩個(gè)參數(shù)座韵,后面一個(gè)參數(shù)是struct timeval的超時(shí)參數(shù)险绘,如果是NULL就表示無限期等待,這里先就不考慮這種情況誉碴。其中最主要做的事情就是調(diào)用evmap_io_add_/evmap_signal_add_函數(shù)將事件加入到base當(dāng)中:
// 對(duì)照上面宦棺,返回-1 error, 0 沒有實(shí)際操作, 1 真實(shí)添加了
res = evmap_io_add_(base, ev->ev_fd, ev);
res = evmap_signal_add_(base, (int)ev->ev_fd, ev);
// 上面函數(shù)的核心操作
GET_IO_SLOT_AND_CTOR(ctx, io, fd, evmap_io, evmap_io_init,
evsel->fdinfo_len);
evsel->add(base, ev->ev_fd,
old, (ev->ev_events & EV_ET) | res, extra);
首先,由于Libevent的設(shè)計(jì)是跨平臺(tái)的黔帕,而Windows和Linux對(duì)socket和fd的表達(dá)和處理方式不同代咸,GET_IO_SLOT_AND_CTOR的行為也有差異:Windows使用的是hashtable維護(hù)著struct event_map_entry結(jié)構(gòu),而Linux平臺(tái)就直接是用的指針數(shù)組(數(shù)組成黄,元素類型是指針)呐芥,用fd作為偏移來索引,指針指向的結(jié)構(gòu)按需分配奋岁,十分的簡(jiǎn)潔高效思瘟。
/* Used to map signal numbers to a list of events. ?If EVMAP_USE_HT is not
defined, this structure is also used as event_io_map, which maps fds to a
list of events.
*/
struct event_signal_map {
/* An array of evmap_io * or of evmap_signal *; empty entries are
* set to NULL. */
void **entries;
/* The number of entries available in entries */
int nentries;
};
/** Mapping from file descriptors to enabled (added) events */
struct event_io_map io;
/** Mapping from signal numbers to enabled (added) events. */
struct event_signal_map sigmap;
在每一個(gè)event_base結(jié)構(gòu)體中,都定義了struct event_signal_map類型的兩個(gè)成員io和sigmap(Linux平臺(tái))厦取,用于信號(hào)量和FD與events之間的事件映射潮太。然后看GET_IO_SLOT_AND_CTOR(GET_SIGNAL_SLOT_AND_CTOR)這個(gè)宏,查詢或者創(chuàng)建fd對(duì)應(yīng)的struct evmap_io對(duì)象ctx,將當(dāng)前的事件和之前的事件進(jìn)行合并铡买,并調(diào)用evsel->add進(jìn)行更新(最終反應(yīng)到底層epoll上面就是epoll_ctl命令進(jìn)行操作了)更鲁,并把當(dāng)前的event結(jié)構(gòu)體插入到前面找到的ctx->events鏈表當(dāng)中。
三奇钞、建立主事件循環(huán)
無論是主線程澡为,還是對(duì)于每個(gè)線程池用自己的event_base,最終都會(huì)調(diào)用這個(gè)函數(shù)作為主循環(huán)進(jìn)行事件處理景埃。
event_base_loop(main_base, 0);
先不考慮那些FLAG(控制何時(shí)推出啊啥的)媒至,在event_base_loop主要做的事情是:
/* F1 */ event_queue_make_later_events_active(base);
/* F2 */ res = evsel->dispatch(base, tv_p);
/* F3 */ timeout_process(base);
F1的函數(shù)主要是在Libevent中引用了Deferred Callback機(jī)制,操作上就是從event_base的active_later_queue隊(duì)列中將事件取出來谷徙,然后添加到activequeues[evcb->evcb_pri]對(duì)應(yīng)優(yōu)先級(jí)隊(duì)列上面拒啰。
F2對(duì)于epoll類型,就是調(diào)用epoll_dispatch函數(shù):首先調(diào)用epoll_apply_changes(base);對(duì)event_base->changelist上面掛靠的所有對(duì)fd的事件修改都執(zhí)行底層修改使之生效完慧;然后用res = epoll_wait(epollop->epfd, events, epollop->nevents, timeout);獲取被激活的事件谋旦;對(duì)獲取到的每個(gè)fd的事件,提取被激活的事件類型屈尼,然后調(diào)用evmap_io_active_(base, events[i].data.fd, ev | EV_ET);函數(shù)處理册着。
evmap_io_active_(struct event_base *base, evutil_socket_t fd, short events)
{
struct event_io_map *io = &base->io;
struct evmap_io *ctx;
struct event *ev;
GET_IO_SLOT(ctx, io, fd, evmap_io);
LIST_FOREACH(ev, &ctx->events, ev_io_next) {
if (ev->ev_events & events)
event_active_nolock_(ev, ev->ev_events & events, 1);
}
}
執(zhí)行的結(jié)果就是,這個(gè)fd對(duì)應(yīng)的evmap_io上的所有事件脾歧,以及Deferred Callback事件甲捏,都被收集添加到event_base->activequeues[evcb->evcb_pri]隊(duì)列中去。
于是乎鞭执,最后的好戲就是:
if (N_ACTIVE_CALLBACKS(base)) {
int n = event_process_active(base);
在event_process_active中調(diào)用了event_process_active_single_queue司顿。當(dāng)然作者考慮的細(xì)節(jié)還是比較細(xì)膩的:如果當(dāng)前被調(diào)度的活動(dòng)事件過多,就考慮在timer和maxcb兩個(gè)維度上限制本輪的事件處理量蚕冬,而在event_process_active_single_queue中免猾,會(huì)不斷從事件鏈表中取出事件處理(包括執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù))。從實(shí)現(xiàn)方式上看來囤热,按照優(yōu)先級(jí)從高到低的順序猎提,每一輪只處理一個(gè)最高優(yōu)先級(jí)非空事件隊(duì)列中的事件,然后就返回了旁蔼。這樣看來锨苏,如果高優(yōu)先級(jí)的事件太多太活躍,那么低優(yōu)先級(jí)的事件還是會(huì)有被餓死的風(fēng)險(xiǎn)棺聊。
四伞租、基于bufferevent的普通socket讀寫事件
bufferevent使得網(wǎng)絡(luò)的開發(fā)變的很方便,無論是從事件還是底層的evbuffer都提供了一套豐富靈活的接口限佩。但是需要注意的是bufferevent目前只能用于TCP連接的類型葵诈,對(duì)于UDP只能手動(dòng)建立struct event事件裸弦,然后設(shè)置事件和回調(diào)函數(shù)了,而且在回調(diào)函數(shù)中作喘,一般也只能調(diào)用sendto/recvfrom等操作接口理疙。
struct event_base *base = evconnlistener_get_base(listener);
struct bufferevent *bev =
bufferevent_socket_new(base, fd, 0 /*BEV_OPT_CLOSE_ON_FREE*/);
bufferevent_setcb(bev, bufferread_cb, NULL, bufferevent_cb, NULL);
bufferevent_enable(bev, EV_READ|EV_WRITE);
上面算是在網(wǎng)絡(luò)開發(fā)中用的最頻繁的了,比如在listener的connection回調(diào)函數(shù)中泞坦,接收到一個(gè)新的套接字fd窖贤,那么就對(duì)這個(gè)套接字設(shè)置bufferevent事件,設(shè)置對(duì)應(yīng)的回調(diào)函數(shù)贰锁。
在bufferevent_socket_new函數(shù)中:
struct bufferevent_private *bufev_p;
struct bufferevent *bufev;
bufferevent_init_common_(bufev_p, base, &bufferevent_ops_socket, options);
bufev_p->bev;
event_assign(&bufev->ev_read, bufev->ev_base, fd,
EV_READ|EV_PERSIST|EV_FINALIZE, bufferevent_readcb, bufev);
event_assign(&bufev->ev_write, bufev->ev_base, fd,
EV_WRITE|EV_PERSIST|EV_FINALIZE, bufferevent_writecb, bufev);
evbuffer_add_cb(bufev->output, bufferevent_socket_outbuf_cb, bufev);
evbuffer_freeze(bufev->input, 0);
evbuffer_freeze(bufev->output, 1);
跟之前的struct evconnlistener_event一樣赃梧,這里返回給用戶空間可用的struct bufferevent也是struct bufferevent_private的一部分,不過相比listener的單個(gè)struct event豌熄,這里的bufferevent內(nèi)容成員要復(fù)雜的多授嘀,其中我們比較熟悉的有:input、output兩個(gè)evbuffer锣险,可以調(diào)用evbuffer的族函數(shù)進(jìn)行相關(guān)的高級(jí)處理粤攒;此外還設(shè)置了be_ops為bufferevent_ops_socket,而enabled使能的時(shí)間中默認(rèn)為EV_WRITE囱持,所以EV_READ需要手動(dòng)enable;接下來設(shè)置bufev_private->deferred的callback回調(diào)函數(shù)和調(diào)用參數(shù)為bufferevent_run_deferred_callbacks_locked和bufev_private焕济。
最后的兩個(gè)event_assign分別將ev_read和ev_write兩個(gè)event的回調(diào)函數(shù)設(shè)置為了bufferevent_readcb/bufferevent_writecb纷妆。由于EV_WRITE默認(rèn)是使能的,所以還調(diào)用了evbuffer_add_cb設(shè)置其默認(rèn)的回調(diào)函數(shù)為bufferevent_socket_outbuf_cb晴弃。為了安全掩幢,還將兩個(gè)evbuffer先凍結(jié)起來,準(zhǔn)備工作還未就緒上鞠,所以此時(shí)還不允許數(shù)據(jù)傳輸际邻。
其實(shí),正如上面的例子芍阎,對(duì)于bufferevent世曾,通常的寫操作就使用其默認(rèn)的callback就可以了,實(shí)際開發(fā)當(dāng)中我們最關(guān)心的是讀事件谴咸,因?yàn)槲覀円邮諗?shù)據(jù)處理數(shù)據(jù)(即便只是轉(zhuǎn)發(fā)操作)轮听,而寫數(shù)據(jù)只要準(zhǔn)備好要發(fā)送的數(shù)據(jù),底層的寫就讓其自動(dòng)處理就可以了岭佳。
說到底血巍,這里的bufferevent和evconnlistener類似,也是采用了兩段式設(shè)計(jì):在bufferevent中的ev_read/write被激活調(diào)度的時(shí)候珊随,其自動(dòng)執(zhí)行的是bufferevent_readcb/writecb函數(shù)述寡,在這些標(biāo)準(zhǔn)函數(shù)中會(huì)做一些的預(yù)先處理操作柿隙,比如evbuffer_read/evbuffer_write_atmost的讀寫,到最后通過bufferevent_trigger_nolock_調(diào)用用戶設(shè)定的回調(diào)函數(shù)鲫凶。然后你可能感興趣EV_WRITE默認(rèn)的bufferevent_socket_outbuf_cb干了啥禀崖?查看其代碼,其實(shí)也就是:檢查確保當(dāng)前ev_write是否是pending的掀序,如果不是就bufferevent_add_event_變成pending狀態(tài)就好了帆焕。我們知道,epoll事件最底層是操作系統(tǒng)直接驅(qū)動(dòng)的不恭,所以如果底層驅(qū)動(dòng)發(fā)現(xiàn)socket是可寫的叶雹,就可以調(diào)度底層發(fā)送數(shù)據(jù)了,因此這個(gè)函數(shù)實(shí)際上其實(shí)啥都沒做换吧。
最后的bufferevent_enable(bev, EV_READ|EV_WRITE);跟之前的evconnlistener_enable也大差不差的折晦,最終都是通過be_socket_enable->bufferevent_add_event_->event_add方式,將event加入到對(duì)應(yīng)的event_base上面去沾瓦,使之變?yōu)閜ending狀態(tài)满着。
五、小結(jié)
Libevent的代碼實(shí)現(xiàn)的十分精妙贯莺,注釋也比較多风喇,當(dāng)然也有一些參數(shù)和符號(hào)尚無注釋,自己暫時(shí)也沒能意會(huì)缕探,本文尚有很多需要補(bǔ)充之處魂莫。
哎,想想自己epoll+線程池兩個(gè)c文件搞定爹耗,而Libevent把這個(gè)做的如此之精妙(且尚無線程池模型)耙考,不得不由衷的讓人敬佩:牛人怎么就那么牛呢?這也使得我堅(jiān)信潭兽,初學(xué)者和企業(yè)應(yīng)用之間總隔著一條溝溝需要跨越倦始,如果沒有項(xiàng)目帶著你走,那就可以靠讀這些開源項(xiàng)目的代碼來彌合山卦,希望大家都能愉快的玩耍和成長(zhǎng)鞋邑!
本文完!