Libuv代码简单分析

2019-01-23
10分钟阅读时长

本文基于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_tcp_t

可以看到,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相关类来:

uv_handler_t

以下解释几个特殊的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

下面简单说明一下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_iteration

  1. 每次loop循环开始的时候,首先更新时间,以减少这部分系统调用次数。
  2. 如果loop当前不活跃,则直接退出函数了。如何判断是否活跃:看是否存在活跃的handler、活跃的请求。
  3. 遍历定时器的最小堆,调用所有超时的定时器。
  4. 遍历pending队列调用pending的handler。
  5. 遍历idle队列调用idle的handler。
  6. 遍历prepare队列调用prepare的handler。
  7. 计算poll操作的超时时间,有以下的情况需要考虑:
  • 如果loop使用UV_RUN_NOWAIT标志运行,则返回0。
  • 如果loop即将结束,则返回0。
  • 如果没有活跃的handler或请求,返回0。
  • 如果存在idle handler,返回0。
  • 如果有等待关闭的handler,返回0。
  • 如果以上情况都不是,那么从最小堆中得到距离当前时间最近的超时事件;如果连定时器都没有,则返回-1,即一直等待下去。
  1. 执行loop操作,而传入的超时时间就是第7步返回的时间。
  2. 遍历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的实现则是将操作全部异步化。

参考资料