基于 epoll 设计类似 libevent 的异步 I/O 库 - 接口

这篇文章可以算是我在 GitHub 上一个工程的设计概要了。简要说明了该工程的设计思路以及技术要点。

本文章纯原创,没有参考资料。不过有设计过程中记录下的相关文章:

使用 pipe 在程序正文中捕获和处理信号

本文地址:https://segmentfault.com/a/1190000010098194

工程简介

基本原理

总所周知,在 Linux 中实现异步 I/O,适用的系统 API 就是 epoll。这里主要包括 epoll_ctl()epoll_wait() 两个函数。前者配置各个文件描述符在 epoll 里的机制,而后者则是关键的阻塞调用。

这个工程,就是基于 epoll,实现类似于 libevent 的异步 I/O 库。

函数的具体用法可以参照 man 页,或者工程的 AMCEpoll.c 源文件的 _dispatch_main_loop() 函数。


设计缘由

早期打算通用的写一个进程间通信库。通信库想要使基于异步 I/O 来做,又不想只是使用裸的 epoll。想起自己对异步 I/O 的原理也算是大致了解了,就自己研究着写一个。目前已经大致完成的 AMCEpoll 并没有经过严格的测试验证,更多地像是一个学习工程,或者说是课后作业一样。就像 Andrew S. Tanenbaum 教授的 Minix 类似哈。

不过这个库是瞄准了实用来设计的。如果要面向可靠的话,建议用开源的 libevent 来构建;如果觉得 libevent 太大了,则推荐 libev;如果觉得 libev 的开发者太少,不靠谱的话,那么可以用 libuv。如果需要有自己知识产权的产品的话,那么,可以自己设计一个——这也就是我开发这个库的初衷。

API

公共的 API 都在 AMCEpoll.h 文件中。各函数的说明如下:

接口说明

struct AMCEpoll

相当于 libevent 的 “event base”。这是整个 AMCEpoll 对象,每一个对象可执行一个事件循环(event loop)。

struct AMCEpollEvent

相当于 libevent 的 “event”。与 libevent 不同的是,每一个 event 可以从某个 base 中分离,再加入到另一个 base 里面去。不过实际上应该没有这样的需求,只是说我的程序架构允许这么做。

AMCEpoll 支持三种事件:

  • 文件事件:或者称 fd 事件、文件描述符事件。基于 Linux 的 file descriptor 的事件。同时支持事件超时
  • 信号事件:也就是 signal 事件。可以捕获信号,然后在事件循环中处理信号,而不用担心信号函数的上下文。信号时间也支持超时机制
  • 超时事件:纯粹的超时事件,支持一次性的定时,也支持循环定时。

AMCEpoll_New()

创建、初始化并返回一个 AMCEpoll 对象。其中参数 pollSize 指的是 epoll_wait() 函数中的 maxevents 参数。

AMCEpoll_Free()

销毁一个 AMCEpoll 对象。如果对象中还有事件存在,并且事件关注了 EP_EVENT_FREE 事件,则会收到对应的回调。

不能在 event loop 中调用。

AMCEpoll_NewEvent()

创建并返回一个事件(AMCEpollEvent)对象。各参数说明如下:

  1. fd:当创建文件事件时,fd 参数就是对应的文件描述符;当创建信号事件时,fd 则是信号码。如果是纯超时时间,fd 必须设置为 -1。需要注意的是,所有的文件事件在 event loop 启动之前,必须确保 fd 是非阻塞的。
  2. events:EP_EVENT_XXX 掩码组成的时间列表集。具体各个掩码的作用,后文再说。纯粹的超时事件还没集成上去(这有什么难的呢,毕竟 fd 事件和 signal 事件的 timeout 都已经完成了。只是我最近没时间……)。
  3. timeout:超时时间,单位是毫秒。当然,实际上超时调用的时候是大于这个数的,毕竟函数切换需要时间。在压力大的时候,建议超时不要小于 1 秒,最好大于 10 秒。
  4. callback:接收事件的回调函数。
  5. userData:事件回调函数的自定义参数。

AMCEpoll_FreeEvent()

销毁一个事件。如果事件关注了 EP_EVENT_FREE,那么会在此时收到回调。程序员可以在这个地方执行一些库以外的清理工作,比如 close(fd) 之类的。

如果该事件已经加入到某个 AMCEpoll 对象中,那么这个函数会出错。如果要避免这个问题,建议使用 AMCEpoll_DelAndFreeEvent() 函数。

