Libev使用

之前没有用过libev,一般直接裸写的epoll,总结的话,libev的功能是: 支持将SOCKET,管道, 信号,以及定时器统一为通用的变成逻辑,给开发人员提供了一个简单高效的异步网络编程库。

先看一段简单的客户端程序,标准echo服务:

1 #include <unistd.h>
2 #include <stdio.h>
3 #include <stdlib.h>
4 #include <math.h>
5 #include <errno.h>
6 #include <netinet/in.h>
7 #include <strings.h>
8 #include "ev.h"
9  
10 #define PORT 8333
11 #define BUFFER_SIZE 1024
12  
13 //gcc test.c -lm ev.o
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);
17  
18 int main() {
19     struct ev_loop *loop = ev_default_loop(0);
20     int sd;
21     struct sockaddr_in addr;
22     int addr_len = sizeof(addr);
23     struct ev_io socket_watcher;
24     ev_timer timeout_watcher;
25  
26     if( (sd = socket(PF_INET, SOCK_STREAM, 0)) < 0 )
27     {
28         printf("socket error. errno:%d\n"errno);
29         return -1;
30     }
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);
37     }
38     if (listen(sd, 2) < 0) {
39         printf("listen error\n");
40         return -1;
41     }
42     printf("ev_loop beg\n") ;
43  
44     //设置cb函数,字段等
45     ev_io_init(&socket_watcher, accept_cb, sd, EV_READ);
46     ev_io_start(loop, &socket_watcher);
47  
48     ev_timer_init (&timeout_watcher, timeout_cb, 2, 1);
49     ev_timer_start (loop, &timeout_watcher);
50  
51     while (1) {
52         printf("ev_loop\n") ;
53         ev_loop(loop, 0);
54     }
55     return 0;
56 }
57  
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);
61     int client_sd;
62  
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");
66         return;
67     }
68  
69     client_sd = accept(watcher->fd, (struct sockaddr *)&client_addr, &client_len);
70     if (client_sd < 0) {
71         printf("accept error\n");
72         return;
73     }
74     printf("someone connected.\n");
75  
76     ev_io_init(w_client, read_cb, client_sd, EV_READ);
77     ev_io_start(loop, w_client);
78 }
79  
80 void read_cb(struct ev_loop *loop, struct ev_io *watcher, int revents){
81     char buffer[BUFFER_SIZE];
82     ssize_t read;
83  
84     if(EV_ERROR & revents) {
85         printf("error event in read");
86         return;
87     }
88  
89     read = recv(watcher->fd, buffer, BUFFER_SIZE, 0);
90     if(read < 0){
91         printf("read error,errno:%d\n"errno);
92         return;
93     }
94     if(read == 0) {
95         printf("someone disconnected.errno:%d\n"errno);
96         ev_io_stop(loop,watcher);
97         free(watcher);
98         return;
99     else {
100         printf("get the message:%s\n",buffer);
101     }
102  
103     send(watcher->fd, buffer, read, 0);
104     bzero(buffer, read);
105 }
106  
107 static void timeout_cb (EV_P_ ev_timer *w, int revents) {
108     puts ("timeout");
109     //ev_break (EV_A_ EVBREAK_ONE);
110 }

100行左右的代码,很简单的echo服务,这是libev程序的基本写法,下面分析一下主要的函数。由于现在大部分系统都使用epoll,所以下面假定用的是epoll模式。

一、ev_default_loop初始化epoll句柄

ev_default_loop函数返回一个struct ev_loop *的变量地址,实际上是全局变量default_loop_struct,这个结构体的内容如下,其实就是整个事件循环的总结构体,里面很多成员变量,全部都是用宏定义在ev_vars.h中。

1   struct ev_loop
2   {
3     ev_tstamp ev_rt_now;
4     #define ev_rt_now ((loop)->ev_rt_now)
5     #define VAR(name,decl) decl;
6       #include "ev_vars.h"
7     #undef VAR
8   };
9   #include "ev_wrap.h"
10  
11 //ev_vars.h
12 VAR (pendings, ANPENDING *pendings [NUMPRI])  //实际上就是ANPENDING * ev_loop->pendings ,是个指针数组,实际上当优先级队列用的,类似于linux内核里面的进程优先级队列。
13 VARx(ANFD *, anfds) //同上,这个用来记录每个SOCK的注册的事件列表,用fd索引。
14  
15 //ev_wrap.h
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);//关注可读事件,回调函数为accept_cb,用来接收新连接。
2 //函数内容:
3 #define ev_io_init(ev,cb,fd,events)          do { ev_init ((ev), (cb)); ev_io_set ((ev),(fd),(events)); } while (0)
4  
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);            \ //设置优先级,注意libev是能支持对不同的事件加优先级的,这个优先级决定:在同时epoll_waite返回的多个句柄后,优先级高的先得到回调。
9   ev_set_cb ((ev), cb_);            \    //设置回调函数到ev->cb, 注意这个跟其他的比如redis,nginx会分别有2个函数不一样。
10 while (0)
11  
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分别设置可读可写事件。

