Nodejs 源码剖析 基于 inotify 的文件监听机制

发布于 2023-12-24 05:39:49 字数 8332 浏览 30 评论 0

Node.js 中实现了基于轮询的文件监听机制,基于轮询的监听其实效率是很低的,因为需要我们不断去轮询文件的元数据,如果文件大部分时间里都没有变化,那就会白白浪费 CPU。如果文件改变了会主动通知我们那就好了,这就是基于 inotify 机制的文件监听。Node.js 提供的接口是 watch。watch 的实现和 watchFile 的比较类似。

1.  function watch(filename, options, listener) {  
2.    // Don't make changes directly on options object  
3.    options = copyObject(options);  
4.    // 是否持续监听
5.    if (options.persistent === undefined) 
6.        options.persistent = true;  
7.      // 如果是目录,是否监听所有子目录和文件的变化
8.    if (options.recursive === undefined) 
9.        options.recursive = false;  
10.     // 有些平台不支持
11.   if (options.recursive && !(isOSX || isWindows))  
12.     throw new ERR_FEATURE_UNAVAILABLE_ON_PLATFORM('watch recursively');  
13.   if (!watchers)  
14.     watchers = require('internal/fs/watchers');  
15.     // 新建一个 FSWatcher 对象管理文件监听,然后开启监听
16.   const watcher = new watchers.FSWatcher();  
17.   watcher[watchers.kFSWatchStart](filename,  
18.                   options.persistent,  
19.                   options.recursive,  
20.                   options.encoding);  
21.   
22.   if (listener) {  
23.     watcher.addListener('change', listener);  
24.   }  
25.   
26.   return watcher;  
27. }

FSWatcher 函数是对 C++层 FSEvent 模块的封装。我们来看一下 start 函数的逻辑,start 函数透过 C++层调用了 Libuv 的 uv_fs_event_start 函数。在讲解 uv_fs_event_start 函数前,我们先了解一下 inotify 的原理和它在 Libuv 中的实现。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 机制的基础上做了一层封装。我们看一下 inotify 在 Libuv 的架构图如图所示。

我们再来看一下 Libuv 中的实现。我们从一个使用例子开始。

1.  int main(int argc, char **argv) {  
2.      // 实现循环核心结构体 loop  
3.      loop = uv_default_loop();   
4.      uv_fs_event_t *fs_event_req = malloc(sizeof(uv_fs_event_t));
5.      // 初始化 fs_event_req 结构体的类型为 UV_FS_EVENT  
6.      uv_fs_event_init(loop, fs_event_req);  
7.      /* 
8.        argv[argc]是文件路径,
9.        uv_fs_event_start 向底层注册监听文件 argv[argc],
10.       cb 是事件触发时的回调 
11.     */  
12.     uv_fs_event_start(fs_event_req, 
13.                           cb, 
14.                           argv[argc], 
15.                           UV_FS_EVENT_RECURSIVE);  
16.     // 开启事件循环  
17.     return uv_run(loop, UV_RUN_DEFAULT);  
18. }

Libuv 在第一次监听文件的时候(调用 uv_fs_event_start 的时候),会创建一个 inotify 实例。

1.  static int init_inotify(uv_loop_t* loop) {  
2.    int err;  
3.    // 初始化过了则直接返回       
4.    if (loop->inotify_fd != -1)  
5.      return 0;  
6.    /*
7.        调用操作系统的 inotify_init 函数申请一个 inotify 实例,
8.        并设置 UV__IN_NONBLOCK,UV__IN_CLOEXEC 标记  
9.    */
10.   err = new_inotify_fd();  
11.   if (err < 0)  
12.     return err;  
13.   // 记录 inotify 实例对应的文件描述符,一个事件循环一个 inotify 实例  
14.   loop->inotify_fd = err;  
15.   /*
16.       inotify_read_watcher 是一个 IO 观察者,
17.       uv__io_init 设置 IO 观察者的文件描述符(待观察的文件)和回调  
18.   */
19.   uv__io_init(&loop->inotify_read_watcher, 
20.                 uv__inotify_read, 
21.                 loop->inotify_fd);  
22.   // 往 Libuv 中注册该 IO 观察者,感兴趣的事件为可读  
23.   uv__io_start(loop, &loop->inotify_read_watcher, POLLIN);  
24.   
25.   return 0;  
26. }

Libuv 把 inotify 实例对应的 fd 通过 uvio_start 注册到 epoll 中,当有文件变化的时候,就会执行回调 uvinotify_read。分析完 Libuv 申请 inotify 实例的逻辑,我们回到 main 函数看看 uv_fs_event_start 函数。用户使用 uv_fs_event_start 函数来往 Libuv 注册一个待监听的文件。我们看看实现。

