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_init
uv__handle_start
uv__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
的更多内容,将在后续的分析中结合相关功能继续介绍。