libevent详解,libevent libev libuv

  libevent详解,libevent libev libuv

  libelevent-IntelIOT-blog park简介

  libev libevent简介最近开始重构定制公司的网站后台服务器,开始关注LibeEvent和livev。也欢迎相关同学与我们探讨。两者采用相同的架构和设计思想,很多原理和代码可以通过相互借鉴和比较来理解。

  简介

  Libev和libevent更适合分布式并发系统。与传统方法比较,请参考以下内容:

  原地址:3358 www . IBM . com/developer works/cn/AIX/library/au-libev/index . html

  许多服务器部署(尤其是web服务器部署)面临的最大问题之一是,它们必须能够处理大量的连接。无论是构建基于云的服务来处理网络流量,在IBM Amazon EC实例上分发应用程序,还是为网站提供高性能组件,都需要能够处理大量并发连接。

  一个很好的例子是web应用程序最近变得越来越动态,尤其是那些使用AJAX技术的应用程序。如果要部署的系统允许成千上万的客户端直接在网页中更新信息,比如提供事件或问题实时监控的系统,那么提供信息的速度就非常重要。在网格或云环境中,可能会同时打开来自数千个客户端的持久连接,因此有必要能够处理和响应每个客户端的请求。

  在讨论libevent和libev如何处理多个网络连接之前,让我们简单回顾一下处理这种连接的传统解决方案。

  回到顶端

  处理多个客户

  有许多不同的传统方法来处理多个连接,但它们在处理大量连接时经常会出现问题,因为它们使用了太多的内存或CPU,或者达到了一定的操作系统限制。

  使用的主要方法如下:

  Loop:早期的系统使用一种简单的循环选择解决方案,即遍历开放网络连接列表来确定是否有数据要读取。这种方法速度慢(尤其是当连接数增加时)且效率低(因为在处理当前连接时,其他连接可能正在发送请求并等待响应)。当系统循环通过每个连接时,其他连接必须等待。如果有100个连接,其中只有一个有数据,那么在轮到真正需要处理的连接之前,其他99个连接仍然必须被处理。Poll、epoll和variants:这是对loop方法的改进。它使用一个结构来保存要监控的每个连接的数组,当在网络套接字上发现数据时,通过回调机制调用处理程序函数。poll的问题是这个结构会非常大。当新的网络连接添加到列表中时,修改结构会增加负载并影响性能。select:select()函数调用使用的是静态结构,事先硬编码成一个相当小的数字(1024个连接),所以不适合非常大的部署。各种平台上还有其他实现(比如Solaris上的/dev/poll或者FreeBSD/NetBSD上的kqueue)。它们可能在各自的OS上有更好的性能,但是不能移植,也不一定能解决处理请求的高层问题。

  以上所有解决方案都通过一个简单的循环来等待和处理请求,然后将请求分派给另一个函数来处理实际的网络交互。关键是循环和网络套接字需要大量的管理代码,以便监控、更新和控制不同的连接和接口。

  处理许多连接的另一种方法是,通过使用现代内核中的多线程支持来监控和处理连接,为每个连接启动一个新线程。这就把责任直接交给了操作系统,但是会在RAM和CPU上增加相当大的开销,因为每个线程都需要自己的执行空间。此外,如果每个线程都忙于处理网络连接,线程之间的上下文切换将会很频繁。最后,许多内核不适合处理如此大量的活动线程。

  回到顶端

  Libevent方法

  Libevent库实际上没有替换select()、poll()或其他机制的基础。相反,使用每个平台最高效的高性能解决方案来为实现添加包装器。

  为了实际处理每个请求,libevent库提供了一个事件机制,它充当底层网络后端的包装器。事件使得为连接添加处理程序变得容易,同时降低了底层I/O的复杂性,这是libevent系统的核心。

  libevent库的其他组件提供其他功能,包括缓冲事件系统(用于缓冲发送到客户端/从客户端接收的数据)和HTTP、DNS和RPC系统的核心实现。

  创建libevent服务器的基本方法是注册一个操作发生时应该执行的函数,比如接受客户端的连接,然后调用主事件循环event_dispatch()。执行过程的控制现在由libevent系统处理。注册事件和要调用的函数后,事件系统开始自治;当应用程序运行时,您可以在事件队列中添加(注册)或删除(取消注册)事件。事件注册非常方便,可以添加新的事件来处理新打开的连接,从而构建一个灵活的网络处理系统。

  例如,可以打开一个监听套接字,然后注册一个回调函数,每当需要调用accept()函数打开一个新的连接时,就调用这个回调函数,这样就创建了一个网络服务器。清单1中显示的代码片段说明了基本过程:

  系统界面介绍

  地址:http://www.dirlt.com/libev.html

  目录1 libev 1.1关于代码1.2 EventLoop1.3 Watcher1.4工作原理1 . 4 . 1 Ev _ run 1 . 4 . 2 FD _ reify 1 . 4 . 3 back end _ poll 1 . 4 . 4 timers _ reify 1 . 4 . 5 Ev _ invoke _ pending 1.5示例1 . 5 . 1 common . h 1 . 5 . 2 echo-client . cc 1 . 5 . 3 echo-server . cc 1 libev主页http://software.schmorp.de/pkg/libev.html

  记录http://software.schmorp.de/pkg/libev.html

  libev实现的功能是一个强大的反应堆。也许通知事件主要包括以下内容:

  Ev_io //IO可读写ev_stat //文件属性变化ev_async //激活线程ev_signal //信号处理ev_timer //timer ev_periodic //周期任务ev_child //子进程状态变化ev_fork //开拓进程ev_cleanup //事件循环退出触发事件ev_idle //每一个事件循环空闲触发事件ev _ embed//todo (zhangyan04):我没有概念。ev _ prepare//event loop前的每一个事件ev _ check//event loop后的每一个事件event 1.1About The Code代码风格相当严谨,布局非常工整,而且从域名就能看出作者是德国人。但是内部使用了大量的宏,使得代码很难阅读。而且从代码上看,一开始应该有一个默认的event_loop,但是随着多核的出现,实际应用中可能会用到多个event _ loop。我猜作者为了方便,用了很多宏来代替。允许多个event_loop的宏是EV_MULTIPLICITY。例如,下面的代码

  void no inline EV _ io _ start(EV _ P _ EV _ io * w){ int FD=w-if(expect _ false(EV _ is _ active(w))return;assert ((libev: ev_io_start用负fd 调用,FD=0));assert ((libev: ev_io_start用非法事件掩码调用,(w-events ~(EV _ _ iof dset EV _ READ EV _ WRITE)));EV _ frequency _ CHECK;ev_start (EV_A_ (W)w,1);array_needsize (ANFD,anfds,anfdmax,fd 1,array _ init _ zero);wlist_add ( anfds[fd].头,(WL)西);fd_change (EV_A_ fd,w-events EV _ _ iof dset EV _ ANFD _ REIFY);w-events=~ EV _ _ iof dset;EV _ frequency _ CHECK;}第一次看这段代码会很难理解。

  宏描述定义了ev _ pevent参数struct ev_loop * loopev _ p _ ev _ p,ev _ aevent参数loopev _ a _ ev _ a,然后很多作为ev_loop成员的变量都封装到宏中。例如代码中的anfds,实际的宏定义是

  #定义anfds ((loop)- anfds)其实一个ev_loop的字段也不少,但也很正常,本身就是一个强大的反应器。但直接的结果是,很难知道ev_loop的全貌,所以想彻底了解libev也很麻烦,只能尝试从应用层面去了解。

  1.2EventLoop首先我们来关注一下反应器本身。在libev下,reactor对象被称为event_loop,event_loop允许动态创建和销毁,并绑定自定义数据。

  struct ev_loop * ev_loop_new(无符号int标志);void EV _ loop _ destroy(EV _ P);void EV _ set _ user data(EV _ P _ void * data);void * EV _ user data(EV _ P);在这里,我们主要关注旗帜。这里,我们主要选择使用什么后端进行轮询操作。您可以从以下选项中选择:

  ev back end _ selectevback end _ pollevback end _ epoll//通常我们选择这个ev back end _ kqueueevback end _ devpollevback end _ port,但是还有三个更重要的选项:

  ev flag _ no notify//inofity调用不适用于使用ev_stat。这可以减少fd的使用。EVFLAG_SIGNALfd //使用SIGNALfd检测是否有信号发生,这样也可以减少fd的使用。大多数时候,我们通常使用EVFLAG_AUTO(0)来满足需求。从代码上看,如果支持epoll,我们会首先选择epoll。因为在watcher的回调函数中可以知道当前的event_loop,这样我们就可以得到自定义的数据。然后我们来看看这个event_loop是如何运行和停止的。

  void ev_run (EV_P_ int标志);void EV _ break(EV _ P _ int how);同样,我们在这里更关注标志和how。Flags具有以下内容:

  0.通常这就是我们想要的。每个轮询将在轮询时等待一段时间,然后处理未决事件。EVRUN_NOWAIT。运行一次,轮询时不要等待。这种效果相当于只处理未决事件。运行一次。运行一次,但在轮询时等待,然后处理未决事件。以及以下内容是如何:

  EVBREAK_ONE。退出ev_run调用一次即可。总的来说,这样就够了。EVBREAK_ALL。退出所有ev_run调用。这种情况存在,ev_run在处理丁鹏时会被递归调用。在后端/epoll的底部,libev提供了一个接口回调,每次epoll_wait前后都可以调用这个接口回调。

  void EV _ set _ loop _ release _ CB(loop,void (*release)(EV_P),void (*acquire)(EV_P))静态void epoll _ poll(EV _ P _ EV _ t stamp time out){/* epoll等待时间不能大于(LONG_MAX - 999UL)/HZ毫秒,这低于*//*默认的libev最大等待时间。*/EV _ RELEASE _ CB;eventcnt=epoll_wait (backend_fd,epoll_events,epoll_eventmax,epoll_epermcnt?0:ev _ time out _ to _ ms(time out));EV _ ACQUIRE _ CB}在event_loop中,我们还关心一件事,就是每次event_loop轮询的持续时间。一般来说这个问题不大,但是我们需要在高性能的情况下设置。

  void EV _ set _ io _ collect _ interval(EV _ P _ EV _ t stamp interval);void EV _ set _ time out _ collect _ interval(EV _ P _ EV _ t stamp interval);在ev_run中有使用这些参数的代码是很麻烦的。但大意是这样的。如果这是timeout_interval,那么我们每次都必须在timeout_interval检查超时,并使用这个时间ev_sleep。但这样会影响io_interval,所以做了一些内部转换,转换结果视为epoll_wait超时。但很多时候,我们并不需要在意。默认时间为0.0,系统会用最快的响应方式来处理。

  1.3watcher接下来我们来关心一下EventHandler。在libev下,Watcher相当于EventHandler的概念,通常绑定了fd回调函数和我们需要关注的事件。那么一旦事件被触发,我们使用的回调函数也会被触发。回调函数的参数通常包括反应器、观察器和触发事件。我不打算在这里重复文档中与watcher和相应API相关的内容,但有些内容可能会提到一些评论。我们先来看看之前的大致流程。这里,TYPE用于区分不同类型的观察器。

  typedef void(*)(struct ev _ loop * loop,ev_TYPE *watcher,int revents)回调;//回调都是这种类型ev_init (ev_TYPE *watcher,回调);//初始化watcher ev _ TYPE _ set(ev _ TYPE * watcher,[args]);//设置watcher ev _ TYPE _ init(ev _ TYPE * watcher,callback,[args]);//通常使用这个函数最方便,初始化和设置都在这里ev_TYPE_start (loop,ev _ TYPE * watcher);//注册watcher ev_TYPE_stop (loop,ev _ TYPE * watcher);//注销观察器ev _ set _ priority(ev _ TYPE * watcher,int priority);//设置优先级ev_feed_event (loop,ev_TYPE *watcher,int revents);//这个做跨线程通知非常有用,相当于触发了某个事件bool ev _ is _ active(ev _ TYPE * watcher);//观察器是否活跃bool ev _ is _ pending(ev _ TYPE * watcher);//观察器是否待定int ev_clear_pending (loop,ev _ TYPE * watcher);//清除观察者待定状态并且返回事件韦克瑟的状态有下面这么几种:

  已初始化。调用初始化函数初始化活跃。调用开始进行注册待定。已经触发事件但是没有处理不活动。调用停止注销。这个状态等同于初始化这个状态。其实关于每个看守人具体是怎么实现的没有太多意思,因为大部分现有代码都差不多。会在下一节说说内部数据结构是怎么安排的,了解内部数据结构以及过程之后很多问题就可以避免了,比如文件描述符消失的特殊问题这类问题。

  1.4工作原理1.4.1ev_run最主要的还是看看电动汽车_运行这个部分代码。我们不打算仔细阅读只是看看梗概然后大体分析一下数据结构应该怎么样的

  void EV _ run(EV _ P _ int flags){ assert(( libev:EV _ loop recursion during release detected ,loop_done!=ev break _ RECURSE));loop _ done=ev break _ CANCELEV _ INVOKE _ PENDING/*在我们递归的情况下,确保排序保持良好和干净*/do { if(expect _ false(loop _ done))break;/*更新软驱相关的内核结构*/FD _ reify(EV _ A);/*计算阻塞时间*/{ ev_tstamp waittime=0 .ev_tstamp睡眠时间=0。/*记住io_blocktime计算的旧时间戳*/ev _ t戳prev _ Mn _ now=Mn _ now/*抵消回调处理开销的更新时间*/time _ update(EV _ A _ 1e 100);if (expect_true(!(flags EVRUN_NOWAIT idleall !active CNT)){等待时间=MAX _ block时间;if(timer CNT){ ev _ t stamp to=ANHE _ at(timers[heap 0])-Mn _ now back end _ fudge;if(等待时间to)等待时间=to} /*不要让超时将等待时间减少到time out _ block time */if(expect _ false(wait time time time out _ block time))等待时间=time out _ block time/*额外检查因为io_blocktime一般为0 */if(expect _ false(io _ block time)){ sleep time=io _ block time-(Mn _ now-prev _ Mn _ now);if(睡眠时间等待时间-后端_忽悠)睡眠时间=等待时间-后端_忽悠;if (expect_true (sleeptime 0 .)){ ev _ sleep(睡眠时间);等待时间-=睡眠时间;} } } assert((loop _ done=ev break _ RECURSE,1))。/*副作用断言*/后端_轮询(EV _ A _ wait time);assert((loop _ done=ev break _ CANCEL,1));/*为副作用断言*//*更新ev_rt_now,do magic */time _ update(EV _ A _ wait time睡眠时间);} /*将挂起的定时器排队并重新调度*/timers _ reify(EV _ A);/*调用最后*/EV_INVOKE_PENDING的相对计时器;} while (expect_true ( activecnt!循环_完成!(flags(ev run _ ONCE ev run _ NOWAIT));if(loop _ done==ev break _ ONE)loop _ done=ev break _ CANCEL;} 我们可以总结一下大致步骤,其实和大部分的事件循环写出来差不多。

  首先触发那些已经悬而未决的的观察者。判断是否loop_donefd_reify .这个后面会单独说。计算出等待时间并且进行必要的sleep.backend_poll开始轮询,并且整理好悬而未决的事件计时器_具体化。这个和fd _具体化不同调用电动汽车_调用_待定来触发悬而未决的的超正析象管事件非常简单。接下来我们看看fd _具体化,后端_轮询,定时器具体化以及电动汽车_调用_待定。

  1 .4 .2 FD _具体化下面是fd _具体化代码片段。可以看出,这个部分就是在修改软驱关注的事件。

  inline _ size void FD _ reify(EV _ P){ int I;for(I=0;I fdchangencti){ int FD=FD changes[I];ANFD * ANFD=anfds fdev _ io * w;无符号char o _ events=anfd-events;无符号char o _ reify=anfd-reify;anfd-reify=0;/* if(expect _ true(o _ REIFY EV _ ANFD _ REIFY))可能是一个反优化*/{ ANFD-event=0;for(w=(ev _ io *)anfd-head;w;w=(ev _ io *)((WL)w)-next)anfd-events =(无符号char)w-事件;if (o_events!=anfd-events)o _ reify=EV _ _ iof dset;/*其实=*/} if(o _ reify EV _ _ iof dset)back end _ modify(EV _ A _ FD,o_events,anfd-events);} fdchangecnt=0;} 而这个fdchanges这个是在哪里调用的呢。我们可以看到就是在ev_io_start这个部分。也就是说如果我们想要修改软驱关注事件的话,我们必须显示地电动汽车_io_stop掉然后修正之后重新ev_io_start .底层调用fd_change的话底层维护数组fdchanges来保存发生事件变动的fd .

  void no inline EV _ io _ start(EV _ P _ EV _ io * w){ int FD=w-if(expect _ false(EV _ is _ active(w))return;断言(( libev: ev_io_start用负fd 调用,FD=0));断言(( libev: ev_io_start用非法事件掩码调用,(w-events ~(EV _ _ iof dset EV _ READ EV _ WRITE));EV _频率_检查;ev_start (EV_A_ (W)w,1);array_needsize (ANFD,anfds,anfdmax,fd 1,array _ init _ zero);wlist_add ( anfds[fd].头,(WL)西);fd_change (EV_A_ fd,w-events EV _ _ iof dset EV _ ANFD _ REIFY);w-events=~ EV _ _ iof数据集;EV _频率_检查;} inline _ size void FD _ change(EV _ P _ int FD,int flags){ unsigned char reify=anf ds[FD].具体化;anfds [fd].reify =flagsif (expect_true(!reify)){ fdchangecnt;array_needsize (int,fdchanges,fdchangemax,fdchangecnt,empty 2);FD changes[fdchangecnt-1]=FD;} } 1 .4 .3后端_轮询后端_轮询底层支持很多投票实现,我们这里仅仅看ev_epoll.c就可以。代码在这里面我们不列举了,如果某个软驱触发事件的话那么最终会调用fd_event(EV_A_,fd,event)来进行通知。所以我们看看fd_event .

  inline _ speed void FD _ event _ nocheck(EV _ P _ int FD,int revents){ ANFD * ANFD=anfds FD;ev _ io * w;for(w=(ev _ io *)anfd-head;w;w=(ev _ io *)((WL)w)-next){ int ev=w-events revents;if (ev) ev_feed_event (EV_A_ (W)w,EV);} } void no inline EV _ feed _ event(EV _ P _ void * W,int revents){ W W _=(W)W;int pri=ABS pri(w _);if(expect _ false(w _-pending))pendings[pri][w _-pending-1].events =reventselse { w _-pending=pending nt[pri];array_needsize (ANPENDING,pendings [pri],pendingmax [pri],w_- pending,empty 2);//设置观察器和事件pendings [pri][w_- pending - 1].w=w _;pendings [pri][w_- pending - 1].事件=事件;} } 可以看到底层是一个死亡之指的数组,根据软驱进行偏移。如果软驱过大的话似乎会影响性能没有hpserver里面的多路分解表实现方式好。然后得到这个软驱下面所有的观察者,然后在循环挂起里面记录所有这些触发的观察者。

  计时器_具体化其中HEAP0就是最小堆下标。如果重复的话说明需要重复发生,那么就会重新调整时间戳,如果不是重复的话,那么内部会调用电动汽车计时器停止这个方法将这个计时器移除。所有的定时任务都通过进给_反向添加。进给_反向内部是维护一个动态数组来保存所有的定时器任务,然后在进给_反转_完成里面遍历这些任务来触发这些定时器任务。

  inline _ size void timers _ reify(EV _ P){ EV _ frequency _ CHECK;if(timer CNT ANHE _ at(timers[heap 0])Mn _ now){ do { ev _ timer * w=(ev _ timer *)ANHE _ w(timers[heap 0]);/*assert ((libev:检测到计时器堆上的非活动计时器,ev _ is _ active(w)));*//*第一次重新调度或停止定时器*/if(w-重复){ ev _ at(w)=w-重复;if(ev _ at(w)Mn _ now)ev _ at(w)=Mn _ now;断言(( libev:处理计时器时发现负的电动自行车计时器重复值,w-重复0 .));ANHE _ at _ cache(timers[heap 0]);downheap (timers,timercnt,heap 0);} else EV _ timer _ stop(EV _ A _ w);/*非重复:停止计时器*/EV _ frequency _ CHECK;feed _ reverse(EV _ A _(W)W);} while(timer CNT ANHE _ at(timers[heap 0])Mn _ now);进给_反向_完成(电动汽车_电动汽车_定时器);} 1 . 4 . 5 ev _ INVOKE _待定这个宏最终调用的函数就是下面这个,遍历所有的待定事件并且逐一触发。

  void no inline EV _ invoke _ pending(EV _ P){ int pri;for(pri=num pri;pri-;)while(pending CNT[pri]){ an pending * p=pendings[pri]-pending CNT[pri];p-w-pending=0;EV_CB_INVOKE (p- w,p-events);EV _频率_检查;} } 1.5示例尝试编写一个简单的带有超时的回声服务器和回声客户端就发现其实还有非常多的其他的工作量,比如缓冲器的管理状态机实现等。所以我没有写出一个完整的例子,只是简单地写了假设回声客户端连接上计算机网络服务器的话就简单地打印链接信息并且关闭。

  1 .5 .1常见。H # IFN def _ COMMON _ H _ # define _ COMMON _ H _ #包含unistd。H #包括fcntl。H #包含系统/类型。H #包含系统/插座。h包括arpa/inet。H #包含字符串。h # include cstdlib # include cstddio # include cstddef # include字符串命名空间公共{ # define D(exp,fmt,)do { \ if(!(exp)){ \ fprintf(stderr,fmt,# # _ _ VA _ ARGS _ _);\ abort();\ } \ } while(0)static void setnonblock(int FD){ fcntl(fd,F_SETFL,fcntl(FD,F _ GETFL) O _ NONBLOCK);}静态void setreuseaddr(int FD){ int ok=1;setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,ok,sizeof(ok));}静态void set address(const char * IP,int port,struct sockaddr _ in * addr){ bzero(addr,sizeof(* addr));addr-sin _ family=AF _ INET;inet_pton(AF_INET,ip,(addr-sin _ addr));addr-sin _ port=htons(port);} static STD:string address _ to _ string(struct sockaddr _ in * addr){ char IP[128];inet_ntop(AF_INET,(addr- sin_addr),ip,sizeof(IP));充电端口[32];snprintf(port,sizeof(port), %d ,ntohs(addr-sin _ port));STD:string r;r=r ( IP : port );return r;} static int new _ TCP _ server(int port){ int FD=socket(AF _ INET,SOCK_STREAM,IP proto _ TCP);D(fd 0,’套接字失败(% m)\ n );setnonblock(FD);setreuseaddr(FD);sockaddr _ in addrsetaddress( 0 . 0 . 0 . 0 ,port,addr);bind(fd,(struct sockaddr*) addr,sizeof(addr));听(fd,64);//backlog=64 return FD;} static int new _ TCP _ client(const char * IP,int port){ int fd=socket(AF_INET,SOCK_STREAM,IP proto _ TCP);setnonblock(FD);sockaddr _ in addrsetaddress(ip,port,addr);connect(fd,(struct sockaddr*)( addr),sizeof(addr));返回FD;} };//命名空间COMMON # endif//_ COMMON _ H _ 1。5 .2 echo-客户端。cc #包含 ev。H # include COMMON。h static void do _ connected(struct ev _ loop * reactor,ev_io* w,int events){ close(w-ev _ break(reactor,ev break _ ALL);} int main(){ struct ev _ loop *反应器=ev _ loop _ new(ev flag _ AUTO);int FD=common:new _ TCP _ client( 127。0 .0 .1 ,34567);ev _ io ioev_io_init( io,do_connected,fd,EV _ WRITE);电动汽车_io_start(反应器,io);ev_run(反应器,0);关闭(FD);ev_loop_destroy(反应器);返回0;} 1 .5 .3回声服务器。cc #包含 ev。h # include common。h static void do _ accept(struct ev _ loop * reactor,ev_io* w,int events){ struct sockaddr _ in addr;socklen _ t addr _ size=sizeof(addr);int conn=accept(w- fd,(struct sockaddr*) addr,addr _ size);STD:string r=common:address _ to _ string(addr);fprintf(stderr, accept %s\n ,r . c _ str());关闭(连接);} int main(){ struct ev _ loop *反应器=ev _ loop _ new(ev flag _ AUTO);int FD=common:new _ TCP _ server(34567);ev _ io w;ev_io_init( w,do_accept,fd,EV _ READ);电动汽车_io_start(反应器,ev_run(反应器,0);关闭(FD);ev_loop_destroy(反应器);}

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: