之前没有用过libev,一般直接裸写的epoll,总结的话,libev的功能是: 支持将SOCKET,管道, 信号,以及定时器统一为通用的变成逻辑,给开发人员提供了一个简单高效的异步网络编程库。
先看一段简单的客户端程序,标准echo服务:
6 |
#include
<netinet/in.h> |
11 |
#define
BUFFER_SIZE 1024 |
14 |
void accept_cb( struct ev_loop
*loop, struct ev_io
*watcher, int revents); |
15 |
static void timeout_cb
(EV_P_ ev_timer *w, int revents)
; |
16 |
void read_cb( struct ev_loop
*loop, struct ev_io
*watcher, int revents); |
19 |
struct ev_loop
*loop = ev_default_loop(0); |
21 |
struct sockaddr_in
addr; |
22 |
int addr_len
= sizeof (addr); |
23 |
struct ev_io
socket_watcher; |
24 |
ev_timer
timeout_watcher; |
26 |
if (
(sd = socket(PF_INET, SOCK_STREAM, 0)) < 0 ) |
28 |
printf ( "socket
error. errno:%d\n" , errno ); |
31 |
bzero(&addr, sizeof (addr)); |
32 |
addr.sin_family
= AF_INET; |
33 |
addr.sin_port
= htons(PORT); |
34 |
addr.sin_addr.s_addr
= INADDR_ANY; |
35 |
if (bind(sd,
( struct sockaddr*)
&addr, sizeof (addr))
!= 0) { |
36 |
printf ( "bind
error.errno:%d\n" , errno ); |
38 |
if (listen(sd,
2) < 0) { |
39 |
printf ( "listen
error\n" ); |
42 |
printf ( "ev_loop
beg\n" )
; |
45 |
ev_io_init(&socket_watcher,
accept_cb, sd, EV_READ); |
46 |
ev_io_start(loop,
&socket_watcher); |
48 |
ev_timer_init
(&timeout_watcher, timeout_cb, 2, 1); |
49 |
ev_timer_start
(loop, &timeout_watcher); |
58 |
void accept_cb( struct ev_loop
*loop, struct ev_io
*watcher, int revents){ |
59 |
struct sockaddr_in
client_addr; |
60 |
socklen_t
client_len = sizeof (client_addr); |
63 |
struct ev_io
*w_client = ( struct ev_io*) malloc ( sizeof ( struct ev_io)); |
64 |
if (EV_ERROR
& revents){ |
65 |
printf ( "error
event in accept\n" ); |
69 |
client_sd
= accept(watcher->fd, ( struct sockaddr
*)&client_addr, &client_len); |
71 |
printf ( "accept
error\n" ); |
74 |
printf ( "someone
connected.\n" ); |
76 |
ev_io_init(w_client,
read_cb, client_sd, EV_READ); |
77 |
ev_io_start(loop,
w_client); |
80 |
void read_cb( struct ev_loop
*loop, struct ev_io
*watcher, int revents){ |
81 |
char buffer[BUFFER_SIZE]; |
84 |
if (EV_ERROR
& revents) { |
85 |
printf ( "error
event in read" ); |
89 |
read
= recv(watcher->fd, buffer, BUFFER_SIZE, 0); |
91 |
printf ( "read
error,errno:%d\n" , errno ); |
95 |
printf ( "someone
disconnected.errno:%d\n" , errno ); |
96 |
ev_io_stop(loop,watcher); |
100 |
printf ( "get
the message:%s\n" ,buffer); |
103 |
send(watcher->fd,
buffer, read, 0); |
107 |
static void timeout_cb
(EV_P_ ev_timer *w, int revents)
{ |
100行左右的代码,很简单的echo服务,这是libev程序的基本写法,下面分析一下主要的函数。由于现在大部分系统都使用epoll,所以下面假定用的是epoll模式。
一、ev_default_loop初始化epoll句柄
ev_default_loop函数返回一个struct ev_loop *的变量地址,实际上是全局变量default_loop_struct,这个结构体的内容如下,其实就是整个事件循环的总结构体,里面很多成员变量,全部都是用宏定义在ev_vars.h中。
4 |
#define
ev_rt_now ((loop)->ev_rt_now) |
5 |
#define
VAR(name,decl) decl; |
12 |
VAR
(pendings, ANPENDING *pendings [NUMPRI]) |
16 |
#define
anfds ((loop)->anfds) //逆天了,不就直接写一下嘛,用的这么生僻·· |
17 |
#define
pendings ((loop)->pendings) |
比如定义的总之一句,这个结构就是整个库的中心结构,里面包含各种需要的成员变量。
二、事件初始化
接下来程序用socket新建了一个SOCK句柄,并且绑定了本地端口,然后用listen设置句柄为监听状态。 这些都是比较通用的,跟libev没有太大关系。 后面就需要针对不同的句柄,设置不同的事件回调。
值得注意的是libev支持文件通知,定时器,管道等,其分别用ev_io, ev_timer等表示,记录相关的数据,然后分别用不同的函数初始化ev_io_init, ev_timer_init,实际上就是讲几份代码放在一起,稍微柔和下。从而支持不同的类型。
ev_io_init 函数初始化epoll的回调函数,以及记录事件的参数,存放在ev_io结构里面,但此时还没有加入到epoll里面的。
1 |
ev_io_init(&socket_watcher,
accept_cb, sd, EV_READ); |
3 |
#define
ev_io_init(ev,cb,fd,events) do { ev_init ((ev), (cb)); ev_io_set ((ev),(fd),(events)); } while (0) |
5 |
#define
ev_init(ev,cb_) do { \ |
6 |
((ev_watcher
*)( void *)(ev))->active
= \ |
7 |
((ev_watcher
*)( void *)(ev))->pending
= 0; \ |
8 |
ev_set_priority
((ev), 0); \ |
9 |
ev_set_cb
((ev), cb_); \ |
12 |
#define
ev_io_set(ev,fd_,events_) do { (ev)->fd = (fd_); (ev)->events = (events_) | EV__IOFDSET; } while (0) |
可以看出ev_io_init没有做什么实质性的事情,就是记录了一下几个参数而已。
ev_io_start 函数虽然名字叫start,但实际有点骗人,因为没有start,记录这个句柄的事件,实际上还没有加入epoll的, 只是记录到了fdchanges里面。
libev里面对于所有的SOCKT句柄,都会在ev_loop->anfds数组里面按下标索引了一个槽位,其里面是个anfds[fd].head链表,每个节点代表我们上面用ev_io_init设置的事件,因此如果要同时关注可读,可写事件,需要调用2次ev_io_init,ev_io_start分别设置可读可写事件。
2 |
ev_io_start
(EV_P_ ev_io *w) EV_THROW |
7 |
if (expect_false
(ev_is_active (w))) |
10 |
assert (( "libev:
ev_io_start called with negative fd" ,
fd >= 0)); |
11 |
assert (( "libev:
ev_io_start called with illegal event mask" ,
!(w->events & ~(EV__IOFDSET | EV_READ | EV_WRITE)))); |
15 |
ev_start
(EV_A_ (W)w, 1); |
16 |
array_needsize
(ANFD, anfds, anfdmax, fd + 1, array_init_zero); |
17 |
wlist_add
(&anfds[fd].head, (WL)w); |
20 |
assert (( "libev:
ev_io_start called with corrupted watcher" ,
((WL)w)->next != (WL)w)); |
22 |
fd_change
(EV_A_ fd, w->events & EV__IOFDSET | EV_ANFD_REIFY); |
23 |
w->events
&= ~EV__IOFDSET; |
28 |
fd_change
(EV_P_ int fd, int flags) |
30 |
unsigned char reify
= anfds [fd].reify; |
31 |
anfds
[fd].reify |= flags; |
33 |
if (expect_true
(!reify)) |
36 |
array_needsize
( int ,
fdchanges, fdchangemax, fdchangecnt, EMPTY2); |
37 |
fdchanges
[fdchangecnt - 1] = fd; |
上面比较关注的函数是,因为这里根本没有将句柄加入epoll, 而是将这个有改动事件的事情记录到ev_loop->fdchangeslim .意思是说:这个fdchanges数组里面的句柄都有新的事件加入或者删除,待会需要调用epoll_ctl来注册。
对于定时器事件来说,大体类似,只是不需要epoll了。就不多说了。下面看最主要的ev_loop循环。
三、ev_loop事件监听循环
实际上ev_loop是个宏观定义,内容就是ev_run (EV_A_ flags); ev_run 函数挺大的,分步介绍。
1 |
int ev_run
(EV_P_ int flags)
{ |
9 |
backend_poll
(EV_A_ waittime); |
10 |
time_update
(EV_A_ waittime + sleeptime); |
- ev_run简化一下就是这么简单,先用fd_reify将之前调用ev_io_init,ev_io_start挂到ev_loop->fdchanges数组里面的socket句柄一个个处理掉,也就是放入epoll监听事件集合中。
- 然后调用backend_poll进行等待监听可读写事件,实际上是调用epoll_poll 函数,后者再调用epoll_wait真正去等待事件。
- 最后用宏EV_INVOKE_PENDING 来将发生过事情的事件回调函数按优先级触发。
下面分别介绍一下。
fd_reify注册可读写事件到epoll
fd_reify将fdchanges里面在ev_io_start里面设置记录的这些新事件一个个处理,真正加入epoll里面.所谓的reify具体化。
1 |
inline_size void fd_reify
(EV_P) |
5 |
for (i
= 0; i < fdchangecnt; ++i) |
7 |
int fd
= fdchanges [i]; |
8 |
ANFD
*anfd = anfds + fd; |
11 |
unsigned char o_events
= anfd->events; |
12 |
unsigned char o_reify
= anfd->reify; |
21 |
for (w
= (ev_io *)anfd->head; w; w = (ev_io *)((WL)w)->next) |
22 |
anfd->events
|= (unsigned char )w->events; |
24 |
if (o_events
!= anfd->events) |
25 |
o_reify
= EV__IOFDSET; |
29 |
if (o_reify
& EV__IOFDSET) |
30 |
backend_modify
(EV_A_ fd, o_events, anfd->events); |
从上面可以看出fd_reify会遍历ev_loop->fdchanges 数组,将里面有事件改动的socket的(ev_io *)anfd->head事件列表进行整合,然后看是否跟之前有变动,如果有,则调用backend_modify修改epoll注册,实际调用的其实是epoll_modify, 后者根据具体情况调用epoll_ctl函数,将新事件设置到epoll里面去 ,或者减少。
epoll_modify在ev_epoll.c里面,其整体就是个epoll_ctl调用,再熟悉不过了。
至此,一个SOCKET的epoll事件已经加入到了epollfd中了,这样只要wait,就能监听这个事件的变化。
backend_poll监听等待SOCKET可读
backend_poll实际上是epoll_poll,这个是在整个初始化的时候在epoll_init里面设置的。epoll_poll挺简单的,做2件事情:
- 调用epoll_wait等待监听事件通知;
- fd_event解析事件的类型,然后放到pendings的优先级队列里面,这样到后面再慢慢处理;
注意这个epoll_poll实际上还没有触发回调函数。里面对每个可读写的fd都会调用fd_event (EV_A_ fd, got); , 最终调用fd_event_nocheck, ev_feed_event。
2 |
fd_event_nocheck
(EV_P_ int fd, int revents) |
4 |
ANFD
*anfd = anfds + fd; |
7 |
for (w
= (ev_io *)anfd->head; w; w = (ev_io *)((WL)w)->next) |
10 |
int ev
= w->events & revents; |
13 |
ev_feed_event
(EV_A_ (W)w, ev); |
17 |
ev_feed_event
(EV_P_ void *w, int revents)
EV_THROW |
20 |
int pri
= ABSPRI (w_); |
22 |
if (expect_false
(w_->pending)) |
23 |
pendings
[pri][w_->pending - 1].events |= revents; |
26 |
w_->pending
= ++pendingcnt [pri]; |
27 |
array_needsize
(ANPENDING, pendings [pri], pendingmax [pri], w_->pending, EMPTY2); |
28 |
pendings
[pri][w_->pending - 1].w = w_; |
29 |
pendings
[pri][w_->pending - 1].events = revents; |
32 |
pendingpri
= NUMPRI - 1; |
从上面也可以看出libev的优先级是怎么回事,就是会在本次epoll_wait返回后,优先级高的先回调。实现方法是用ev_loop->pendings[]数组来记录不同优先级的事件。从而让后面的代码按顺序处理。
ep_run里面还用timers_reify处理了一下定时器事件,periodics_reify处理绝对时间的事件。
最后调用EV_INVOKE_PENDING 而触发各种毁掉函数,其实一个宏。
1 |
#
define EV_INVOKE_PENDING invoke_cb (EV_A) |
2 |
#define
invoke_cb ((loop)->invoke_cb) |
4 |
static void noinline ecb_cold |
5 |
loop_init
(EV_P_ unsigned int flags)
EV_THROW |
8 |
invoke_cb
= ev_invoke_pending; |
从此可见,EV_INVOKE_PENDING实际上就是ev_invoke_pending函数,其很简单,从高优先级的开始,一个个处理pendings数组。
2 |
ev_invoke_pending
(EV_P) |
10 |
while (pendingcnt
[pendingpri]) |
12 |
ANPENDING
*p = pendings [pendingpri] + --pendingcnt [pendingpri]; |
15 |
EV_CB_INVOKE
(p->w, p->events); |
20 |
#
define EV_CB_INVOKE(watcher,revents) (watcher)->cb (EV_A_ (watcher), (revents)) |
这样遍历从高优先级的开始处理,从而达到优先级的效果。不过其实感觉这个没啥太多用处。因为快慢都差不多反正立即处理了。