这篇文章可以算是我在 GitHub 上一个工程的设计概要了。简要说明了该工程的设计思路以及技术要点。
本文章纯原创,没有参考资料。不过有设计过程中记录下的相关文章:
本文地址: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)对象。各参数说明如下:
- fd:当创建文件事件时,fd 参数就是对应的文件描述符;当创建信号事件时,fd 则是信号码。如果是纯超时时间,fd 必须设置为 -1。需要注意的是,所有的文件事件在 event loop 启动之前,必须确保 fd 是非阻塞的。
- events:EP_EVENT_XXX 掩码组成的时间列表集。具体各个掩码的作用,后文再说。纯粹的超时事件还没集成上去(这有什么难的呢,毕竟 fd 事件和 signal 事件的 timeout 都已经完成了。只是我最近没时间……)。
- timeout:超时时间,单位是毫秒。当然,实际上超时调用的时候是大于这个数的,毕竟函数切换需要时间。在压力大的时候,建议超时不要小于 1 秒,最好大于 10 秒。
- callback:接收事件的回调函数。
- 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 事件基本流程:
- 创建 AMCEpoll 对象
- 创建 DGRAM fd,必要的话 bind
- 基于 fd 创建一个 AMCEpollEvent 对象
- 将 AMCEpollEvent 对象 add 到 AMCEpoll 对象中
- 写一个 callback,遇到 read 事件的时候,调用
recvfrom()
- dispatch
其他事件的用法,则可以参照工程内的 test_server.c 文件。这个文件创建了三种事件:
- 一个 DNS 请求和解析,是一个 UDP read 事件。获取到 DNS 响应后,将自身事件清除掉。
- 一个 HTTP server(其实 HTTP 响应不规范,请别在意),并且 echo 回数据。是一个边沿触发的 TCP read 事件。
- 两个信号事件,分别监听
SIGINT
和SIGQUIT
。监听到前者时,退出 AMCEpoll 事件循环。监听到后者时,打印调试信息。
下一篇
下一篇会说明一下工程的实现篇。文章尚未完成,敬请期待。