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通过uv__io_start注册到epoll中,当有文件变化的时候,就会执行回调uv__inotify_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