AMCEpoll_AddEvent()

将某个 AMCEpollEvent 对象加入到 AMCEpoll 中,这样才能在事件循环中关注这个对象上的事件。

AMCEpoll_DelEvent()

将已经加入到 AMCEpoll 的时间分离开来,成为 “孤魂野鬼” 状态。

AMCEpoll_DelAndFreeEvent()

Del 和 Free 两个函数的结合。

AMCEpoll_SetEventTimeout()AMCEpoll_GetEventTimeout()

重新设置或者获取一个事件的超时时间。这只有在创建时,添加了 EP_EVENT_TIMEOUT 事件,才会有效。

AMCEpoll_Dispatch()

在当前线程启动事件循环。当没有事件注册在案的时候,event loop 会退出,也就是这个函数会返回。

AMCEpoll_LoopExit()

退出事件循环。如果这个函数在 event loop 外调用,那么 AMCEpoll 会在走完下一次 event chain 之后退出;如果在 event loop 中间调用,则 AMCEpoll 会走完当前 event chain 之后退出。Event chain 的原理,在后文说明。


事件类型

事件类型就是 event_t 数据类型,声明如下:

enum {
    EP_EVENT_READ    = (1 << 0),
    EP_EVENT_WRITE   = (1 << 1),
    EP_EVENT_ERROR   = (1 << 2),
    EP_EVENT_FREE    = (1 << 3),
    EP_EVENT_TIMEOUT = (1 << 4),
    EP_EVENT_SIGNAL  = (1 << 5),

    EP_MODE_PERSIST  = (1 << 8),    /* only used when adding events */
    EP_MODE_EDGE     = (1 << 9),    /* only used when adding events */
};

当创建事件的时候,上述所有的类型都可以使用;而当回调的时候,回调函数只可能接收到 EV_EVENT_XXX 类型的值。三种 AMCEpollEvent 所支持的具体类型如下:

———— _READ _WRITE _ERROR _FREE _TIMEOUT _SIGNAL _PERSIST _EDGE
文件事件 O O O O O . O O
信号事件 . . O O O O O O
超时事件 . . . O O . . .

具体的参数说明如下:

  • EP_EVENT_READ:读事件。如果出现这个事件类型,则视为文件事件。
  • EP_EVENT_WRITE:读事件。如果出现这个事件类型,则视为文件事件。
  • EP_EVENT_ERROR:一般是系统调用出错。保留。
  • EP_EVENT_FREE:“销毁” 事件。当调用 AMCEpoll_FreeEvent() 的时候触发。
  • EP_EVENT_TIMEOUT:超时事件
  • EP_EVENT_SIGNAL:信号事件。如果出现这个事件类型,则视为信号事件。不得与文件事件同时存在。
  • EP_MODE_PERSIST:持续事件。当一次回调(除了 free)完成后,程序会自动将事件及其超时配置加入到 event loop 中。当然,如果程序手动 add 的话,也是可以的,只是没必要。
  • EP_MODE_EDGE:边缘触发模式。如果使用这个模式的话,只在出现事件才会触发回调。

典型调用过程

基本的使用流程和 libevent 和 libev 非常类似,可以参照我的这篇文章:《使用 libev 构建 TCP 响应服务器的简单流程》

比如一个 UDP 事件基本流程:

  1. 创建 AMCEpoll 对象
  2. 创建 DGRAM fd,必要的话 bind
  3. 基于 fd 创建一个 AMCEpollEvent 对象
  4. 将 AMCEpollEvent 对象 add 到 AMCEpoll 对象中
  5. 写一个 callback,遇到 read 事件的时候,调用 recvfrom()
  6. dispatch

其他事件的用法,则可以参照工程内的 test_server.c 文件。这个文件创建了三种事件:

  1. 一个 DNS 请求和解析,是一个 UDP read 事件。获取到 DNS 响应后,将自身事件清除掉。
  2. 一个 HTTP server(其实 HTTP 响应不规范,请别在意),并且 echo 回数据。是一个边沿触发的 TCP read 事件。
  3. 两个信号事件,分别监听 SIGINTSIGQUIT。监听到前者时,退出 AMCEpoll 事件循环。监听到后者时,打印调试信息。

下一篇

下一篇会说明一下工程的实现篇。文章尚未完成,敬请期待。

你可能感兴趣的:(linux,epoll,c,异步io,libevent)