libuv 源码分析1: loop和poll

libuv 源码分析1: loop和poll


0 背景

libuv是一个开源异步I/O库(Asynchronous I/O)。主页在这里libuv

应用案例:Nodejs . 比起libevent来说,比较年轻。

前提假设:本文假设你对unix上的套接字编程比较熟悉,熟悉阻塞/非阻塞套接字,了解select, poll, epoll。

先上一张libuv架构图:

libuv

在linux上,libuv是对epoll的封装;在windows上,是对完成端口的封装;在macOS/FreeBSD上,是对kqueue的封装。

本文不讨论libuv基本的用法,只是稍微分析一下源码。本文只分析loop, poll和tcp。tcp下一章再讲。

1. uv_run执行了些什么

uv_run是uv消息队列的入口函数,我们看一下里面执行了些什么:

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__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;
  }

  /* The if statement lets gcc compile it to a conditional store. Avoids
   * dirtying a cache line.
   */
  if (loop->stop_flag != 0)
    loop->stop_flag = 0;

  return r;
}

我们可以看到主要执行了以下几个步骤:

  1. uv__update_time: 更新时间
  2. uv__run_timers: 调用timer。
  3. uv__run_pending: 调用回调。通常来说回调一般在poll fd结束后就立即执行,但是总有例外:有一些I/O回调会延迟到下一次迭代中执行。那些被延迟的回调正是在这里执行
  4. uv__run_idle: 执行idle。查看uv__run_idle的实现,发现仅仅是对idle进行遍历、执行回调,并没有删除的操作,所以idle是每次while都执行的。idle名不副实。(除了idle, prepare, check,都是执行完一个删除一个。既然回调都执行完了,保存也肯定没有必要)
  5. uv__run_prepare: 执行prepare
  6. uv__io_poll(loop, timeout): 把新增的需要被监听的fd放到poll中;poll我们所关心的fd,注意有一个timeout。
  7. uv__run_check: 执行check
  8. uv__run_closing_handles: 执行close handle。

这和uv官网上贴的图是一致的:

loop

下面我们简要来看一下几个主要函数:

1.0 uv_handle_t

uv_handle_t: 一个handle, 是uv_tcp_t, uv_udp_t, uv_timer_t, uv_poll_t等的公共父类(通过包含uv_handle_t结构体的所有成员来达到继承,通过uv_handle_t.type达到辨别是哪个子类)。

对与uv_handle_t的任何子类的关闭,比如uv_tcp_t,都需要调用uv_close(uv_handle_t*, uv_close_cb)。其中uv_close_cb一般用于释放资源,比如我们的handle是通过malloc得到的,此时就要free。如果是在栈上分配的,就不能free,可以进行其他清理工作。特别是timer,不仅仅要stop,还要close。

1.1 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((struct heap*) &loop->timer_heap);
    if (heap_node == NULL)
      break;

    handle = container_of(heap_node, uv_timer_t, heap_node);
    if (handle->timeout > loop->time)
      break;

    uv_timer_stop(handle);
    uv_timer_again(handle);
    handle->timer_cb(handle);
  }
}

从一个小堆中依次取出node,如果超时了,执行cb,再把timer放回小堆里面;如果没超时,则break,后续的也不再检查(最小的都没超时,后面更大的也不可能超时)。

1.2 uv_run_pending

简单的从队列从取出,再自行回调:

while (!QUEUE_EMPTY(&pq)) {
    q = QUEUE_HEAD(&pq);
    QUEUE_REMOVE(q);
    QUEUE_INIT(q);
    w = QUEUE_DATA(q, uv__io_t, pending_queue);
    w->cb(loop, w, POLLOUT);
  }

1.3 uv__run_idle uv__run_prepare uv__run_check

同上。不过idle从队列取出后,会再放回去。所以idle每次都要手动的stop。我就碰到过一次,cpu占用率100%,检查后发现是idle没有暂停。

