libev库

article/2025/10/8 15:05:27

libev库

    • 概念
    • 数据结构
      • watch集合
      • watch结构
    • 全局触发事件集合
      • 数据结构
      • 事件触发之IO事件
      • 定时器原理
      • ev_run函数

概念

libev是一个轻量级的事件通知库,具备支持多种事件通知能力。

数据结构

在熟悉代码之前先了解其相关数据结构往往更加方便后续代码的阅读。
在libev中关键的数据结构是,loop结构体,该结构体定义的字段较多,但是主要核心的可以分为两大类。
ev_loop结构体(loop为ev_loop结构的全局变量)的字段定义在ev_vars.h头文件中,然后在ev.c中通过include的方式导入。

ev_loop
ev_vars.h

watch集合

loop中有支持很多类型的事件,如下:
ev_io // IO可读可写
ev_stat // 文件属性变化
ev_signal // 信号处理
ev_timer // 相对定时器
ev_periodic // 绝对定时器
ev_child // 子进程状态变化
ev_fork // fork事件
ev_cleanup // event loop退出触发事件
ev_idle // event loop空闲触发事件
ev_embed // 嵌入另一个后台循环
ev_prepare // event loop之前事件
ev_check // event loop之后事件
ev_async // 线程间异步事件

这些事件的监控管理都对应一种类型的watcher数组,比如
(loop)->anfds :维护所有fd事件
(loop)->timers :维护所有的定时器
(loop)->periodics:周期性事件
(loop)->prepares:该事件是loop启动之前就会执行的事件等,每类事件都能在loop结构中找到对应的数组来维护对应的watchers。

watch结构

ev_watch
对于不同类型的事件的watcher,采用继承的方式来实现各个类型的watcher,libev是使用c的宏定义来实现继承。
ev_watch2
active表示这个监视器是否处于激活状态。
priority表示监视器的优先级,其值可以从-2~2,共5个级别。其中2是最高级别,-2是最低级别。级别高的监视器会优先于级别低的监视器执行。
cb是事件响应函数指针,data则是用于保存用户自定义的数据。这样的组合设计在使用回调函数的开源库中很常见。因为回调的调用机会并不由我们掌握,我们无法区分每次回调对应于我们哪次注册行为。而可以通过在向框架注册回调函数时保存回调调用的数据来达到区分的目的。
pending用于表示该监视器在触发过的相同优先级下所有监视器数组的索引下标。因为相同优先级的监视器可能有很多,所以我们需要一个结构保存这样的一组数据,于是就需要索引/下标进行区分。

ev_io
ev_watch_list

比如io事件的watcher如上定义,类似的各种watcher都采取此类方式来生成(这样的好处是可以减少代码的重复度,降低了代码维护的成本,但是可读性方面也相应降低)。

全局触发事件集合

loop->pendings
loop->pendings记录管理当前已经发生等待调用回调函数的watcher集合,libev检查到事件发生后将对应的watcher加入到loop->pendings。

数据结构

*Watcher[N][M]:二位数组用来维护一轮循环下来,需要触发回调函数的watcher
 其中N:是每一类watcher的优先级
 另外还有loop->pendingcnt结构,也是一个二维数组,用来维护pending中每一行最大watcher下标。
以下是事件函数回调过程的代码。
EV_P
code1
invoke
本质上就是个遍历loop->pendings挨个调用watcher的注册的回调函数,可以看到按照优先级从高(小)到低(大),对于同一优先级watcher按照下标从大到小的方式来调用callBack函数。
值得注意的,就是pendingcnt结构,该结构维护当前每一行watcher数组当前的最大下标。

事件触发之IO事件

该节将会以IO事件在EPOLL平台的触发整个流程来阐述libev是如何来实现事件触发回调的整个过程。在介绍IO事件触发之前需要介绍一下相关的数据结构
ANFD:用来记录对一个fd的所有监听事件,每一个监听事件使用链表结构进行组织。
anfd
anfds:是ANFD类型的数组,用来管理每一个fd的监听的事件
fdchanges:是int
类型数组,每个元素记录当前发生更改的fd,比如加入新的监听fd,或者fd的监听事件发送修改。
在每次循环之前,都会对fdchanges和anfds的结构中对应的fd事件列表的所有event进行 | 操作,得到当前整个fd当前的监听事件和ANFD.events进行对比,如果没有修改将不会该表epoll的fd的监听事件,这样做的好处,避免了无效的修改,保证了所有对epoll的修改都是必须的,毕竟频繁对epoll进行修改代价还是挺大的。

下面分析IO事件是如何触发的,当一个fd监听事件加入时
step1:调用ev_io_start将watcher加入到anfds中,并将修改记录在changfds中
ev_io_start
steps2: 调用fd_reify,通过比对changfds & anfds确定是否需要加入epoll事件,此时显然是需要的。
循环处理
step3:调用backend_poll函数得到当前监听已经发生的事件
epoll_init
#得到事件的fd & 已经发生的events,从anfds中获取对应的ANFD
#遍历ANFD中的watcher链表,比对监听事件和已经发生的监听事件,如果符合将该watcher加入到loop->pendings,修改watcher中的pending变量标记在loop->pendings中的数组下标

step4:遍历循环loop->pendings结构,挨个调用回调函数,从而完成事件触发的一个完整过程。
调用流程

定时器原理

libev在初始化默认循环时调用了ev_default_loop方法,其会在底层调用evpipe_init方法。它会通过eventfd创建一个永远等不到的事件。这样我们就可以调整等待该事件的超时时间来达到定时执行的目的。

void noinline
ev_signal_start (EV_P_ ev_signal *w) EV_THROW
{if (expect_false (ev_is_active (w)))return;assert (("libev: ev_signal_start called with illegal signal number", w->signum > 0 && w->signum < EV_NSIG));#if EV_MULTIPLICITYassert (("libev: a signal must not be attached to two different loops",!signals [w->signum - 1].loop || signals [w->signum - 1].loop == loop));signals [w->signum - 1].loop = EV_A;ECB_MEMORY_FENCE_RELEASE;
#endifEV_FREQUENT_CHECK;#if EV_USE_SIGNALFDif (sigfd == -2){sigfd = signalfd (-1, &sigfd_set, SFD_NONBLOCK | SFD_CLOEXEC);if (sigfd < 0 && errno == EINVAL)sigfd = signalfd (-1, &sigfd_set, 0); /* retry without flags */if (sigfd >= 0){fd_intern (sigfd); /* doing it twice will not hurt */sigemptyset (&sigfd_set);ev_io_init (&sigfd_w, sigfdcb, sigfd, EV_READ);ev_set_priority (&sigfd_w, EV_MAXPRI);ev_io_start (EV_A_ &sigfd_w);ev_unref (EV_A); /* signalfd watcher should not keep loop alive */}}if (sigfd >= 0){/* TODO: check .head */sigaddset (&sigfd_set, w->signum);sigprocmask (SIG_BLOCK, &sigfd_set, 0);signalfd (sigfd, &sigfd_set, 0);}
#endifev_start (EV_A_ (W)w, 1);wlist_add (&signals [w->signum - 1].head, (WL)w);if (!((WL)w)->next)
# if EV_USE_SIGNALFDif (sigfd < 0) /*TODO*/
# endif{
# ifdef _WIN32evpipe_init (EV_A);signal (w->signum, ev_sighandler);
# elsestruct sigaction sa;evpipe_init (EV_A);sa.sa_handler = ev_sighandler;sigfillset (&sa.sa_mask);sa.sa_flags = SA_RESTART; /* if restarting works we save one iteration */sigaction (w->signum, &sa, 0);if (origflags & EVFLAG_NOSIGMASK){sigemptyset (&sa.sa_mask);sigaddset (&sa.sa_mask, w->signum);sigprocmask (SIG_UNBLOCK, &sa.sa_mask, 0);}
#endif}EV_FREQUENT_CHECK;
}

因为定时器并非是由事件触发而执行,而是由于事件没有触发导致等待超时而执行。所以backend_poll函数指针调用时,不可以一直等待下去,而要传递超时时间。从而让libev中利用“永远等不到的事件”相关的监视器有机会执行。
利用等待超时这个思路非常有意思。但是又面临另一个问题,超时时间的选择?比如我们现在有两个定时器:2秒一次和3秒一次,那么超时时间该设置成多少呢?如果设置成2秒超时,那么3秒一次的定时器将被延期1秒执行(需要等待到第二个周期)。如果设置为3秒超时,2秒一次的定时器也将被延期1秒执行。如果设置成1秒超时,则超时导致循环的次数增多……这种固定超时的方案怎么都不太好。那么libev是如何解决这个问题的呢?
libev在实现的内部不会有“定时”这样的概念,也就是说每次事件等待的时长是不确定的。这也是为什么各个IO模型需要暴露backend_poll方法的原因——需要每次指定超时的时间。
那这个超时时间怎么计算?每个需要使用等待超时功能触发的监视器,都会在一个结构中保存下次触发的时间。以上面例子为例,并且假设没有其他事件的干扰,假如现在时间是12:00:00,则2秒一次定时器监视器(后称2秒监视器)的“下次执行时间”为12:00:02;3秒一次的定时器监视器(后称3秒监视器)的“下次执行时间”为12:00:03。那么本次等待的时间是离当前时间最近的2秒监视器“下次执行时间”减去当前时间,即12:00:02-12:00:00=2秒。等到时间为12:00:02时,2秒定时器会被执行,并且其“下次执行时间”修改成12:00:04。假设2秒定时器和本次循环中逻辑的执行时间消耗了0.5秒,此时时钟已经走到12:00:02.5。此时离现在最近的“下次执行时间”是3秒监视器,则下次循环的等待时间是12:00:03-12:00:02.5=0.5秒。于是12:00:03时,3秒监视器会被执行。

