Libuv源码分析 —— 3. 定时器

恭喜你,完成了上一节的学习,下面让咱们一起来分析一下定时器是如何实现的吧!

helloworld

  • 首先,让咱们来一个最简单的 Libuv 程序
    #include <stdio.h>
    #include <stdlib.h>
    #include <uv.h>
    
    int main() {
        uv_loop_t *loop = malloc(sizeof(uv_loop_t));
        // 初始化给定的 uv_loop_t 结构体
        uv_loop_init(loop);
    
        printf("Now quitting.\n");
        // 运行事件循环
        uv_run(loop, UV_RUN_DEFAULT);
    
        // 释放所有的内部循环资源
        uv_loop_close(loop);
        free(loop);
        return 0;
    }
    
  • 可以看到,一个最简单的程序是由初始化,运行,停止运行这三部分组成的。下面咱们来看看源码中是如何实现的呢?

源码分析

uv_loop_init
  • 可以想象到,在 uv_loop_init 就是对 uv_loop_t 结构体执行各种初始化操作
  • 因为 uv_loop_init 和 uv_loop_t 结构体代码有点长,现在给大家说了,有可能会把大家思维搞乱。而且 uv_loop_t 结构体中的内容在咱们之后的学习中会一直用到,所以这一部分内容咱们暂时就不进行分析了。
uv_run
  • 咱们来看一下 uv_run 中的部分源码
    int uv_run(uv_loop_t* loop, uv_run_mode mode) {
     int timeout;
     int r;
     int ran_pending;
    
     r = uv__loop_alive(loop);
     if (!r)
       uv__update_time(loop);
    
     while (r != 0 && loop->stop_flag == 0) {
       // 更新时间并开始倒计时 loop->time
       uv__update_time(loop);
       uv__run_timers(loop);
       // 处理挂起的handle
       ran_pending = uv__run_pending(loop);
       // 运行idle handle
       uv__run_idle(loop);
       // 运行prepare handle
       uv__run_prepare(loop);
    
       timeout = 0;
       if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
         timeout = uv__backend_timeout(loop);
    
       // 计算要阻塞的时间,开始阻塞
       uv__io_poll(loop, timeout);
       uv__metrics_update_idle_time(loop);
    
       // 程序执行到这里表示被唤醒了,被唤醒的原因可能是I/O可读可写、或者超时了,检查handle是否可以操作
       uv__run_check(loop);
       // 看看是否有close的handle
       uv__run_closing_handles(loop);
    
       // 单次模式 
       if (mode == UV_RUN_ONCE) {
         uv__update_time(loop);
         uv__run_timers(loop);
       }
    
       // handle保活处理
       r = uv__loop_alive(loop);
       if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
         break;
     }
    
     if (loop->stop_flag != 0)
       loop->stop_flag = 0;
    
     return r;
    }
    
  • 是不是感觉很复杂呢?没关系,uv_run中的内容不需要大家一次就看懂,咱会分为好多个部分来进行讲解。主要是需要大家了解这个函数中通过 while 循环来执行一些事件。这一讲,咱们来说一说 while 循环中的第一个部分 —— 定时器。

定时器

  • 在 Libuv 中,定时器是以最小堆来实现的,即最快过期的节点是根节点。
数据结构
struct uv_timer_s {
  // 句柄[handle]相关参数
  // uv_handle_t
  void* data;
  uv_loop_t* loop;
  uv_handle_type type;
  uv_close_cb close_cb;
  void* handle_queue[2];
  union {                                                                     
    int fd;                                                                   
    void* reserved[4];                                                        
  } u; 
  uv_handle_t* next_closing;
  unsigned int flags;

  // 定时器相关参数
  uv_timer_cb timer_cb;                                                       
  void* heap_node[3];                                                         
  uint64_t timeout;                                                           
  uint64_t repeat;                                                            
  uint64_t start_id; 
}
相关操作
uv_timer_init
  • 源码
    // 初始化uv_timer_t结构体
    int uv_timer_init(uv_loop_t* loop, uv_timer_t* handle) {
      // 初始化 handle
      uv__handle_init(loop, (uv_handle_t*)handle, UV_TIMER);
      // 初始化一些私有字段
      handle->timer_cb = NULL;
      handle->timeout = 0;
      handle->repeat = 0;
      return 0;
    }
    
uv_timer_start
  • 启动一个计时器
  • 源码
