Libuv代码简单分析
本文基于libuv 1.x版本进行简单的分析。
数据结构
uv__io_t
uv__io_t用来表示一个IO事件。
其成员包括:
成员 | 说明 |
---|---|
uv__io_cb cb | IO事件被触发的回调函数 |
void* pending_queue[2] | pending队列 |
void* watcher_queue[2] | watcher队列 |
unsigned int pevents | pending的事件mask,等待下一次被添加到事件中 |
unsigned int events | 当前的事件mask |
int fd | 事件fd |
queue
libuv的queue实现比较奇葩,一个queue里面的元素会有两个指针,一个指向队列前一个成员,一个指向队列下一个成员,在这里不做阐述,看到类似:
void* watcher_queue[2]
这样子定义了有两个void*指针的数组知道这是一个队列就好了。
uv_timer_t
定义定时器的结构体,libuv中使用最小堆来维护定时器。
一般而言,都是首先从这个最小堆数据结构中获得距离当前最近的定时器,然后拿到它的超时时间,以该超时时间做为下一次loop事件循环的时间,某些情况下会无视这个值,比如存在idle handler的情况下,此时会以0做为超时时间。
uv_handle_t及其子类
uv_handle_t是libuv中所有handler的基类,libuv中实现继承的手段也比较奇葩:
- 类成员定义放在宏里。
- 继承自某个基类的子类按照继承顺序依次放它前面基类的宏。
比如uv_tcp_t继承自uv_stream_t,而后者又继承自uv_handle_t,三者的定义如下:
struct uv_handle_s {
UV_HANDLE_FIELDS
};
struct uv_stream_s {
UV_HANDLE_FIELDS
UV_STREAM_FIELDS
};
struct uv_tcp_s {
UV_HANDLE_FIELDS
UV_STREAM_FIELDS
UV_TCP_PRIVATE_FIELDS
};
代码中可以看到:
- uv_handle_t的成员放在宏UV_HANDLE_FIELDS。
- uv_stream_t继承自uv_handle_t,所以在它结构体定义的开始部分先放的就是前面的宏UV_HANDLE_FIELDS。另外,uv_stream_t本身的成员定义又放在了宏UV_STREAM_FIELDS里。
- uv_tcp_t继承自uv_stream_t,所以它结构体开始部分依次是宏UV_HANDLE_FIELDS,再到宏UV_STREAM_FIELDS。
可以看到,uv_stream_t结构体的内存布局的开始部分与uv_handler_t一样,而uv_tcp_t又与uv_stream_t一样,因此可以这样来实现类似C++中的继承:
uv_tcp_t *tcp;
uv_stream_t *stream = (uv_stream_t*)tcp;
uv_handle_t *handle = (uv_handle_t*)stream;
C语言实现继承的方式很多,不一定非得使用宏来实现,使用宏最大的问题是导致代码的可读性下降,查找问题时带来困难。
简单看下UV_HANDLE_FIELDS的成员:
成员 | 说明 |
---|---|
uv_loop_t* loop | 事件循环 |
uv_handle_type type | handler类型 |
void* handle_queue[2] | 加入到的handler队列 |
以下简单列出了handle相关类来:
以下解释几个特殊的handler。
uv_async_t
该结构体用于线程之间消息通知之用。
uv_prepare_t
prepare handler,用于注册在每次loop循环时需要被调用的回调函数,这些回调函数会在IO事件处理之前被回调。
uv_check_t
check handler,用于注册在每次loop循环时需要被调用的回调函数,这些回调函数会在IO事件处理之后被回调。
uv_idle_t
idle handler与prepare handler已经,在每次loop循环中处理IO事件之前被调用。两者的区别在于,当存在idle handler的时候,loop循环会以超时时间0来调用事件循环,即不论有没有IO事件都马上返回。
uv_req_t及其子类
handler主要应对一定与某个文件fd相关联的事件,除了这些以外,libuv希望把所有可能导致阻塞的操作全部异步化,包括:文件操作、查询域名操作等。这部分需要异步化的流程,全部封装到了uv_req_t结构体中。
每个uv_req_t子类中,都有一个类型为struct uv__work的成员:
struct uv__work {
// 工作时回调函数
void (*work)(struct uv__work *w);
// 工作结束时回调函数
void (*done)(struct uv__work *w, int status);
// 对应的loop指针
struct uv_loop_s* loop;
// 工作队列指针
void* wq[2];
};
最终,每一个uv_req_t都会被放到一个线程中进行处理,处理完毕了才回调对应的函数。后面会展开讨论这个流程。
uv_req_t有以下子类:
- uv_getaddrinfo_t:用于getaddrinfo调用。
- uv_getnameinfo_t:用于getnameinfo调用。
- uv_shutdown_t:用于shutdown操作。
- uv_write_t:用于写操作。
- uv_connect_t:用于TCP连接。
- uv_udp_send_t:
- uv_fs_t:用于文件的IO读写请求。
- uv_worker_t:用于向线程提交一个任务。
除了uv_worker_t之外,其它几个操作都有以下的特点:可能会阻塞线程,所以就单独拿出来处理了。
uv_loop_t
uv_loop_t用于表达一个事件循环,即内部封装了epoll、kqueue这类的事件通知API。
其内部数据分为两大部分:
- 每个事件机制都会使用的数据,即公有数据。
- 每个事件机制独立的数据,使用宏UV_LOOP_PRIVATE_FIELDS,区分了unix和win两种平台的实现。而在unix版本的宏UV_LOOP_PRIVATE_FIELDS定义的最后部分,又引入了宏UV_PLATFORM_LOOP_FIELDS,用于定义不同unix操作系统相关的数据。
下面简单说明一下uv_loop_t公用的成员。
成员 | 说明 |
---|---|
unsigned int active_handles | 活跃的handler计数,每增加一个加一,相反减一 |
void* handle_queue[2] | 存储handler的队列,每个添加到uv_loop_t的handler都会存储到这里来 |
active_reqs | 存储活跃的req计数,不理解为什么这个成员要定义成union |
unsigned int stop_flag | 事件循环终止标志位 |
下面简单说明一下uv_loop_t各事件机制独立的成员,即宏UV_LOOP_PRIVATE_FIELDS成员,以linux平台来说明。
成员 | 说明 |
---|---|
int backend_fd | 事件监听fd,如epoll_create返回的fd就保存到这里 |
void* pending_queue[2] | pending事件队列,后面会加以说明 |
void* watcher_queue[2] | 观察事件队列,还没有加入事件监听的事件会先放在这里,后面会加以说明 |
uv__io_t** watchers | 存储uv__io_t*数组,其数组索引是fd |
unsigned int nwatchers | watchers的大小,不够的时候需要扩容 |
unsigned int nfds | watchers数组中实际存储的watcher数量 |
void* wq[2] | 存储worker的队列 |
void* process_handles[2] | 存储process handler的队列 |
void* prepare_handles[2] | 存储prepare handler的队列 |
void* check_handles[2] | 存储check handler的队列 |
void* idle_handles[2] | 存储idle handler的队列 |
核心流程
有了对以上核心数据结构的了解,可以来看看libuv的核心流程了。
事件循环
先来简单了解一下一个事件框架的主流程,有如下的伪代码:
循环:
根据定时器拿到距离最近的定时器超时时间
进行事件查询,以刚刚的超时时间做为查询最大时间
遍历查询回来的事件,对事件进行处理
遍历定时器,取出超时的事件进行处理
然而,这只是最核心的步骤,实现时还需要一定的优化,来看看libuv的做法。
新增的IO事件
uv_loop_t结构体中有watcher_queue队列,新增加进来的IO事件,并不首先直接添加到epoll事件中,而是首先放在watcher队列,待到下一次进行poll操作时,会首先将watcher队列中的IO事件添加进来,然后再执行poll操作。而定义一个IO事件,用到的就是前面的uv__io_t结构体。
同时,由于IO事件肯定是uv_handler_t的子类,因此同时uv_loop_t又有一个handler queue,用于保存所有的handler。每次loop的循环中,也会去查看有哪些handler可以被释放了。
pending队列
pending队列用于收集需要在一次loop中被回调的IO事件。loop循环的时候,每次会对pending队列的handler进行遍历然后回调,同时会返回这一次遍历了多少pending的IO事件。
check、idle和prepare队列
在src/unix/loop-watcher.c文件中,使用宏定义了check、idle和prepare三种队列。
idle和prepare队列,会分别在loop主循环中,在poll操作之前被遍历调用,而check队列则在poll操作之后被遍历,主要用于添加poll之后的检查操作。
而idle和prepare队列的区别在于,如果idle队列不为空,那么将会使用0做为poll操作的超时时间。
小结
以libuv官网文档中的一幅图来总结loop主循环的流程:
- 每次loop循环开始的时候,首先更新时间,以减少这部分系统调用次数。
- 如果loop当前不活跃,则直接退出函数了。如何判断是否活跃:看是否存在活跃的handler、活跃的请求。
- 遍历定时器的最小堆,调用所有超时的定时器。
- 遍历pending队列调用pending的handler。
- 遍历idle队列调用idle的handler。
- 遍历prepare队列调用prepare的handler。
- 计算poll操作的超时时间,有以下的情况需要考虑:
- 如果loop使用UV_RUN_NOWAIT标志运行,则返回0。
- 如果loop即将结束,则返回0。
- 如果没有活跃的handler或请求,返回0。
- 如果存在idle handler,返回0。
- 如果有等待关闭的handler,返回0。
- 如果以上情况都不是,那么从最小堆中得到距离当前时间最近的超时事件;如果连定时器都没有,则返回-1,即一直等待下去。
- 执行loop操作,而传入的超时时间就是第7步返回的时间。
- 遍历check队列调用check handler。 10.关闭需要释放的handler。
uv_req_t的处理
uv_req_t系列的子类,最后都会放到某个线程中处理,完成操作了之后再进行回调。
原因在于:libuv的设计中,不希望存在这些可能导致阻塞的操作,而是希望把这些操作全部异步化。
以uv_getaddrinfo函数为例,本质上这个操作封装了getaddrinfo,该操作对应的uv__work结构体中,work函数为uv__getaddrinfo_work,即在该操作被回调时的回调函数,内部封装了getaddrinfo函数,done函数为uv__getaddrinfo_done,封装了执行结束之后的回调操作。
libuv中,最终调用uv__work_submit函数向某个线程发送一个uv__work结构体指针来完成操作,其原型为:
void uv__work_submit(uv_loop_t* loop,
struct uv__work* w,
enum uv__work_kind kind,
void (*work)(struct uv__work* w),
void (*done)(struct uv__work* w, int status))
这里传入的参数,前面已经做了大体的解释,而枚举类型uv__work_kind有如下类型:
enum uv__work_kind {
UV__WORK_CPU, // 耗CPU的操作
UV__WORK_FAST_IO, // 快速IO操作
UV__WORK_SLOW_IO // 慢速IO操作
};
根据worker类型的不同,在将任务放到线程中会有不太一样的处理。
如果worker是UV__WORK_SLOW_IO,则该任务会放到slow_io_pending_wq队列中,如果慢速IO的任务数量,大于当前线程数量的一半以上,此时将暂停线程的执行,等待这些慢IO的操作完成才继续调度其它任务来执行。
总结
以下是读代码之后的一些简单总结:
- 大量的使用宏来模拟面向对象,导致可读性下降,不是很推荐这种做法。
- 将所有可能阻塞线程的操作都异步化了,用户使用这个库写不出阻塞同步的代码来。
- 事件框架的内部实现其实已经大同小异,无非是红黑树(Nginx)还是最小堆(libevent、libuv)来实现定时器之类的区别,已经玩不出太多的花样来,但是在怎么让上层用户更好的使用上,libevent走的路线是再往上走实现HTTP协议,libuv的实现则是将操作全部异步化。