ev_run函数

1.更新更改的FD事件
2.进行必要的sleep
3.backend_poll收集pending的IO事件
4.收集pending的timer事件
5.调用所有pending的事件

int
ev_run (EV_P_ int flags)
{
#if EV_FEATURE_API++loop_depth;
#endifassert (("libev: ev_loop recursion during release detected", loop_done != EVBREAK_RECURSE));loop_done = EVBREAK_CANCEL;EV_INVOKE_PENDING; /* in case we recurse, ensure ordering stays nice and clean */do{
#if EV_VERIFY >= 2ev_verify (EV_A);
#endif#ifndef _WIN32if (expect_false (curpid)) /* penalise the forking check even more */if (expect_false (getpid () != curpid)){curpid = getpid ();postfork = 1;}
#endif#if EV_FORK_ENABLE/* we might have forked, so queue fork handlers */if (expect_false (postfork))if (forkcnt){queue_events (EV_A_ (W *)forks, forkcnt, EV_FORK);EV_INVOKE_PENDING;}
#endif#if EV_PREPARE_ENABLE/* queue prepare watchers (and execute them) */if (expect_false (preparecnt)){queue_events (EV_A_ (W *)prepares, preparecnt, EV_PREPARE);EV_INVOKE_PENDING;}
#endifif (expect_false (loop_done))break;//检测是否有fork事件,如果有进行fork事件的回调函数/* we might have forked, so reify kernel state if necessary */if (expect_false (postfork))loop_fork (EV_A);//检测fd的监听事件是否发生变化,是否需要修改epoll的监听事件/* update fd-related kernel structures */fd_reify (EV_A);/*计算需要休眠的事件#根据定时器 & 周期任务 & timeout_blocktime(超时事件收集间隔事件) & io_blocktime(io事件收集间隔事件)等信息计算此次循环需要sleep的时间*//* calculate blocking time */{ev_tstamp waittime  = 0.;ev_tstamp sleeptime = 0.;/* remember old timestamp for io_blocktime calculation */ev_tstamp prev_mn_now = mn_now;/* update time to cancel out callback processing overhead */time_update (EV_A_ 1e100);/* from now on, we want a pipe-wake-up */pipe_write_wanted = 1;ECB_MEMORY_FENCE; /* make sure pipe_write_wanted is visible before we check for potential skips */if (expect_true (!(flags & EVRUN_NOWAIT || idleall || !activecnt || pipe_write_skipped))){waittime = MAX_BLOCKTIME;if (timercnt){ev_tstamp to = ANHE_at (timers [HEAP0]) - mn_now;if (waittime > to) waittime = to;}#if EV_PERIODIC_ENABLEif (periodiccnt){ev_tstamp to = ANHE_at (periodics [HEAP0]) - ev_rt_now;if (waittime > to) waittime = to;}
#endif/* don't let timeouts decrease the waittime below timeout_blocktime */if (expect_false (waittime < timeout_blocktime))waittime = timeout_blocktime;/* at this point, we NEED to wait, so we have to ensure *//* to pass a minimum nonzero value to the backend */if (expect_false (waittime < backend_mintime))waittime = backend_mintime;/* extra check because io_blocktime is commonly 0 */if (expect_false (io_blocktime)){sleeptime = io_blocktime - (mn_now - prev_mn_now);if (sleeptime > waittime - backend_mintime)sleeptime = waittime - backend_mintime;if (expect_true (sleeptime > 0.)){//如果需要休眠则进行休眠ev_sleep (sleeptime);waittime -= sleeptime;}}}#if EV_FEATURE_API++loop_count;
#endifassert ((loop_done = EVBREAK_RECURSE, 1)); /* assert for side effect */backend_poll (EV_A_ waittime);//这里开始调用上层封装的epool,select进行轮询,收集pending事件assert ((loop_done = EVBREAK_CANCEL, 1)); /* assert for side effect */pipe_write_wanted = 0; /* just an optimisation, no fence needed */ECB_MEMORY_FENCE_ACQUIRE;if (pipe_write_skipped){assert (("libev: pipe_w not active, but pipe not written", ev_is_active (&pipe_w)));ev_feed_event (EV_A_ &pipe_w, EV_CUSTOM);}/* update ev_rt_now, do magic */time_update (EV_A_ waittime + sleeptime);}/* queue pending timers and reschedule them */timers_reify (EV_A); /* relative timers called last *///对pending的timer事件进行收集
#if EV_PERIODIC_ENABLEperiodics_reify (EV_A); /* absolute timers called first */
#endif#if EV_IDLE_ENABLE/* queue idle watchers unless other events are pending */idle_reify (EV_A);
#endif#if EV_CHECK_ENABLE/* queue check watchers, to be executed first */if (expect_false (checkcnt))queue_events (EV_A_ (W *)checks, checkcnt, EV_CHECK);
#endifEV_INVOKE_PENDING;//遍历所有pending事件}while (expect_true (activecnt&& !loop_done&& !(flags & (EVRUN_ONCE | EVRUN_NOWAIT))));if (loop_done == EVBREAK_ONE)loop_done = EVBREAK_CANCEL;#if EV_FEATURE_API--loop_depth;
#endifreturn activecnt;
}

