Nodejs 源码剖析 基于 inotify 的文件监听机制
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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论