1.  int uv_fs_event_start(uv_fs_event_t* handle,  
2.                        uv_fs_event_cb cb,  
3.                        const char* path,  
4.                        unsigned int flags) {  
5.    struct watcher_list* w;  
6.    int events;  
7.    int err;  
8.    int wd;  
9.    
10.   if (uv__is_active(handle))  
11.     return UV_EINVAL;  
12.   // 申请一个 inotify 实例  
13.   err = init_inotify(handle->loop);  
14.   if (err)  
15.     return err;  
16.   // 监听的事件  
17.   events = UV__IN_ATTRIB  
18.          | UV__IN_CREATE  
19.          | UV__IN_MODIFY  
20.          | UV__IN_DELETE  
21.          | UV__IN_DELETE_SELF  
22.          | UV__IN_MOVE_SELF  
23.          | UV__IN_MOVED_FROM  
24.          | UV__IN_MOVED_TO;  
25.   // 调用操作系统的函数注册一个待监听的文件,返回一个对应于该文件的 id  
26.   wd = uv__inotify_add_watch(handle->loop->inotify_fd, path, events);  
27.   if (wd == -1)  
28.     return UV__ERR(errno);  
29.   // 判断该文件是不是已经注册过了  
30.   w = find_watcher(handle->loop, wd);  
31.   // 已经注册过则跳过插入的逻辑  
32.   if (w)  
33.     goto no_insert;  
34.   // 还没有注册过则插入 Libuv 维护的红黑树  
35.   w = uv__malloc(sizeof(*w) + strlen(path) + 1);  
36.   if (w == NULL)  
37.     return UV_ENOMEM;  
38.   
39.   w->wd = wd;  
40.   w->path = strcpy((char*)(w + 1), path);  
41.   QUEUE_INIT(&w->watchers);  
42.   w->iterating = 0;  
43.   // 插入 Libuv 维护的红黑树,inotify_watchers 是根节点  
44.   RB_INSERT(watcher_root, CAST(&handle->loop->inotify_watchers), w);  
45.   
46. no_insert:  
47.   // 激活该 handle  
48.   uv__handle_start(handle);  
49.   // 同一个文件可能注册了很多个回调,w 对应一个文件,注册在用一个文件的回调排成队  
50.   QUEUE_INSERT_TAIL(&w->watchers, &handle->watchers);  
51.   // 保存信息和回调  
52.   handle->path = w->path;  
53.   handle->cb = cb;  
54.   handle->wd = wd;  
55.   
56.   return 0;  
57. }

下面我们逐步分析上面的函数逻辑。 1 如果是首次调用该函数则新建一个 inotify 实例。并且往 Libuv 插入一个观察者 io,Libuv 会在 Poll IO 阶段注册到 epoll 中。 2 往操作系统注册一个待监听的文件。返回一个 id。 3 Libuv 判断该 id 是不是在自己维护的红黑树中。

不在红黑树中,则插入红黑树。返回一个红黑树中对应的节点。把本次请求的信息封装到 handle 中(回调时需要)。然后把 handle 插入刚才返回的节点的队列中。 这时候注册过程就完成了。Libuv 在 Poll IO 阶段如果检测到有文件发生变化,则会执行回调 uv__inotify_read。

1.  static void uv__inotify_read(uv_loop_t* loop,  
2.                               uv__io_t* dummy,  
3.                               unsigned int events) {  
4.    const struct uv__inotify_event* e;  
5.    struct watcher_list* w;  
6.    uv_fs_event_t* h;  
7.    QUEUE queue;  
8.    QUEUE* q;  
9.    const char* path;  
10.   ssize_t size;  
11.   const char *p;  
12.   /* needs to be large enough for sizeof(inotify_event) + strlen(path) */  
13.   char buf[4096];  
14.   // 一次可能没有读完  
15.   while (1) {  
16.     do  
17.       // 读取触发的事件信息,size 是数据大小,buffer 保存数据  
18.       size = read(loop->inotify_fd, buf, sizeof(buf));  
19.     while (size == -1 && errno == EINTR);  
20.     // 没有数据可取了  
21.     if (size == -1) {  
22.       assert(errno == EAGAIN || errno == EWOULDBLOCK);  
23.       break;  
24.     }  
25.     // 处理 buffer 的信息  
26.     for (p = buf; p < buf + size; p += sizeof(*e) + e->len) {  
27.       // buffer 里是多个 uv__inotify_event 结构体,里面保存了事件信息和文件对应的 id(wd 字段)  
28.       e = (const struct uv__inotify_event*)p;  
29.   
30.       events = 0;  
31.       if (e->mask & (UV__IN_ATTRIB|UV__IN_MODIFY))  
32.         events |= UV_CHANGE;  
33.       if (e->mask & ~(UV__IN_ATTRIB|UV__IN_MODIFY))  
34.         events |= UV_RENAME;  
35.       // 通过文件对应的 id(wd 字段)从红黑树中找到对应的节点  
36.       w = find_watcher(loop, e->wd);  
37.   
38.       path = e->len ? (const char*) (e + 1) : uv__basename_r(w->path);  
39.       w->iterating = 1;  
40.       // 把红黑树中,wd 对应节点的 handle 队列移到 queue 变量,准备处理  
41.       QUEUE_MOVE(&w->watchers, &queue);  
42.       while (!QUEUE_EMPTY(&queue)) {  
43.           // 头结点  
44.         q = QUEUE_HEAD(&queue);  
45.         // 通过结构体偏移拿到首地址  
46.         h = QUEUE_DATA(q, uv_fs_event_t, watchers);  
47.         // 从处理队列中移除  
48.         QUEUE_REMOVE(q);  
49.         // 放回原队列  
50.         QUEUE_INSERT_TAIL(&w->watchers, q);  
51.         // 执行回调  
52.         h->cb(h, path, events, 0);  
53.       }  
54.     }  
55.   }  
56. }

uv__inotify_read 函数的逻辑就是从操作系统中把数据读取出来,这些数据中保存了哪些文件触发了用户感兴趣的事件。然后遍历每个触发了事件的文件。从红黑树中找到该文件对应的红黑树节点。再取出红黑树节点中维护的一个 handle 队列,最后执行 handle 队列中每个节点的回调。

更多 Node.js 底层原理,参考 https://github.com/theanarkh/understand-nodejs

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

做个ˇ局外人

暂无简介

0 文章
0 评论
22 人气
更多

推荐作者

qq_E2Iff7

文章 0 评论 0

Archangel

文章 0 评论 0

freedog

文章 0 评论 0

Hunk

文章 0 评论 0

18819270189

文章 0 评论 0

wenkai

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文