http://chatgpt.dhexx.cn/article/kpmYtkmp.shtml

相关文章

libev 源码解析

一 libev简介 libev是一个轻量级的事件通知库&#xff0c;具备支持多种事件通知能力&#xff0c;通过对libev的源码的阅读&#xff0c;可以清楚了解事件通知实现内部机制。 二 核心数据结构 在libev中关键的数据结构是&#xff0c;loop结构体&#xff0c;该结构体定义的字段较…

android libev 编译,移植libev事件库到Android中

因为libev库是使用C语言写的&#xff0c;所以在Android项目中使用此库的方法是把libev编译成.so文件&#xff0c;在Android中使用jni方式来调用libev的.so文件。 我们大家都知道android的ndk开发可以编译c,c代码&#xff0c;不过需要自己写Android.mk文件。但是对于大多数开源项…

libev

开始之前先看一下libevent libev libuv&#xff0c;参考附录1. 本着我自己的个性&#xff0c;我喜欢简单的东西&#xff0c;越简单越好&#xff0c;越傻越好&#xff0c;所以在此我考虑libev&#xff0c;只是tmd&#xff0c;libev的官网打不开&#xff0c;真是无语了。 上例子…

libev:详解

事件库之Libev&#xff08;一&#xff09; 使用Libev Libev的作者写了一份很好的官方Manual,比较的齐全&#xff0c;即介绍了Libev的设计思想&#xff0c;也介绍了基本使用还包括内部各类事件详细介绍。这里略微赘述一下。Libev通过一个 struct ev_loop 结结构表示一个事件驱动…

事件驱动库 libev 使用详解

C/CLinux服务器开发/后台架构师知识体系 libev 是一个通过 C 语言编写的&#xff0c;高性能的事件循环库&#xff0c;支持多种事件类型&#xff0c;与此类似的事件循环库还有 libevent、libubox 等&#xff0c;在此详细介绍下 libev 相关的内容。 简介 这是一个简单而且高性…

数据库和实例

这篇文章跟数据库开发有什么关系呢&#xff1f;我感觉呢只从字面上看确实没有什么关系&#xff0c;可是了解的话跟大牛讨论时他们最起码不会被鄙视、面试时可能也会有用。如果你再深入的钻研下去你就会发现里面的内容好“丰满”。作为一个菜鸟&#xff08;指本人&#xff09;以…

【达梦数据库实例创建】

达梦数据库实例创建 系统环境&#xff1a;银河麒麟V10 数据库版本&#xff1a;dm8 一、创建数据库实例 1.通过命令切换到路径/dm8/tool 2.执行命令进入配置助手 ./dbca.sh 选择一般用途&#xff0c;自动调整数据库性能参数&#xff0c;进行下一步操作&#xff1a; 设置数据…

SQL 经典实例

《SQL经典实例》 1&#xff0c;在WHERE子句中引用别名列 直接引用-报错 把查询包装为一个内嵌视图&#xff0c;这样就可以引用别名列了 select * from ( select sal as salary, comm as commission from emp ) x where salary < 5000同理&#xff1a;当你想在 WHERE 子句中…

