Alby's blog

世上没有巧合,只有巧合的假象。

0%

(草稿)Libuv 源码分析(9):I/O ( 或 event ) 循环的结构( struct )

一、概述

Libuv 官方文档( https://github.com/libuv/libuv/tree/v1.x/docs )对各种 C 结构及操作对应结构的 API 也有清晰的描述。本文首先简单分析 uv_loop_s 结构的定义。接着深入源码分析了 uv_loop_XXXuv__loop_XXX 系列函数。

二、uv_loop_s 结构定义

uv_loop_s 是一个 C 结构,其作为 Libuv 的核心结构,几乎无处不在无处不用。
其定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
struct uv_loop_s {
/* User data - use this for whatever. */
void* data;
/* Loop reference counting. */
unsigned int active_handles;
void* handle_queue[2];
void* active_reqs[2];
/* Internal flag to signal loop stop. */
unsigned int stop_flag;
void* reserved[4];
UV_LOOP_PRIVATE_FIELDS
};

单从定义是无法完全分析出结构各字段的作用的,需结合使用结构的代码进行分析。

分析结构的字段,首先明确字段的作用是什么,在哪些代码对其进行了赋值或修改;如果是容器(数组、队列或堆等),其中的元素是什么,必要的话看其中的元素是如何增删的。

1、 data: 用户数据

较早的 Libuv 是完全不用 data 字段的,后来开发人员认为这种情况下可能对调试有帮助:在调试模式下,使用 uv_loop_close 函数关闭不清除 data ,在再次使用 uv_loop_init 函数进行重新初始化的时候沿用之前的 data 。

2、 handle_queue: Handle 队列

Handle 队列元素数总是小于或等于 active_handles

排除 uv_loop_suv_handle_suv_stream_t ,Libuv 当期有 14 种 Handle ,其中 3 种是 Stream。

① 入列

绝大部分 Handle 的初始化函数格式是 uv_XXX_init ,如 uv_timer_init。这些初始化函数内部使用的宏 uv__handle_init会将 Handle 加入到队列的尾部:

1
2
3
4
5
6
7
8
9
10
#define uv__handle_init(loop_, h, type_)                                      \
do { \
(h)->loop = (loop_); \
(h)->type = (type_); \
(h)->flags = UV__HANDLE_REF; /* Ref the loop when active. */ \
QUEUE_INSERT_TAIL(&(loop_)->handle_queue, &(h)->handle_queue); \
uv__handle_platform_init(h); \
} \
while (0)

uv_tcp_s, uv_pipe_suv_tty_s 三个 Stream 是在 uv__stream_init 函数里进行初始化的。特别的,对于 uv_process_s , 没有 uv_process_init 函数,是在 uv_spawn 函数里进行初始化的。

uv_walk 函数会针对每个 Handle 分别添加一个回调函数,回调函数本身也是加入到队列的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void uv_walk(uv_loop_t* loop, uv_walk_cb walk_cb, void* arg) {
QUEUE queue;
QUEUE* q;
uv_handle_t* h;

QUEUE_MOVE(&loop->handle_queue, &queue);
while (!QUEUE_EMPTY(&queue)) {
q = QUEUE_HEAD(&queue);
h = QUEUE_DATA(q, uv_handle_t, handle_queue);

QUEUE_REMOVE(q);
QUEUE_INSERT_TAIL(&loop->handle_queue, q);

if (h->flags & UV__HANDLE_INTERNAL) continue;
walk_cb(h, arg);
}
}

② 出列

uv__finish_close 会将 Handle 移出队列。调用链:

uv_run -> uv__run_closing_handles -> uv__finish_close

3、 active_handles: 事件循环中活动的 Handle 计数

Handle 在循环中不总是”活动”的,因为 Handle 有三种状态:未初始化、已初始化未激活、已激活。通常需要 start 或类似的操作进行激活。 active_handles 的值总是小于或等于循环中的 Handle 数。(扩展:为什么 Handle 使用一个中间的“未激活”的状态,而不像 Request 那样?)

① 增加 Handle 计数的调用链

01

图1: 增加 Handle 计数的调用链。

② 减少 Handle 计数的调用链

02

图2: 减少 Handle 计数的调用链。

4、 active_reqs: 活动的 Request 队列

不像 Handle ,Request 在循环中总是”活动”的。对 Request 进行初始化时,会将其加入循环中。在使用过程中发生错误,或者使用完成后再从循环中删除。
不是所有的 Request 都关联了一个 Handle。(扩展:具体哪些 Request 与 Handle 无关。)

5、 stop_flag: 用于指示循环停止的内部标记

6、 reserved: 保留字段

7、 UV_LOOP_PRIVATE_FIELDS: 宏,定义平台相关的私有字段集

主要是为了跨 UNIX 平台和 Windows 平台。针对 Linux 和 macOS 等 UNIX (或类 UNIX ) 平台,使用 UV_LOOP_PRIVATE_FIELDS 内部的 UV_PLATFORM_LOOP_FIELDS 宏来定义平台相关的字段集。

三、UV_LOOP_PRIVATE_FIELDS

UV_LOOP_PRIVATE_FIELDS 宏定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#define UV_LOOP_PRIVATE_FIELDS                                                \
unsigned long flags; \
int backend_fd; \
void* pending_queue[2]; \
void* watcher_queue[2]; \
uv__io_t** watchers; \
unsigned int nwatchers; \
unsigned int nfds; \
void* wq[2]; \
uv_mutex_t wq_mutex; \
uv_async_t wq_async; \
uv_rwlock_t cloexec_lock; \
uv_handle_t* closing_handles; \
void* process_handles[2]; \
void* prepare_handles[2]; \
void* check_handles[2]; \
void* idle_handles[2]; \
void* async_handles[2]; \
void (*async_unused)(void); /* TODO(bnoordhuis) Remove in libuv v2. */ \
uv__io_t async_io_watcher; \
int async_wfd; \
struct { \
void* min; \
unsigned int nelts; \
} timer_heap; \
uint64_t timer_counter; \
uint64_t time; \
int signal_pipefd[2]; \
uv__io_t signal_io_watcher; \
uv_signal_t child_watcher; \
int emfile_fd; \
UV_PLATFORM_LOOP_FIELDS \

如果在分析具体的结构时遇到宏,可以先预处理展开宏,如:

1
gcc -E src/uv-common.c -o src/uv-common.i -Iinclude -Isrc -Isrc/unix

然后在预处理结果文件中定位到 struct uv_loop_s 的定义处,找到UV_LOOP_PRIVATE_FIELDS 宏展开的代码( UV_LOOP_PRIVATE_FIELDS 宏嵌套不深,预处理后的代码和定义几乎对应的。另,这里包含了 macOS 上 UV_PLATFORM_LOOP_FIELDS 宏展开的代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// UV_LOOP_PRIVATE_FIELDS
unsigned long flags;
int backend_fd;
void* pending_queue[2];
void* watcher_queue[2];
uv__io_t** watchers;
unsigned int nwatchers;
unsigned int nfds;
void* wq[2];
uv_mutex_t wq_mutex;
uv_async_t wq_async;
uv_rwlock_t cloexec_lock;
uv_handle_t* closing_handles;
void* process_handles[2];
void* prepare_handles[2];
void* check_handles[2];
void* idle_handles[2];
void* async_handles[2];
void (*async_unused)(void);
uv__io_t async_io_watcher;
int async_wfd;
struct {
void* min;
unsigned int nelts;
} timer_heap;
uint64_t timer_counter;
uint64_t time;
int signal_pipefd[2];
uv__io_t signal_io_watcher;
uv_signal_t child_watcher;
int emfile_fd;

// UV_PLATFORM_LOOP_FIELDS (macOS)
uv_thread_t cf_thread;
void* _cf_reserved;
void* cf_state;
uv_mutex_t cf_mutex;
uv_sem_t cf_sem;
void* cf_signals[2];

1、 flags: 循环标记

目前仅支持唯一一个标记: UV_LOOP_BLOCK_SIGPROF 。如果启用该标记,则每次轮询时会阻止 SIGPROF 信号传递。
目的是使用取样探查器( sampling profile )时减少 kevent/epoll_pwait 等 I/O 复用函数的中断次数和随后使用 clock_gettime 系统调用的次数。
当启用该标记时,在 Linux 2.6.19 及以后, 将从 epoll_wait 切换到 epoll_pwait
在 Linux 2.6.19 以前和其他平台,使用 pthread_sigmask 阻止 SIGPROF 信号传递。

一般使用 uv_loop_configure 函数设置该标记。

2、 backend_fd: 后台文件描述符

实际上就是 kqueueepoll 等对象的文件描述符。

3、 pending_queue: 挂起的回调队列

大部分时候, I/O 回调在 I/O 轮询( poll )之后马上被调用,但有时回调会延迟到循环的下一次迭代,即保存至 pending_queue 中,具体见 uv__io_feeduv_run 函数调用 uv__run_pending 函数,后者调用上一次迭代产生的回调。

4、 watcher_queue: 监视器队列

该队列用于收集要监听的文件描述符和事件,最终会在调用 kevent/epoll_pwait 等 I/O 复用函数时使用。

5、 watchers: 监视器队列

6、 nwatchers: watchers 元素数加 2

当文件描述符被关闭后, 并且新的文件描述符 (同样数字) 在 kqueue/epoll/port 循环的回调中被打开,旧的事件可能在错误的监视器上调用回调函数。

检查在调用所有事件后是否更改了观察程序并使用了无效的相同的文件描述符。

备注:未完待续

参考资料