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

103 篇文章 9 订阅
本文详细介绍了Node.js中文件监听的实现,从低效的轮询方式到高效的inotify机制。通过分析Node.js的`fs.watch`接口,揭示了如何利用inotify在Linux系统下进行文件变化的实时监控。同时,深入探讨了Libuv如何封装inotify,创建inotify实例,并通过epoll监听文件变化,以及在事件触发时如何执行回调函数。
摘要由CSDN通过智能技术生成

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
在Linux下安装Node.js源码有多种方法。其中一种方法是通过建立软连接来使其可以在全局被访问到。首先,你需要使用以下命令建立软连接: ``` ln -s (自己存放nodejs的路径)nodejs/bin/node /usr/local/bin/ ln -s (自己存放nodejs的路径)nodejs/bin/npm /usr/local/bin/ ``` 这样就可以将Node.js的可执行文件和npm命令链接到/usr/local/bin/目录下,使其可以在全局被访问到。\[1\] 另一种方法是通过编译Node.js源码来安装Node.js。然而,在编译过程中可能会遇到各种编译错误问题,很难解决。因此,这种方法并不推荐。\[2\] 还有一种方式是使用包管理器来安装Node.js,比如使用yum install node或者apt-get install node命令来安装。但是需要注意的是,在Linux下默认源中可能没有最新版的Node.js程序,因此这种方式可能安装的是不是最新版的Node.js。\[3\] #### 引用[.reference_title] - *1* *2* [Linux服务器安装NodeJs简易方法](https://blog.csdn.net/weixin_44248258/article/details/124054432)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [linux下安装nodejs的方式](https://blog.csdn.net/u011296285/article/details/128370859)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值