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機制。他是對操作系統的封裝,但是也加入了自己的一些邏輯。文中有很多地方沒有展開分析,是因爲在之前的文章中已經分析過了很多次。如果有疑問可以留言。