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機制。他是對操作系統的封裝,但是也加入了自己的一些邏輯。文中有很多地方沒有展開分析,是因爲在之前的文章中已經分析過了很多次。如果有疑問可以留言。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章