前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >【Nodejs源码剖析】基于inotify的文件监听机制

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

作者头像
theanarkh
发布2021-05-28 17:29:07
发布2021-05-28 17:29:07
1.2K00
代码可运行
举报
文章被收录于专栏:原创分享原创分享
运行总次数:0
代码可运行

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

代码语言:javascript
代码运行次数:0
运行
复制
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中的实现。我们从一个使用例子开始。

代码语言:javascript
代码运行次数:0
运行
复制
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实例。

代码语言:javascript
代码运行次数:0
运行
复制
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通过uv__io_start注册到epoll中,当有文件变化的时候,就会执行回调uv__inotify_read。分析完Libuv申请inotify实例的逻辑,我们回到main函数看看uv_fs_event_start函数。用户使用uv_fs_event_start函数来往Libuv注册一个待监听的文件。我们看看实现。

代码语言:javascript
代码运行次数:0
运行
复制
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。

代码语言:javascript
代码运行次数:0
运行
复制
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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-05-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程杂技 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档