专栏首页原创分享libuv之inotify源码分析

libuv之inotify源码分析

inotify是linux系统提供用于监听文件系统的机制。inotify机制的逻辑大致是 1 init_inotify创建一个inotify机制的实例,返回一个文件描述符。类似epoll。 2 inotify_add_watch往inotify实例注册一个需监听的文件(inotify_rm_watch是移除)。 3 read((inotify实例对应的文件描述符, &buf, sizeof(buf))),如果没有事件触发,则阻塞(除非设置了非阻塞)。否则返回待读取的数据长度。buf就是保存了触发事件的信息。 libuv在inotify机制的基础上做了一层封装。 今天分析一下libuv中的实现。我们从一个使用例子开始。

int main(int argc, char **argv) {
    // 实现循环核心结构体loop
    loop = uv_default_loop();
    fprintf(stderr, "Adding watch on %s\n", argv[0]);
    uv_fs_event_t *fs_event_req = malloc(sizeof(uv_fs_event_t));
    // 初始化fs_event_req结构体的类型为UV_FS_EVENT
    uv_fs_event_init(loop, fs_event_req);
    // argv[argc]是文件路径,uv_fs_event_start向底层注册监听文件argv[argc],cb是事件触发时的回调
    uv_fs_event_start(fs_event_req, cb, argv[argc], UV_FS_EVENT_RECURSIVE);
    // 开启事件循环
    return uv_run(loop, UV_RUN_DEFAULT);
}

libuv在第一次监听文件的时候,会创建一个inotify实例。

static int init_inotify(uv_loop_t* loop) {
  int err;
  // 初始化过了则直接返回     
  if (loop->inotify_fd != -1)
    return 0;
  // 调用操作系统的inotify_init函数申请一个inotify实例,并设置UV__IN_NONBLOCK,UV__IN_CLOEXEC标记
  err = new_inotify_fd();
  if (err < 0)
    return err;
  // 记录inotify实例对应的文件描述符
  loop->inotify_fd = err;
  // inotify_read_watcher是一个io观察者,uv__io_init设置io观察者的文件描述符(待观察的文件)和回调
  uv__io_init(&loop->inotify_read_watcher, uv__inotify_read, loop->inotify_fd);
  // 往libuv中注册该io观察者,感兴趣的事件为可读
  uv__io_start(loop, &loop->inotify_read_watcher, POLLIN);

  return 0;
}

分析完libuv申请inotify实例的逻辑,我们回到main函数看看uv_fs_event_start函数。

uv_fs_event_start(fs_event_req, cb, argv[argc], UV_FS_EVENT_RECURSIVE);

用户使用uv_fs_event_start函数来往libuv注册一个待监听的文件。我们看看实现。

int uv_fs_event_start(uv_fs_event_t* handle,
                      uv_fs_event_cb cb,
                      const char* path,
                      unsigned int flags) {
  struct watcher_list* w;
  int events;
  int err;
  int wd;

  if (uv__is_active(handle))
    return UV_EINVAL;
  // 申请一个inotify实例
  err = init_inotify(handle->loop);
  if (err)
    return err;
  // 监听的事件
  events = UV__IN_ATTRIB
         | UV__IN_CREATE
         | UV__IN_MODIFY
         | UV__IN_DELETE
         | UV__IN_DELETE_SELF
         | UV__IN_MOVE_SELF
         | UV__IN_MOVED_FROM
         | UV__IN_MOVED_TO;
  // 调用操作系统的函数注册一个待监听的文件,返回一个对应于该文件的id
  wd = uv__inotify_add_watch(handle->loop->inotify_fd, path, events);
  if (wd == -1)
    return UV__ERR(errno);
  // 判断该文件是不是已经注册过了
  w = find_watcher(handle->loop, wd);
  // 已经注册过则跳过插入的逻辑
  if (w)
    goto no_insert;
  // 还没有注册过则插入libuv维护的红黑树
  w = uv__malloc(sizeof(*w) + strlen(path) + 1);
  if (w == NULL)
    return UV_ENOMEM;

  w->wd = wd;
  w->path = strcpy((char*)(w + 1), path);
  QUEUE_INIT(&w->watchers);
  w->iterating = 0;
  // 插入libuv维护的红黑树,inotify_watchers是根节点
  RB_INSERT(watcher_root, CAST(&handle->loop->inotify_watchers), w);

no_insert:
  // 激活该handle
  uv__handle_start(handle);
  // 同一个文件可能注册了很多个回调,w对应一个文件,注册在用一个文件的回调排成队
  QUEUE_INSERT_TAIL(&w->watchers, &handle->watchers);
  // 保存信息和回调
  handle->path = w->path;
  handle->cb = cb;
  handle->wd = wd;

  return 0;
}

我们先看一下架构图,然后再来具体分析。

下面我们逐步分析上面的函数逻辑。 1 如果是首次调用该函数则新建一个inotify实例。并且往libuv插入一个观察者io,libuv会在poll io阶段注册到epoll中。 2 往操作系统注册一个待监听的文件。返回一个id。 3 libuv判断该id是不是在自己维护的红黑树中。不在红黑树中,则插入红黑树。返回一个红黑树中对应的节点。把本次请求的信息封装到handle中(回调时需要)。然后把handle插入刚才返回的节点的队列中。见上图。 这时候注册过程就完成了。libuv在poll io阶段如果检测到有文件发生变化,则会执行回调uv__inotify_read。