1 void noinline
2 ev_io_start (EV_P_ ev_io *w) EV_THROW
3 {//记录这个句柄的事件,实际上还没有加入epoll的, 只是记录到了fdchanges里面
4   int fd = w->fd;
5  
6   //检查是否已经开启了
7   if (expect_false (ev_is_active (w)))
8     return;
9  
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))));
12  
13   EV_FREQUENT_CHECK;
14  
15   ev_start (EV_A_ (W)w, 1);
16   array_needsize (ANFD, anfds, anfdmax, fd + 1, array_init_zero);//为新的句柄准备空间,放到(loop)->anfds里面去的
17   wlist_add (&anfds[fd].head, (WL)w);//将W插入到anfds[fd]的头部,挂一个未处理事件,比如增加,删除啥的
18  
19   /* common bug, apparently */
20   assert (("libev: ev_io_start called with corrupted watcher", ((WL)w)->next != (WL)w));
21  
22   fd_change (EV_A_ fd, w->events & EV__IOFDSET | EV_ANFD_REIFY);
23   w->events &= ~EV__IOFDSET;
24  
25   EV_FREQUENT_CHECK;
26 }
27 inline_size void
28 fd_change (EV_P_ int fd, int flags)
29 {//标记这个句柄的改动到fdchanges,后面run的时候会处理加入epoll的
30   unsigned char reify = anfds [fd].reify;
31   anfds [fd].reify |= flags;
32  
33   if (expect_true (!reify))
34     {
35       ++fdchangecnt;
36       array_needsize (int, fdchanges, fdchangemax, fdchangecnt, EMPTY2);
37       fdchanges [fdchangecnt - 1] = fd;//因为这里根本没有将句柄加入epoll, 而是将这个有改动事件的事情记录
38       //到ev_loop->fdchangeslim .意思是说:这个fdchanges数组里面的句柄都有新的事件加入或者删除,待会需要调用epoll_ctl来注册。
39     }
40 }

上面比较关注的函数是,因为这里根本没有将句柄加入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) {
2   do {
3       fd_reify (EV_A);//这个函数是将ev_io_start开启的事件记录的fdchanges列表设置到epoll句柄监听事件集里面去
4       /* calculate blocking time */
5       {
6 //···根据定时器的最近触发时间,计算这次的epoll等待最长可以等待多久。
7       }
8         //这里虽然waite了,而且解析读写事件类型了,但是还没有调用那些处理函数的,只是将他们挂入了pendings 的优先级队列
9         backend_poll (EV_A_ waittime);//实际上调用的是epoll_wait等待监听事件可读可写
10         time_update (EV_A_ waittime + sleeptime);
11       timers_reify (EV_A); /* relative timers called last */
12  
13       EV_INVOKE_PENDING;// FUCK, 又用宏,实际上就是调用了ev_invoke_pending函数,一个个将上面backend_poll里面
14       //检测到的有事件的SOCKT pendings队列里面的事件进行处理
15 }
  • 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)
