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机制的基础上做了一层封装。 今天分析一下libuv中的实现。我们从一个使用例子开始。
int main(int argc, char **argv) {
// 实现循环核心结构体loop
loop = uv_default_loop();
fprintf(stderr, "Adding watch on %s\n", argv[0]);
uv_fs_event_t *fs_event_req = malloc(sizeof(uv_fs_event_t));
// 初始化fs_event_req结构体的类型为UV_FS_EVENT
uv_fs_event_init(loop, fs_event_req);
// argv[argc]是文件路径,uv_fs_event_start向底层注册监听文件argv[argc],cb是事件触发时的回调
uv_fs_event_start(fs_event_req, cb, argv[argc], UV_FS_EVENT_RECURSIVE);
// 开启事件循环
return uv_run(loop, UV_RUN_DEFAULT);
}
libuv在第一次监听文件的时候,会创建一个inotify实例。
static int init_inotify(uv_loop_t* loop) {
int err;
// 初始化过了则直接返回
if (loop->inotify_fd != -1)
return 0;
// 调用操作系统的inotify_init函数申请一个inotify实例,并设置UV__IN_NONBLOCK,UV__IN_CLOEXEC标记
err = new_inotify_fd();
if (err < 0)
return err;
// 记录inotify实例对应的文件描述符
loop->inotify_fd = err;
// inotify_read_watcher是一个io观察者,uv__io_init设置io观察者的文件描述符(待观察的文件)和回调
uv__io_init(&loop->inotify_read_watcher, uv__inotify_read, loop->inotify_fd);
// 往libuv中注册该io观察者,感兴趣的事件为可读
uv__io_start(loop, &loop->inotify_read_watcher, POLLIN);
return 0;
}
分析完libuv申请inotify实例的逻辑,我们回到main函数看看uv_fs_event_start函数。
uv_fs_event_start(fs_event_req, cb, argv[argc], UV_FS_EVENT_RECURSIVE);
用户使用uv_fs_event_start函数来往libuv注册一个待监听的文件。我们看看实现。
int uv_fs_event_start(uv_fs_event_t* handle,
uv_fs_event_cb cb,
const char* path,
unsigned int flags) {
struct watcher_list* w;
int events;
int err;
int wd;
if (uv__is_active(handle))
return UV_EINVAL;
// 申请一个inotify实例
err = init_inotify(handle->loop);
if (err)
return err;
// 监听的事件
events = UV__IN_ATTRIB
| UV__IN_CREATE
| UV__IN_MODIFY
| UV__IN_DELETE
| UV__IN_DELETE_SELF
| UV__IN_MOVE_SELF
| UV__IN_MOVED_FROM
| UV__IN_MOVED_TO;
// 调用操作系统的函数注册一个待监听的文件,返回一个对应于该文件的id
wd = uv__inotify_add_watch(handle->loop->inotify_fd, path, events);
if (wd == -1)
return UV__ERR(errno);
// 判断该文件是不是已经注册过了
w = find_watcher(handle->loop, wd);
// 已经注册过则跳过插入的逻辑
if (w)
goto no_insert;
// 还没有注册过则插入libuv维护的红黑树
w = uv__malloc(sizeof(*w) + strlen(path) + 1);
if (w == NULL)
return UV_ENOMEM;
w->wd = wd;
w->path = strcpy((char*)(w + 1), path);
QUEUE_INIT(&w->watchers);
w->iterating = 0;
// 插入libuv维护的红黑树,inotify_watchers是根节点
RB_INSERT(watcher_root, CAST(&handle->loop->inotify_watchers), w);
no_insert:
// 激活该handle
uv__handle_start(handle);
// 同一个文件可能注册了很多个回调,w对应一个文件,注册在用一个文件的回调排成队
QUEUE_INSERT_TAIL(&w->watchers, &handle->watchers);
// 保存信息和回调
handle->path = w->path;
handle->cb = cb;
handle->wd = wd;
return 0;
}
我们先看一下架构图,然后再来具体分析。
下面我们逐步分析上面的函数逻辑。 1 如果是首次调用该函数则新建一个inotify实例。并且往libuv插入一个观察者io,libuv会在poll io阶段注册到epoll中。 2 往操作系统注册一个待监听的文件。返回一个id。 3 libuv判断该id是不是在自己维护的红黑树中。不在红黑树中,则插入红黑树。返回一个红黑树中对应的节点。把本次请求的信息封装到handle中(回调时需要)。然后把handle插入刚才返回的节点的队列中。见上图。 这时候注册过程就完成了。libuv在poll io阶段如果检测到有文件发生变化,则会执行回调uv__inotify_read。
static void uv__inotify_read(uv_loop_t* loop,
uv__io_t* dummy,
unsigned int events) {
const struct uv__inotify_event* e;
struct watcher_list* w;
uv_fs_event_t* h;
QUEUE queue;
QUEUE* q;
const char* path;
ssize_t size;
const char *p;
/* needs to be large enough for sizeof(inotify_event) + strlen(path) */
char buf[4096];
// 一次可能没有读完
while (1) {
do
// 读取触发的事件信息,size是数据大小,buffer保存数据
size = read(loop->inotify_fd, buf, sizeof(buf));
while (size == -1 && errno == EINTR);
// 没有数据可取了
if (size == -1) {
assert(errno == EAGAIN || errno == EWOULDBLOCK);
break;
}
// 处理buffer的信息
for (p = buf; p < buf + size; p += sizeof(*e) + e->len) {
// buffer里是多个uv__inotify_event结构体,里面保存了事件信息和文件对应的id(wd字段)
e = (const struct uv__inotify_event*)p;
events = 0;
if (e->mask & (UV__IN_ATTRIB|UV__IN_MODIFY))
events |= UV_CHANGE;
if (e->mask & ~(UV__IN_ATTRIB|UV__IN_MODIFY))
events |= UV_RENAME;
// 通过文件对应的id(wd字段)从红黑树中找到对应的节点
w = find_watcher(loop, e->wd);
path = e->len ? (const char*) (e + 1) : uv__basename_r(w->path);
w->iterating = 1;
// 把红黑树中,wd对应节点的handle队列移到queue变量,准备处理
QUEUE_MOVE(&w->watchers, &queue);
while (!QUEUE_EMPTY(&queue)) {
// 头结点
q = QUEUE_HEAD(&queue);
// 通过结构体偏移拿到首地址
h = QUEUE_DATA(q, uv_fs_event_t, watchers);
// 从处理队列中移除
QUEUE_REMOVE(q);
// 放回原队列
QUEUE_INSERT_TAIL(&w->watchers, q);
// 执行回调
h->cb(h, path, events, 0);
}
}
}
}
uv__inotify_read函数的逻辑就是从操作系统中把数据读取出来,这些数据中保存了哪些文件触发了用户感兴趣的事件。然后遍历每个触发了事件的文件。从红黑树中找到该文件对应的红黑树节点。再取出红黑树节点中维护的一个handle队列,最后执行handle队列中每个节点的回调。 总结:本文介绍了libuv中的inotify机制。他是对操作系统的封装,但是也加入了自己的一些逻辑。文中有很多地方没有展开分析,是因为在之前的文章中已经分析过了很多次。如果有疑问可以留言。