定时器: Libuv 中的 timer

1,502 阅读3分钟

在看 Node http 模块文档的时候, 才留意到 server.timeout 这个属性, 本想简单介绍一下, 但是在梳理过后发现关于 timeout 有庞大的内容支撑: server.timout -> node core timers -> uv timers -> linux msleep/hrtimer -> clocksource -> tsc -> cmos rtc -> clock cycle, 所以拆分成几部分大致做下介绍, 期望定时器系列结束之后, noder 能够大致明白: clock cycle 是如何驱动 linux 的 msleep/hrtimer; linux 的 timers 与 uv timers 的关系; node timers 与 uv timers的关系。

Nodejs 的timer能力是依赖于Libuv提供的能力,那Libuv的timer是本身提供了哪些能力?又依赖于Linux哪些能力呢?

数据结构

在Linux中,与timer相关的数据结构有用于高精度定时器的红黑树和用于低精度的分桶-链表, Libuv中与定时器相关的数据结构是最小堆,其实Nodejs中与定时器相关的数据结构也是最小堆和双向链表

最小堆的特点是获取最小值的时间复杂度为O(1),可以快速获取总多定时器中的最小值。在 Linux 中将最小值的cycles设置到APIC中,剩下的就是坐等被调用,那libuv 是如何处理的呢?

工作原理

Linux提供了一种同步非阻塞的系统调用函数epoll-epoll(fd, type, timeout),可以在指定时间到期后继续执行。Libuv中关于timer的大概执行原理如下:

const minHeap = new MinHeap(); // 小头堆
// Timer(timeout, handler)
const timer1 = new Timer(10s, () => {print('时间到')}); // 10秒定时器
const timer2 = new Timer(100s, () => {print('时间到')}); // 100秒定时器
minHeap.insert(timer1).insert(timer2); // 压入小头堆

while(minHeap.isEmpty()) {
	const minTimer = minHeap.peek();// 获取最小堆里的最小时间
	epoll(..., minTimer.timeout);// 同步非阻塞调用
	minTimer.handler(); // 执行定时任务
	minHeap.remove();// 删除最小值
} // 直到定时器最小堆为空, 退出执行

每次循环取出需要等待的最小值,然后调用同步非阻塞系统调用函数,等待超时后继续执行,然后执行定时回调, 再次进入循环。由此可见 libuv 提供数据结构和业务逻辑,Linux 提供了同步非阻塞系统调用。最神秘的还是epoll调用,那它是如何实现的呢?

调试uv

在阅读源码的过程中,亲自调试一次可以起到事半功倍的效果,因为个人在编译的过程中也踩了很多坑,也简单记录下, 所以暂且放下 epoll 黑盒子,先说下调试过程。

准备程序 test.c

#include "stdio.h"
#include "uv.h"

static int timer_cb_called;

static void timer_cb(uv_timer_t* handle) {
  timer_cb_called++;
}

int main() {
  uv_loop_t *loop = uv_default_loop();
  uv_timer_t timer_req1;
  uv_timer_t timer_req2;

  uv_timer_init(loop, &timer_req1);
  uv_timer_init(loop, &timer_req2);
  uv_timer_start(&timer_req1, timer_cb, 1e4, 0);
  uv_timer_start(&timer_req2, timer_cb, 1e6, 0);

  uv_run(loop, UV_RUN_DEFAULT);

编译 libuv

  1. ./configure --prefix=指定库文件安装的目录
  2. make
  3. make install

个人在调试过程中觉得第一步非常重要,不要使用默认目录

编译用户代码

gcc test.c -I ./include/ -L 指定库文件安装的目录 -luv -g -o test

个人把test.c放到了libuv目录下编译的

gdb 调试

gdb --args test

神秘的 epoll

epoll 怎么就这么🐮呢,以目前有限的知识大胆猜测,它应该也是利用了Linux的定時器,因为它的精度是ms,所以猜测应该利用了低精度定时器。由于篇(还)幅(没)有限(看),且等下篇。

关注我的微信公众号"SUNTOPO WLOG",欢迎留言讨论,我会尽可能回复,感谢您的阅读。