2 {//将fdchanges里面在ev_io_start里面设置记录的这些新事件一个个处理,真正加入epoll里面.所谓的reify具体化
3   int i;
4  
5   for (i = 0; i < fdchangecnt; ++i)
6     {
7       int fd = fdchanges [i];//只需要获取里面的fd,然后就可以在anfds里面索引描述符数据的位置了
8       ANFD *anfd = anfds + fd;
9       ev_io *w;
10  
11       unsigned char o_events = anfd->events;
12       unsigned char o_reify  = anfd->reify;
13  
14       anfd->reify  = 0;
15  
16       /*if (expect_true (o_reify & EV_ANFD_REIFY)) probably a deoptimisation */
17         {
18           anfd->events = 0;
19  
20           //尝试一个个事件的去处理,看起来不能支持减少字段似得
21           for (w = (ev_io *)anfd->head; w; w = (ev_io *)((WL)w)->next)
22             anfd->events |= (unsigned char)w->events;
23  
24           if (o_events != anfd->events)
25             o_reify = EV__IOFDSET; /* actually |= */
26         }
27  
28         //下面调用的其实是epoll_modify, 后者根据具体情况调用epoll_ctl函数,将新事件设置到epoll里面去 ,或者减少
29       if (o_reify & EV__IOFDSET)
30         backend_modify (EV_A_ fd, o_events, anfd->events);
31     }
32  
33   fdchangecnt = 0;
34 }

从上面可以看出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。

1 inline_speed void
2 fd_event_nocheck (EV_P_ int fd, int revents)
3 {//对一个SOCK进行检查,看他是不是想要revents的事件,如果想要,那么将其加入到pengdings优先级队列里面,以备后续处理
4   ANFD *anfd = anfds + fd;
5   ev_io *w;
6  
7   for (w = (ev_io *)anfd->head; w; w = (ev_io *)((WL)w)->next)
8     {//看到了吧,anfd的head链表实际上就是一个个事件,比如可读,可写分别为2个事件,发生事件时会扫描这里,去寻找需要的事件,然后处>
9
10       int ev = w->events & revents;
11  
12       if (ev)
13         ev_feed_event (EV_A_ (W)w, ev);
14     }
15 }
16 void noinline
17 ev_feed_event (EV_P_ void *w, int revents) EV_THROW
18 {//喂养事件,实际上就是将事件放到(loop)->pendings的数组里面,然后后面会一个个按照优先级去处理的
19   W w_ = (W)w;
20   int pri = ABSPRI (w_);
21  
22   if (expect_false (w_->pending))
23     pendings [pri][w_->pending - 1].events |= revents;
24   else
25     {
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;
30     }
31  
32   pendingpri = NUMPRI - 1;
33 }

从上面也可以看出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)
3  
4 static void noinline ecb_cold
5 loop_init (EV_P_ unsigned int flags) EV_THROW
6 {//调用epoll_create创建epoll句柄
7 //···
8       invoke_cb          = ev_invoke_pending;//这个就是触发回调函数的函数,在ev_loop里面调用
9 //···

从此可见,EV_INVOKE_PENDING实际上就是ev_invoke_pending函数,其很简单,从高优先级的开始,一个个处理pendings数组。

1 void noinline
2 ev_invoke_pending (EV_P)
3 {//触发在epoll_wait后加入到pendings优先级队列里面的事件,一个个一次调用他们的cb回调函数
4   pendingpri = NUMPRI;
5  
6   while (pendingpri) /* pendingpri possibly gets modified in the inner loop */
7     {
8       --pendingpri;
9  
10       while (pendingcnt [pendingpri])
11         {
12           ANPENDING *p = pendings [pendingpri] + --pendingcnt [pendingpri];
13  
14           p->w->pending = 0;
15           EV_CB_INVOKE (p->w, p->events);//调用回调函数
16           EV_FREQUENT_CHECK;
17         }
18     }
19 }
20 # define EV_CB_INVOKE(watcher,revents) (watcher)->cb (EV_A_ (watcher), (revents))

这样遍历从高优先级的开始处理,从而达到优先级的效果。不过其实感觉这个没啥太多用处。因为快慢都差不多反正立即处理了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了小程序应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值