QUEUE_MOVE(&loop->name##_handles, &queue);                                \
    while (!QUEUE_EMPTY(&queue)) {                                            \
      q = QUEUE_HEAD(&queue);                                                 \
      h = QUEUE_DATA(q, uv_##name##_t, queue);                                \
      QUEUE_REMOVE(q);                                                        \
      QUEUE_INSERT_TAIL(&loop->name##_handles, q);                            \
      h->name##_cb(h);                                                        \
    }     

可以看到先把loop中的队列move到queue上,依次遍历queue每个handle后,再把遍历的handle放到loop中的队列。

uv__run_prepareuv__run_checkuv__run_idle都是一样的。因为它们的init, start, stop,run定义都是通过同一个宏实现的(所以prepare和check也要手动停止):

UV_LOOP_WATCHER_DEFINE(prepare, PREPARE)
UV_LOOP_WATCHER_DEFINE(check, CHECK)
UV_LOOP_WATCHER_DEFINE(idle, IDLE)

#define UV_LOOP_WATCHER_DEFINE(name, type)                                    \
 int uv_##name##_init(uv_loop_t* loop, uv_##name##_t* handle) {              \
   uv__handle_init(loop, (uv_handle_t*)handle, UV_##type);                   \
   handle->name##_cb = NULL;                                                 \
   return 0;                                                                 \
 }                                                                           \
 int uv_##name##_start(uv_##name##_t* handle, uv_##name##_cb cb) {           \
      ...                                                                    \
 }
 int uv_##name##_stop(uv_##name##_t* handle) {                               \
       ...                                                                      \
 }  
 void uv__run_##name(uv_loop_t* loop) {                                      \
      ...                                                                   \
 }                                                                           \
                                                                             \
 void uv__##name##_close(uv_##name##_t* handle) {                            \
   ...                                                                         \
 }

UV_LOOP_WATCHER_DEFINE定义了init, start, top, run函数,有兴趣的可以看一下

1.4 uv_backend_timeoutuv__io_poll

这个是监听文件描述符的函数,也就是调用epoll/kqueue/IOCP监听套接字的函数。

首先看一下它的timeout是怎么计算的:

int uv_backend_timeout(const uv_loop_t* loop) {
  if (loop->stop_flag != 0)
    return 0;

  if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop))
    return 0;

  if (!QUEUE_EMPTY(&loop->idle_handles))
    return 0;

  if (!QUEUE_EMPTY(&loop->pending_queue))
    return 0;

  if (loop->closing_handles)
    return 0;

  return uv__next_timeout(loop);
}

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((const struct heap*) &loop->timer_heap);
  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 diff;
}

  1. 没有活跃的handle,返回0. epoll中,timeout为0的意思是立即返回。
  2. idle或pending_queue不为空,返回0
  3. loop被关闭返会0
  4. timer队列为空,返回-1. epoll中,timeout为-1的意思是阻塞直到有fd有event产生
  5. timer队列不为空,如果timer超时了,返回0。否则timeout为从现在到最早timer要超时的时间。比如现在是19:00:00, timer最早超时未19:00:10,那么timeout为10s。

这也就解释了,为什么不手动stop idle,loop会一直转。就是因为这里io poll不等待,使得while一直空转。

下面看看真正的poll:


void uv__io_poll(uv_loop_t* loop, int timeout) {
    while (!QUEUE_EMPTY(&loop->watcher_queue)) {
        q = QUEUE_HEAD(&loop->watcher_queue);
        QUEUE_REMOVE(q);
        QUEUE_INIT(q);
        // ...
        uv__epoll_ctl(loop->backend_fd, UV__EPOLL_CTL_ADD, w->fd, &e)
        // ...
    }
    for (;;) {
        // ...
        nfds = uv__epoll_wait(loop->backend_fd, events, ARRAY_SIZE(events), timeout);
       // ...
       pe->events &= w->pevents | POLLERR | POLLHUP;
       w->cb(loop, w, pe->events);
   }
   // ...
}

int uv__epoll_pwait(int epfd, struct uv__epoll_event* events, int nevents, int timeout, uint64_t sigmask) {
#if defined(__NR_epoll_pwait)
  int result;
  result = syscall(__NR_epoll_pwait, epfd, events, nevents, timeout, &sigmask, sizeof(sigmask));
#if MSAN_ACTIVE
  if (result > 0)
    __msan_unpoison(events, sizeof(events[0]) * result);
#endif
  return result;
#else
  return errno = ENOSYS, -1;
#endif
}
  1. 先把watcher_queue中剩余的fd移出来并放到poll队列中;
  2. poll等待,如果在超时之前有fd返回,则调用对应fd的回调, 然后继续等待直到timeout超时。

uv__io_poll 中包含了非常精巧的一点:libuv中的timer是由poll等待fd的时间来达到的:如果还没达到timer超时,则继续等待;如果超时了,就不在等待,进入到loop下一个步骤当中。并不是调用了操作系统timer相关的API。

通过syscall调用epoll。我们调用系统函数,不一定要通过具体名字,可以通过syscall然后传入要调用的系统函数对应的id。