// 启动一个计时器
int uv_timer_start(uv_timer_t* handle,
                  uv_timer_cb cb,
                  uint64_t timeout,
                  uint64_t repeat) {
 uint64_t clamped_timeout;

 // 如果这个计时器句柄是关闭的或者回调函数为 NULL
 if (uv__is_closing(handle) || cb == NULL)
   return UV_EINVAL;

 // 重新执行start的时候先把之前的停掉
 if (uv__is_active(handle))
   uv_timer_stop(handle);

 // 超时时间,为绝对值
 // handle->loop->time 是在 uv_run 中每一次 while 循环开始的时间
 clamped_timeout = handle->loop->time + timeout;
 if (clamped_timeout < timeout)
   clamped_timeout = (uint64_t) -1;

 // 初始化回调,超时时间,是否重复计时,赋予一个独立无二的id
 handle->timer_cb = cb;
 handle->timeout = clamped_timeout;
 handle->repeat = repeat;
 handle->start_id = handle->loop->timer_counter++;

 // 插入最小堆
 heap_insert(timer_heap(handle->loop),
             (struct heap_node*) &handle->heap_node,
             timer_less_than);

 // 激活该 handle
 uv__handle_start(handle);

 return 0;
}

timer_heap

static struct heap *timer_heap(const uv_loop_t* loop) {
  #ifdef _WIN32
    return (struct heap*) loop->timer_heap;
  #else
    return (struct heap*) &loop->timer_heap;
  #endif
}

timer_less_than

// 两个节点的比较算法
// 按 timeout 从小到大排序,如果时间相等,按 start_id 从小到大排序
static int timer_less_than(const struct heap_node* ha,
                           const struct heap_node* hb) {
  const uv_timer_t* a;
  const uv_timer_t* b;

  a = container_of(ha, uv_timer_t, heap_node);
  b = container_of(hb, uv_timer_t, heap_node);

  if (a->timeout < b->timeout)
    return 1;
  if (b->timeout < a->timeout)
    return 0;

  /* Compare start_id when both have the same timeout. start_id is
   * allocated with loop->timer_counter in uv_timer_start().
   */
  return a->start_id < b->start_id;
}
  • start 函数首先初始化 handle 里的某些字段,包括超时回调,是否重复启动 定时器、超时的绝对时间等。接着把 handle 节点插入到最小堆中。最后给这 个 handle 打上标记,激活这个 handle
uv__run_timers
  • 找出已经超时的节点,并且执行里面的回调
  • 源码
    void uv__run_timers(uv_loop_t* loop) {
     struct heap_node* heap_node;
     uv_timer_t* handle;
    
     for (;;) {
       heap_node = heap_min(timer_heap(loop));
       if (heap_node == NULL)
         break;
    
       handle = container_of(heap_node, uv_timer_t, heap_node);
       // 每次取出的节点都是最小堆中最小的节点
       // 如果当前节点的时间大于当前时间则返回,说明后面的节点也没有超时
       if (handle->timeout > loop->time)
         break;
    
       // 移除该计时器节点,重新插入最小堆,如果设置了repeat的话
       uv_timer_stop(handle);
       // 执行超时回调
       uv_timer_again(handle);
       handle->timer_cb(handle);
     }
    }
    
  • libuv 在每次事件循环开始的时候都会缓存当前的时间,在整个一轮的事件循 环中,使用的都是这个缓存的时间。缓存了当前最新的时间后,就执行 uv__run_timers,该函数的逻辑很明了,就是遍历最小堆,找出当前超时的节 点。因为堆的性质是父节点肯定比孩子小。所以如果找到一个节点,他没有超 时,则后面的节点也不会超时。对于超时的节点就知道他的回调。执行完回调 后,还有两个关键的操作。第一就是 stop,第二就是 again
uv_timer_stop
  • 停止一个计时器
  • 源码
    int uv_timer_stop(uv_timer_t* handle) {
      if (!uv__is_active(handle))
        return 0;
    
      // 从最小堆中移除该计时器节点
      heap_remove(timer_heap(handle->loop),
                  (struct heap_node*) &handle->heap_node,
                  timer_less_than);
    
      // 清除激活状态和handle的active数减一
      uv__handle_stop(handle);
    
      return 0;
    }
    
  • 把 handle 从二叉堆中删除。并且取消激活状态
