libuv源码分析(三)资源抽象:Handle 和 Request
Handle 是 libuv 设计实现的核心部分之一,根据官方描述:Handles 代表长生命周期的对象有能力执行某些操作当它们处于激活状态下。
libuv 采用了组合的方式实现代码复用,并且达到了面向对象编程中的继承的效果。
Handle 有很多种不同的类型,这些类型有一个共同的、公共的基础结构 uv_handle_s,因结构体内存布局字节对齐所有子类型都可以强制类型转换成 uv_handle_t 类型,所以所有能够应用在 uv_handle_t 上的基础API都可用于子类型的 handle。
在开始对不同类型的 Handle 开始分析之前,将会对 Handle 进行整体分析。
首先,看一下 libuv 中的 Handle 相关的类型声明和定义:
https://github.com/libuv/libuv/blob/v1.28.0/include/uv.h#L201
1 | /* Handle types. */ |
以上是 libuv 中所有的 handle 声明,并且都起了类型别名,命名规律显而易见,uv_loop_t 也在其中,uv_loop_t 作为所有资源的统一入口同样也是一种资源,而且是生命周期最长的资源。
下面来开一下 uv_handle_s 的结构定义:
https://github.com/libuv/libuv/blob/v1.28.0/include/uv.h#L411
1 |
|
宏 UV_HANDLE_FIELDS 被放到了 struct uv_handle_s:
https://github.com/libuv/libuv/blob/view-v1.28.0/include/uv.h#L426
1 | /* The abstract base class of all handles. */ |
如注释所言,struct uv_handle_s 是所有 handle 的抽象基类。
在 *nix 平台下,UV_HANDLE_PRIVATE_FIELDS 宏定义如下:
https://github.com/libuv/libuv/blob/v1.x/src/uv-common.h#L62
1 |
|
flags 可以使用的标识如下:
1 | /* Handle flags. Some flags are specific to Windows or UNIX. */ |
所有 handle 都具备 UV_HANDLE_ACTIVE UV_HANDLE_CLOSED 等几个公共状态,还有一些特地 handle 特定的状态。
以上为 uv_handle_s 定义,其中字段是通过 UV_HANDLE_FIELDS 宏定义和引入的,这样做的目的是为了复用字段定义部分的代码,能有效降低代码量,提升可维护性。相关字段的功能描述见字段后的说明。
uv_handle_t 实际上就是作为所有其他 handle 的基类存在的,其他 handle 通过组合的方式集成了 uv_handle_t 字段,通过强制类型转换,可以转换为 uv_handle_t,之后在其上应用 uv_handle_t 的相关方法。
以 stream 为例看一下其类型定义:
https://github.com/libuv/libuv/blob/v1.x/include/uv.h#L461
1 |
|
在 uv_stream_s 结构体中,包含了 uv_handle_s 的宏定义 UV_HANDLE_FIELDS 和 uv_stream_s 类型特定宏定义 UV_STREAM_FIELDS,uv_stream_s 和 uv_handle_s 在结构体内存布局上存在公共的部分且是以起始地址对齐的,uv_stream_s 比 uv_handle_s 多出一块特有的部分, 可以通过强制类型转换将 uv_stream_s 转换为 uv_handle_s。
uv_handle_t 定义了所有 handle 公共的部分,作为一个抽象基类存在。uv_handle_t 是不直接使用的,因为它并不能支持用户需求,无实际意义,实际上,在使用其他派生类型时,会间接使用 uv_handle_t。所有派生类型在初始化的时候,也进行了 uv_handle_t 的初始化,这类似于高级语言构造函数在执行时常常需要调用基类构造函数一样。除初始化操作以外,同样还有其他操作需要调用 uv_handle_t 函数的相关操作。
一般来说,派生类型具备如下几个操作:
uv_{handle}_init:初始化handle结构,把各个字段设置成合理值,并插入loop->handle_queue队列;uv_{handle}_start:启动handle使其处于UV_HANDLE_ACTIVE状态;uv_{handle}_stop:停止handle使其处于UV_HANDLE_CLOSED状态,并移出loop->handle_queue队列。
以上各派生类型的公共操作,体现了 handle 的生命周期,和 loop 生命周期类似,除此之外还包括一些特定 handle 特定处理逻辑。
因为各个派生类型的初始化/启动/停止逻辑都不相同,所以并没有公共的初始化/启动/停止方法,每个派生类型根据需要实现类型特定的初始化/启动/停止函数,同时它们还需要在内部调用基类的方法进行对象 初始化/启动/停止 uv_handle_t,对应的方法为:
uv__handle_inituv__handle_startuv__handle_stop
从命名可以看出这些都是不对外暴露的方法。
接下来我就来看一下 handle 的初始化。
Init:uv__handle_init
uv_handle_t 的初始化代码是用宏 uv__handle_init 定义的宏函数,采用宏可实现内联 inline 函数的效果,实现如下:
https://github.com/libuv/libuv/blob/v1.28.0/src/uv-common.h#L282
1 |
uv__handle_init 是一个使用宏定义的宏函数,do{}while(0) 是一种巧妙用法,形成代码块,且调用时后边必须加分号,会被编译器优化掉。
这段代码主要完成以下几个工作:
- 关联
loop到handle,可以通过handle找到对应的loop; - 设置
handle类型; - 设置
handle标识为UV_HANDLE_REF,这个标识位决定了handle是否计入引用计数。后续 Start Stop 会看到其用途; - 将
handle插入loop->handle_queue队列的尾部,所有初始化的handle就将被插入到这个队列中; - 通过
uv__handle_platform_init平台特定初始化函数将handle的next_closing设置为NULL,这是一个连接了所有关闭的handle的单链表。
如下是 uv_timer_t 的初始化函数 uv_timer_init,它直接引用了 uv__handle_init 初始化 uv_handle_t,其他派生类型也是如此。
https://github.com/libuv/libuv/blob/v1.28.0/src/timer.c#L62
1 | int uv_timer_init(uv_loop_t* loop, uv_timer_t* handle) { |
这样初始化工作就完成了,各个派生结构特定的初始化部分可能很简单,也可能很复杂。
Start:uv__handle_start
uv_handle_t 的启动代码是用宏 uv__handle_start 定义的宏函数,实现如下:
https://github.com/libuv/libuv/blob/v1.28.0/src/uv-common.h#L239
1 |
uv__handle_start 将 handle 设置为 UV_HANDLE_ACTIVE 状态,并通过 uv__active_handle_add 更新活动的 handle 引用计数。如果不存在 UV_HANDLE_REF 标志位,则不会增加引用计数。
虽然对 handle 进行了 Start 操作,但是实际仅仅是设置了个标志位和增加了一个引用计数而已,看不到任何的 Start,实际上是告诉 libuv 该 handle 准备好了,可以 Go 了。因为更新引用计数间接影响了事件循环的活动状态。
uv_run 才是真正的启动操作,向 libuv 表明 Ready 了之后,uv_run 的时候才会处理这个 handle。
Stop:uv__handle_stop
handle 的 Stop: 操作 由 uv__handle_stop 宏实现:
1 |
uv__handle_stop 将 handle 设置为 ~UV_HANDLE_ACTIVE 状态,并通过 uv__active_handle_rm 更新活动的 handle 引用计数。如果不存在 UV_HANDLE_REF 标志位,则不会减少引用计数。
Stop 是 Start 的反向操作,将 handle 修改为非准备状态。
Close:uv_close
对于 handle 来说,还有一个 Close 方法 uv_close,Close 可以认为是 Init 的反向操作,它将 handle 从 loop->handle_queue 移除,清理资源并触发回调。
不同于上面三个方法,uv_close 是对外开放的,适用于所有类型 handle 的方法,在 uv_close 内部根据不同的类型,调用对应的函数处理。
https://github.com/libuv/libuv/blob/v1.28.0/src/unix/core.c#L107
1 | void uv_close(uv_handle_t* handle, uv_close_cb close_cb) { |
https://github.com/libuv/libuv/blob/v1.28.0/src/unix/core.c#L209
1 | void uv__make_close_pending(uv_handle_t* handle) { |
在 loop 上有一个 closing_handles 字段,这是一个单向链表,关联了处于关闭进行中的 handle,这个字段的类型是 uv_handle_t*,指向了 uv_handle_t,而 uv_handle_s 存在了一个 uv_handle_t* 类型的指针 next_closing 指向下一个 handle, 这样就形成一个单向链表。
如下 closing_handles 的声明和初始化:
1 |
1 |
1 | int uv_loop_init(uv_loop_t* loop) { |
uv_close 通过调用 uv__make_close_pending 将待关闭的 handle 放到 loop->closing_handles 链表末尾,panding 的含义是延迟到下次事件循环处理。
在 uv_run 的 Call close callbacks 阶段,通过函数 uv__run_closing_handles 专门负责处理 loop->closing_handles:
https://github.com/libuv/libuv/blob/v1.28.0/src/unix/core.c#L343
1 | int uv_run(uv_loop_t* loop, uv_run_mode mode) { |
https://github.com/libuv/libuv/blob/v1.28.0/src/unix/core.c#L286
1 | static void uv__run_closing_handles(uv_loop_t* loop) { |
https://github.com/libuv/libuv/blob/v1.28.0/src/unix/core.c#L236
1 | static void uv__finish_close(uv_handle_t* handle) { |
首先将 closing_handles 从 loop 摘除,然后遍历 closing_handles,通过 uv__finish_close 对每个 handle 进行最后的 close,handle 被移除 loop->handle_queue 并调用其关联的 close_cb,至此 handle 彻底没有了和 loop 的关联走完了一个完整的生命周期。
uv_close 的处理过程被拆分成了两段,一段是调用 uv__make_close_pending,另一段是在事件循环中调用 uv__run_closing_handles,关闭的过程是异步的,用户程序无法仅仅是通过 uv_close 返回判断关闭是否完成,需要在 close_cb 中接收异步操作结果。那么问题来了,为什么要拆分成两段而不是一次性处理完呢?
uv_close 一般来说都是在异步回调中被调用的,因为一个 handle 的关闭在逻辑上依赖于 handle 完成相关工作,而异步的逻辑中,完成工作后会调用相应的回调,所以只有在回调中调用 uv_close 才能使逻辑上是同步。
Close 阶段可以看做是 Init 阶段的反向操作。
handle 就这样伴随着事件循环经历了 Init -> Start -> Stop -> Close 等生命周期。
Reference counting:Ref & Unref
上文已经遇到过,handle 有个 UV_HANDLE_REF 标志位,这个状态用于控制 handle 是否计入 loop->active_handles 引用计数,因为 handle 的引用计数影响 loop 活动状态,所以 UV_HANDLE_REF 状态会间接影响 loop 的状态。
接下来,我们看下引用计数相关API:
https://github.com/libuv/libuv/blob/view-v1.28.0/src/uv-common.c#L502
1 | void uv_ref(uv_handle_t* handle) { |
https://github.com/libuv/libuv/blob/v1.28.0/src/uv-common.h#L255
1 |
https://github.com/libuv/libuv/blob/v1.28.0/src/uv-common.h#L221
1 |
存在引用计数标志 UV_HANDLE_REF,会计入引用计数,否则不会引用计数。
实现非常简单,在条件满足的情况下,更新 loop->active_handles 值。
在事件循环初始化函数 uv_loop_init 中,loop->child_watcher、loop->wq_async 都被 Unref 了,避免影响 loop 的存活状态。
Status:Active & Closing
https://github.com/libuv/libuv/blob/v1.28.0/src/unix/core.c#L400
1 | int uv_is_active(const uv_handle_t* handle) { |
https://github.com/libuv/libuv/blob/v1.28.0/src/unix/core.c#L301
1 | int uv_is_closing(const uv_handle_t* handle) { |
https://github.com/libuv/libuv/blob/v1.28.0/src/uv-common.h#L233
1 |
实现简单,无需解释。
handle 和 loop 的关联关系
handle 和 loop 之间的关联是最为重要的,handle 必须注册到 loop 中的各种结构中才有意义,脱离 loop 的 handle 是毫无用途的,只有关联到 loop 上的 handle 才能在事件循环的过程中被处理。以上 handle 生命周期的核心就是在管理这种关系。除了以上基本的关联之外,handle 和 loop 还有其他关联。
在 Init 和 Close 操作中,handle 被插入/移除 loop->handle_queue 队列,uv__active_handle_add、uv__active_handle_rm 这两个宏函数修改 handle 的引用计数,进而间接修改了 loop 的状态。
除 loop->handle_queue 外,loop 中还有多个 handle 有关的队列,handle 除了被插入 loop->handle_queue 队列外,还会被插入到类型特定的结构中(如:队列、链表、堆),在 uv_run 的各个阶段,libuv 依赖这些结构完成工作中,下面将逐个来介绍一下都有哪些关联以及都是什么用途。
在 uv_loop_t 中,有多个相关 handle 队列:
uv_idle_t uv_check_t uv_check_t
https://github.com/libuv/libuv/blob/v1.28.0/include/uv/unix.h#L231
1 |
uv_idle_t还会被插入到loop->idle_handles队列头部,队列节点为handle->queue;uv_check_t还会被插入到loop->check_handles队列头部,队列节点为handle->queue;uv_check_t还会被插入到loop->prepare_handles队列头部,队列节点为handle->queue。
队列初始化:
https://github.com/libuv/libuv/blob/v1.28.0/src/unix/loop.c#L29
1 | int uv_loop_init(uv_loop_t* loop) { |
队列插入节点:
https://github.com/libuv/libuv/blob/v1.28.0/src/unix/loop-watcher.c#L24
1 |
|
uv_async_t
1 |
uv_async_t还会被插入到loop->async_handles队列尾部,队列节点为handle->queue
队列初始化:
https://github.com/libuv/libuv/blob/v1.x/src/unix/loop.c#L29
1 | int uv_loop_init(uv_loop_t* loop) { |
队列插入节点:
https://github.com/libuv/libuv/blob/v1.x/src/unix/async.c#L40
1 | int uv_async_init(uv_loop_t* loop, uv_async_t* handle, uv_async_cb async_cb) { |
uv_process_t
1 |
uv_process_t 还会被插入到 loop->process_handles 队列尾部,队列节点为 handle->queue。
队列初始化:
https://github.com/libuv/libuv/blob/v1.x/src/unix/loop.c#L29
1 | int uv_loop_init(uv_loop_t* loop) { |
队列插入节点:
https://github.com/libuv/libuv/blob/v1.28.0/src/unix/process.c#L410
1 | int uv_spawn(uv_loop_t* loop, |
uv_timer_t
1 |
uv_timer_t 还会被插入到 loop->timer_heap 堆(最小堆)中,堆节点为 handle->heap_node。
以上这些针对于不同类型的队列/链表/堆结构,都是为了方便的找到并统一处理相同类型的 handle。除了以上这些类型的 handle 之外,在 loop 上并没有这种特定类型的直接入口,而是通过其他链间接访问到 handle 的。
uv__io_t
在 loop 中,存在一个 watchers 数组,这个数组的每一项都是一个指向 uv__io_t 结构的指针,uv__io_t 是一个I/O观察者被内嵌到多个IO相关的 handle 结构中,所以所有的内嵌I/O观察者的 handle 都通过 watchers 被关联到 loop 上了。uv__io_t 存在多个子类型,这些子类型都可以被放到 watchers 数组中。
另外,还存在两个I/O观察者队列:
watcher_queue:所有的IO观察者都会被插入到这个队列中。pending_queue:所有的挂起IO观察者都会被插入到这个队列中。
uv__io_t 相关字段声明:
https://github.com/libuv/libuv/blob/v1.28.0/include/uv/unix.h#L218
1 |
uv__work
uv__work 是 libuv 中任务的抽象,任务有线程池处理,任务在发起 uv_work_t request 时创建。
在 loop 中,存在一个任务队列,这个队列中记录了所以经线程池处理完成的任务,队列入口为:loop->wq
https://github.com/libuv/libuv/blob/v1.28.0/include/uv/unix.h#L218
1 |
队列初始化:
https://github.com/libuv/libuv/blob/view-v1.28.0/src/unix/loop.c#L29
1 | int uv_loop_init(uv_loop_t* loop) { |
任务被处理完成或者任务取消后,都会被插入到该队里中。
closing_handles
Close 中提到过 closing_handles 关联了所有正在关闭的 handle,这也是一个关联的入口。
handle 可能同时存在于 loop->handle_queue 队列、loop->closing_handles 链表 以及 其他某一个特定类型的 handle 队列中。
通过上面的描述,可以在大脑中勾勒出一幅数据结构图。
除了 通过 loop 可以找到每一个 Handle,每一个 Handle 也可以通过其 loop 字段找到其所在的 loop。
以上这些 handle 通过各种方式关联到 loop 上除了 loop 作为资源的统一入口需要管理注册记录所有资源外,还需要使 事件循环 在运行的时候,能够方便的高效的处理这些 handle。
Request
Requests 一般代表一个短生命周期的操作,有些 Request 需要通过在 Handle 上执行,有些 Request 则可以直接执行。
在 libuv 中,有如下 Request:
1 | /* Request types. */ |
uv_getaddrinfo_t uv_getnameinfo_t uv_fs_t uv_work_t 可以直接执行,不依赖于 handle,直接关联到 loop 上。
uv_connect_t uv_write_t uv_udp_send_t uv_shutdown_t 都是跟读写相关的 request,其操作依赖于 uv_stream_t,也就是这些操作作用于 uv_stream_t,这些 request 通过 handle 关联到 loop 上。
同 handle 相似,request 也有一个基础结构 uv_req_s,其他 request 都通过组合复用 uv_req_s 的字段。
uv_req_s 结构定义如下:
1 |
|
request 并不需要在用户代码中显示的初始化,初始过程在相关的实现代码中由核心处理,通用的初始化部分 由 uv__req_init 函数处理,如下代码实现:
https://github.com/libuv/libuv/blob/v1.28.0/src/uv-common.h#L310
1 |
1 |
https://github.com/libuv/libuv/blob/v1.28.0/src/uv-common.h#L205
1 |
uv__req_init 初始化了 request 的类型,并通过 uv__req_register 更新 request 的引用计数,也可以通过 uv__req_unregister 反注册。
request 的更多内容,将在后续的分析中结合相关功能继续介绍。