SQL示例数据库

在本教程中&#xff0c;我们将向您介绍在整个教程中使用的SQL示例数据库。以下数据库关系图 - HR 示例数据库&#xff1a; 在这个HR 示例数据库有7个表&#xff1a; employees表存储员工的数据信息。 jobs表存储工作数据信息&#xff0c;包括职位和工资范围。 departments表…

数据库案例

目录 微信朋友圈设计 用户及用户关系 发朋友圈 CDN 发布表 相册表 时间线 刷朋友圈 删除、拉黑、标签、不让他看、三天可见该怎么办 谁可以看 标签 第二步的权限控制 评论和赞 微信朋友圈设计 用户及用户关系 肯定有用户表作为基础 用户关系表&#xff0c;用户…

SQL数据库编写及示例

一、 数据库编写 1、数据库常用约束 主键约束: primary key 外键约束: foreign key (references) 唯一值约束: unique 默认值约束: default 检查约束: check 非空约束: not null 标识列: identity 2、创建数据表注意事项 主外键数据类型必须一致 列与列之间用&#xff0c;间隔…

sqlserver:什么是数据库实例?

环境&#xff1a; window server 2019 datacentersqlserver2014 x64 问题&#xff1a; 什么是SQL server实例&#xff1f;数据库的对象架构是怎样设计的&#xff1f; 先把官方的解释贴出来&#xff1a; https://docs.microsoft.com/zh-cn/sql/relational-databases/databases…

【转】数据库设计实例一学习

​​​​​​数据库1​​​​​​​​​​​​​​​​​​​​​对多&#xff0c;1对1&#xff0c;M对N学习 以RBAC为例。​​​​​​​​​​​​​​ 于 RBAC&#xff08;Role-based Access Control&#xff09;权限访问控制。也就是说一个用户可以有多个角色&#xff…

sql server 数据库设计实例

本实例为综合实例,考察数据库原理中的,sql脚本的编写,创建——增删改查,视图和索引的创建等;数据库ER图,关系模式;以及一些高级的应用包括:触发器,函数和存储过程。 (一). 数据库设计题目如下 有一个图书出版发行管理系统,其主要业务规则如下: 一个作者可以编写多…

达梦数据库创建及数据库实例管理

一、配置助手创建和删除数据库 数据库配置助手创建数据库调用 dbca.sh 图形化界面创建数据库&#xff1a;[dmdbaDCA02 tool]$ ./dbca.sh2021-01-11 11:43:45 [com.dameng.dbca.Startup] [INFO] 启动 DBCA 指定数据库名称、实例名称&#xff08;单机情况下数据库和实例名称可以…

MySql 数据库操作实例

MySql 数据库操作实例 案例描述创建插入数据内外连接~问题问题1&#xff1a;查询周星星的成绩问题2&#xff1a;查询所有人的平均成绩以及其他信息1&#xff09;查询所有人的平均成绩2&#xff09;查询平均成绩最高的前三名3&#xff09;查询平均成绩排名第三的学生信息 问题3&…

数据库五个经典实例

创建数据库链接&#xff0c;需要用到connection对象&#xff0c;recordset对象。 对数据库进行操作&#xff0c;需要用到command对象&#xff0c;parameter对象。这两个对象成对出现。 connection对象&#xff1a;创建数据库链接。在对数据库进行操作的前提步骤。 recordset…

Oracle 数据库实例介绍

文章目录 数据库实例介绍实例结构实例配置读写实例与只读实例实例生命周期实例标识Oracle 根目录Oracle 主目录Oracle SID 实例启动与关闭启动实例与数据库管理员登录启动实例加载数据库打开数据库只读模式数据库文件检查 关闭数据库与实例关闭模式关闭数据库正常关闭异常关闭 …

Python:二叉树遍历

二叉树遍历共有四种方法&#xff0c;分别是前序遍历、中序遍历、后序遍历和层次遍历。 前序遍历&#xff1a; 父节点——左孩子——右孩子 中序遍历&#xff1a;左孩子——父节点——右孩子 后序遍历&#xff1a;左孩子——右孩子——父节点 层次遍历&#xff1a;利用队列解…

【算法】二叉树遍历的几种常见方法

二叉树遍历的几种常见方法 一. 二叉树分类&#xff1a; 完全二叉树满二叉树扩充二叉树平衡二叉树 二. 二叉树的四种遍历方式&#xff1a; 前序遍历&#xff08;先根&#xff0c;再左&#xff0c;最后右&#xff09;中序遍历&#xff08;先左&#xff0c;再根&#xff0c;最…