uv_timer_again
  • 重新启动一个计时器,需要设置repeat标记
    int uv_timer_again(uv_timer_t* handle) {
      if (handle->timer_cb == NULL)
        return UV_EINVAL;
    
      // 如果设置了repeat标记说明计时器是需要重复触发的
      if (handle->repeat) {
        // 先把旧的计时器节点从最小堆中移除,然后再重新开启一个计时器
        uv_timer_stop(handle);
        uv_timer_start(handle, handle->timer_cb, handle->repeat, handle->repeat);
      }
    
      return 0;
    }
    
  • 如果 handle 设置了 repeat 标记,则该 handle 在超时后,每 repeat 的时间 后,就会继续执行超时回调。对于 setInterval,就是超时时间是 x,每 x 的时 间后,执行回调。这就是 nodejs 里定时器的底层原理。但 nodejs 不是每次 调 setTimeout 的时候都往最小堆插入一个节点。nodejs 里,只有一个关于 uv_timer_s 的 handle。他在 js 层维护了一个数据结构,每次计算出最早到期 的节点,然后修改 handle 的超时时间
uv_timer_set_repeat
  • 设置计时器为重复触发
  • 源码
    void uv_timer_set_repeat(uv_timer_t* handle, uint64_t repeat) {
     handle->repeat = repeat;
    }
    
uv_timer_get_repeat
  • 获取计时器的状态——是否为重复触发
  • 源码
    uint64_t uv_timer_get_repeat(const uv_timer_t* handle) {
     return handle->repeat;
    }
    
uv_timer_get_due_in
  • 计时器还有多长时间到时
    uint64_t uv_timer_get_due_in(const uv_timer_t* handle) {
     if (handle->loop->time >= handle->timeout)
       return 0;
    
     return handle->timeout - handle->loop->time;
    }
    
timer 在 uv_run 中的使用
  • 咱们先来看一下 timer 中的函数在 uv_run 中是如何使用的
    int uv_run(uv_loop_t* loop, uv_run_mode mode) {
     int timeout;
     int r;
     int ran_pending;
    
     r = uv__loop_alive(loop);
     if (!r)
       uv__update_time(loop);
    
     while (r != 0 && loop->stop_flag == 0) {
       // 更新当前时间
       uv__update_time(loop);
       // 在这里找出超时的节点,运行里面的回调
       uv__run_timers(loop);
       ran_pending = uv__run_pending(loop);
       uv__run_idle(loop);
       uv__run_prepare(loop);
       
       // ———————————— 下面来讲解这一段 ————————————
       timeout = 0;
       if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
         timeout = uv__backend_timeout(loop);
    
       // 计算要阻塞的时间,开始阻塞
       uv__io_poll(loop, timeout);
       // ——————————————————————————————————————————
       
       uv__metrics_update_idle_time(loop);
       uv__run_check(loop);
       uv__run_closing_handles(loop);
    
       if (mode == UV_RUN_ONCE) {
         uv__update_time(loop);
         uv__run_timers(loop);
       }
    
       r = uv__loop_alive(loop);
       if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
         break;
     }
    
     if (loop->stop_flag != 0)
       loop->stop_flag = 0;
    
     return r;
    }
    
  • 首先咱们看一下 epoll_wait 函数,uv__io_poll 就是 epoll_wait 的封装
    #include <sys/epoll.h>
    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
        events:    用来存内核得到事件的集合,
        maxevents: 告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,
        timeout:   是超时时间
                    -1: 阻塞
                     0: 立即返回,非阻塞
                    >0: 指定毫秒
        返回值: 成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1
    
  • 可以看到里面有一个超时时间的参数。意思就是是用户拿一次数据可以等待的时间。 一般我们去使用epoll的时候,如果取不到东西,当然可以无限等待,所以我们平时可能会配成 -1,但是如果在网络库中使用 -1,有可能会造成阻塞,效率大大降低,所以我们要计算出一次最多可以阻塞的时常。
  • 计算这个时长的时候,用到了 uv__backend_timeout 函数,所以下面咱们来看一下是如何计算出这个超时时间的
    // 从 `timer_heap`中得到最小超时的时间, 从而计算出下一次的超时时间
    // 阻塞的时长就是最快到期的定时器节点的时长
    int uv__next_timeout(const uv_loop_t* loop) {
      const struct heap_node* heap_node;
      const uv_timer_t* handle;
      uint64_t diff;
    
      heap_node = heap_min(timer_heap(loop));
      if (heap_node == NULL)
        return -1; /* block indefinitely */
    
      handle = container_of(heap_node, uv_timer_t, heap_node);
      if (handle->timeout <= loop->time)
        return 0;
    
      diff = handle->timeout - loop->time;
      if (diff > INT_MAX)
        diff = INT_MAX;
    
      return (int) diff;
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值