static void uv__inotify_read(uv_loop_t* loop,
                             uv__io_t* dummy,
                             unsigned int events) {
  const struct uv__inotify_event* e;
  struct watcher_list* w;
  uv_fs_event_t* h;
  QUEUE queue;
  QUEUE* q;
  const char* path;
  ssize_t size;
  const char *p;
  /* needs to be large enough for sizeof(inotify_event) + strlen(path) */
  char buf[4096];
  // 一次可能没有读完
  while (1) {
    do
      // 读取触发的事件信息,size是数据大小,buffer保存数据
      size = read(loop->inotify_fd, buf, sizeof(buf));
    while (size == -1 && errno == EINTR);
    // 没有数据可取了
    if (size == -1) {
      assert(errno == EAGAIN || errno == EWOULDBLOCK);
      break;
    }
    // 处理buffer的信息
    for (p = buf; p < buf + size; p += sizeof(*e) + e->len) {
      // buffer里是多个uv__inotify_event结构体,里面保存了事件信息和文件对应的id(wd字段)
      e = (const struct uv__inotify_event*)p;

      events = 0;
      if (e->mask & (UV__IN_ATTRIB|UV__IN_MODIFY))
        events |= UV_CHANGE;
      if (e->mask & ~(UV__IN_ATTRIB|UV__IN_MODIFY))
        events |= UV_RENAME;
      // 通过文件对应的id(wd字段)从红黑树中找到对应的节点
      w = find_watcher(loop, e->wd);

      path = e->len ? (const char*) (e + 1) : uv__basename_r(w->path);
      w->iterating = 1;
      // 把红黑树中,wd对应节点的handle队列移到queue变量,准备处理
      QUEUE_MOVE(&w->watchers, &queue);
      while (!QUEUE_EMPTY(&queue)) {
          // 头结点
        q = QUEUE_HEAD(&queue);
        // 通过结构体偏移拿到首地址
        h = QUEUE_DATA(q, uv_fs_event_t, watchers);
        // 从处理队列中移除
        QUEUE_REMOVE(q);
        // 放回原队列
        QUEUE_INSERT_TAIL(&w->watchers, q);
        // 执行回调
        h->cb(h, path, events, 0);
      }
    }
  }
}

uv__inotify_read函数的逻辑就是从操作系统中把数据读取出来,这些数据中保存了哪些文件触发了用户感兴趣的事件。然后遍历每个触发了事件的文件。从红黑树中找到该文件对应的红黑树节点。再取出红黑树节点中维护的一个handle队列,最后执行handle队列中每个节点的回调。 总结:本文介绍了libuv中的inotify机制。他是对操作系统的封装,但是也加入了自己的一些逻辑。文中有很多地方没有展开分析,是因为在之前的文章中已经分析过了很多次。如果有疑问可以留言。

本文分享自微信公众号 - 编程杂技(theanarkh),作者:theanarkh

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-04-22

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • libuv线程池和主线程通信原理

    代码很简单,就是设置一下async_io_watcher的fd和回调,在epoll_wait返回的时候用到。再看uv__io_start。

    theanarkh
  • libuv之async.c源码解析

    libuv的async.c实现了线程和主线程的通信。在uv_loop_init函数中对async进行初始化。

    theanarkh
  • libuv源码解析之数据结构全景图

    theanarkh
  • 【玩转腾讯云】有一台腾讯云可以用来做什么

    腾讯云还推出针对不同阶段企业或个人的体验活动,海量的云资源可以免费试用,即使是个人用户也可以获得很多长期免费云产品。

    凝神长老
  • 音视频直播技术--视频画中画

    大家好,今天我为大家介绍一下如何在Android系统下实现实时直播互动中的多视频展示功能。 我们现在都清楚实时直播互动中多路视频的展示是目前比较流行的趋势,因为...

    音视频_李超
  • Shader实例(流光实现)

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明...

    bering
  • 解释一下MyBatis中命名空间(namespace)的作用

    在大型项目中,可能存在大量的SQL语句,这时候为每个SQL语句起一个唯一的标识(ID)就变得并不容易了。为了解决这个问题,在MyBatis中,可以为每个映射文件...

    唐怀瑟
  • #7 Python代码调试

    Python已经学了这么久了,你现在已经长大了,该学会自己调试代码了!相信大家在编写程序过程中会遇到大量的错误信息,我也不例外的啦~遇到这些问题该怎么解决呢?使...

    py3study
  • 12306自动刷票下单-查票

    上篇写了12306登录,隔了快一个月了,才准备动手写下单篇,真的要非常感谢博客园的 Asimple朋友,如果不是看到你的留言,我几乎都忘了要写下篇了,这一点在简...

    星星在线
  • Kafka 单节点单Kafka Broker集群

    kongxx

扫码关注云+社区

领取腾讯云代金券