1.5 uv__run_closing_handles

遍历等待关闭的队列,关闭stream(包括tcp,pipe等)或者udp以及其他handle,调用handle对应的close_cb

总结:uv在linux上是对epoll的封装;idle、prepare、check要手动关闭,否则while会一直循环。

下面来整理一下loop的数据结构(挑选了几个比较重要的field,有删减):

struct uv_loop_s {
  int backend_fd;               // epoll_create返回的fd。最终针对这个fd进行epoll操作
  void* data;                   // 用户可以用这个字段来保存数据
  void* handle_queue[2];        // handle双链表。一切uv_handle_t子类(包括但不限于tcp, udp,pipe)实例的地址、地址、地址,都在这个双链表里面。为了简便,后面会忽略存储的是handle的地址。                                               
  void* watcher_queue[2];       // 用户在上一个迭代中新增加的需要监听的fd,这些fd还没有添加到epoll中。这些fd,将在这次迭代中被添加到epoll中
  uv__io_t** watchers;          // 保存我们监听的fd相关的数据结构(即uv__io_t,包括fd本身,callback回调,epoll关心的events等)。是通过watchers[fd]来索引得到uv__io_t的。
  unsigned int nwatchers;       // watchers的大小,不是个数
  unsigned int nfds;            // watchers的个数
  struct {                                                                    
    void* min;                                                                
    unsigned int nelts;                                                       
  } timer_heap;                 // 小堆。用来保存timer
  uv_handle_t* closing_handles; // 待关闭队列。执行了uv_close后的handle被放入到这个队列
};

2. uv_poll_t

要对一个fd poll的话,主要有以下步骤:

  1. int uv_poll_init(uv_loop_t* loop, uv_poll_t* handle, int fd);
  2. int uv_poll_start(uv_poll_t* handle, int events, uv_poll_cb cb);
  3. int uv_poll_stop(uv_poll_t* handle);
  4. uv_close: 把handle放到loop->closing_handles队列中。

2.1 uv_poll_init

主要执行了一下几个步骤:

  1. uv__io_check_fd: 检测fd是否有效。先把fd放到epoll队列里(UV__EPOLL_CTL_ADD),再移出来(UV__EPOLL_CTL_DEL),出错了返回或者abort;
  2. uv__nonblock: 设置套接字为非阻塞
  3. uv__handle_init: 初始化handle。把handle插入到loop->handle_queue的队列尾端,设置handle类型为UV_POLL
  4. uv__io_init: 初始化 uv__io_t

2.2 uv_poll_start

主要执行了以下几个步骤:

  1. uv__io_start: 把fd对应的uv__io_t添加到watcher_queue队尾。更新loop->watchers, loop->nfds等等。如果loop->watchers太小,则先扩容再更新loop->watchers
  2. 把我们传入进来的回调赋值给handle->poll_cb。当有数据来了后,会调用我们这个回调。

2.2 uv_poll_stop

主要做了以下几个步骤:

  1. uv_poll_t对应的uv__io_t(因为是handle子类,所有有uv__io_t)从loop->watcher_queue中移出。

2.3 uv_close

  1. 把fd对应的handle放到loop->closing_handles中。uv会在下个循环close掉closing_handles中的fd,以及会释放相关资源。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,999评论 4 368
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,102评论 1 302
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,709评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,439评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,846评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,881评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,062评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,783评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,517评论 1 248
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,762评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,241评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,568评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,236评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,145评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,941评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,965评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,802评论 2 275

推荐阅读更多精彩内容

  • 名称 libev - 一个 C 编写的功能全面的高性能事件循环。 概要 示例程序 关于 libev Libev 是...
    hanpfei阅读 14,878评论 0 5
  • 前言从Node.js进入人们的视野时,我们所知道的它就由这些关键字组成 事件驱动、非阻塞I/O、高效、轻量,它在官...
    Www刘阅读 1,486评论 0 18
  • 观察者类型 This section describes each watcher in detail, but ...
    hanpfei阅读 955评论 0 1
  • 校方为了提高学生自身各项技能,一般会按照人才培养的规律与目标进行职业技术应用能力训练,也就是所谓的企业实...
    明明这么帅阅读 585评论 0 0
  • 从什么时候开始的呢 你变得啰嗦 一开口就能说个没完没了 像我孩提时那样 向你问长问短 向你说着无关紧要的人和事 从...
    读云轩札记阅读 226评论 0 1