linux 檔案監控之 inotify
某些應用程式需要對檔案或目錄進行監控,以感知這些檔案或目錄發生了特定事件。在 Linux 中提供了 inotify 機制允許應用程式可以監聽檔案(目錄)事件。
本文主要從以下幾個方面對 inotify 進行介紹:
- inotify 使用場景
- inotify 機制關聯的相關係統呼叫
- inotify 支援的事件型別
- inotify 使用示例
使用場景
監聽檔案或者目錄的變更,最終目的一定是基於不同的變更事件採取相對應的處理措施。比較常見的使用場景如下:
- 配置檔案熱載入,當配置檔案發生變化時程序可以自動感知並重新 reload 配置檔案,如 golang 的明星專案--viper
- 配置保持功能,當我們需要保持伺服器上某些檔案不被改動時,可以監聽需要保持的檔案。當檔案出現變更時做相應的恢復處理
- 當檔案移出或者加入到某個目錄下的時候,圖形化檔案管理器需要根據對應的事件作出相對應的調整
系統呼叫
與 inotify 有關的系統呼叫主要有三個:inotify_init
,inotify_add_watch
,inotify_rm_watch
,具體的系統呼叫如下所示:
inotify_init
#include <sys inotify.h="">
int inotify_init(void);
inotify_init
建立一個inotify
例項,該函式會返回檔案描述符用來指代inotify
例項,同時之後需要通過對該檔案描述符進行read 操作
獲取檔案變更事件
inotify_add_watch
#include <sys inotify.h="">
int inotify_add_watch(int fd, const char *pathname, uint32_t mask);
- fd 指代
inotify_init
系統呼叫返回的notify
例項 - pathname 指代需要被監聽事件的檔案或者目錄路徑
- mask 事件掩碼,表明需要監聽的事件型別。具體的事件型別下文會進行描述
該系統呼叫返回值(wd)是監控描述符,指代一條監控項。
上圖展示了一個notify
例項,以及該例項維護的一組監控項
- 監控描述符就是
inotify_add_watch
系統呼叫的返回,唯一指代一個監控描述項 - 掩碼即是 mask 用來定義具體的監聽事項
- pathname 即是完整的待監聽檔案或目錄的合法路徑
inotify_rm_watch
#include <sys inotify.h="">
int inotify_rm_watch(int fd, uint32_t wd);
- fd 指代
inotify_init
系統呼叫返回的notify
例項 - wd 指代監控項描述符
事件型別
常規事件型別
mask 標誌 | 描述 |
---|---|
IN_ACCESS | 檔案被訪問(執行了 read 操作) |
IN_ATTRIB | 檔案元資料發生變更 |
IN_CLOSE_WRITE | 關閉為了寫入而開啟的檔案 |
IN_CLOSE_NOWRITE | 關閉以只讀方式開啟的檔案 |
IN_CREATE | 在受控目錄內建立了檔案或者目錄 |
IN_DELETE | 在受控目錄內刪除了檔案或者目錄 |
IN_DELETE_SELF | 刪除受控檔案或者目錄本身 |
IN_MOVED_FROM | 檔案移出受控目錄之外 |
IN_MOVED_TO | 檔案移入受控目錄 |
IN_OPEN | 檔案被開啟 |
IN_MOVE | IN_MOVED_FROM|IN_MOVED_TO 事件的統稱 |
IN_CLOSE | IN_CLOSE_WRITE|IN_CLOSE_NOWRITE 統稱 |
IN_ATTRIB
監控的元資料變更包括,許可權,所有權,連結數,使用者 ID 等- 重新命名受控物件時會發出
IN_DELETE_SELF
事件,而如果受控目標是一個目錄,那麼受控目標下的檔案發生重新命名時會觸發兩個事件IN_MOVED_FROM
和IN_MOVED_TO
在我們的日常開發工作中,上述事件已經基本涵蓋了檔案變更的所有情況。我們可以按照各自的場景,針對上述不同的事件型別做出相應的處理流程。
其他事件
除了上述檔案的常規事件外,inotify
還提供了以下幾個 mask 來控制事件監聽的過程
mask 標誌 | 描述 |
---|---|
IN_DONT_FOLLOW | 不對符號連結引用 |
IN_MASK_ADD | 將事件追加到 pathname 的當前監控掩碼 |
IN_ONESHOT | 只監控 pathname 的一個事件 |
IN_ONLYDIR | pathname 不為目錄時會失敗 |
將上述 mask 標誌新增到 inotify_add_watch
中時可以控制監聽過程,這麼說有點籠統,舉個例子來說。
inotify_add_watch(fd, pathname, IN_OPEN | IN_CLOSE | IN_ONESHOT);
上面這段程式碼,除了監聽檔案的 IN_OPEN
和IN_CLOSE
事件外,還添加了 IN_ONESHOT
mask,那麼這就意味著,當監聽到 pathname 所指代的檔案一次事件後 inotify
就不會在監聽 pathname 所指代的檔案發出的事件了。
上述 mask 是在新增某個檔案監控項的時候作為inotify_add_watch
系統呼叫的引數傳入的。除此之外還有以下幾個事件,這些事件不需要使用者顯示呼叫inotify_add_watch
新增,僅當出現一些其他異常情況時發出。
mask 標誌 | 描述 |
---|---|
IN_IGNORED | 監控項為核心或者應用程式移除 |
IN_ISDIR | 被監聽的是一個目錄的路徑 |
IN_Q_OVERFLOW | 事件佇列溢位 |
IN_UNMOUNT | 包含物件的檔案系統遭到解除安裝 |
IN_ISDIR
事件表明被監聽的 pathname 指代的是一個目錄,舉例來說 mkdir /tmp/xxx 這個系統命令會產生IN_CREATE|IS_DIR
事件。
事件結構
上文描述了inotify
支援的事件型別,可以看出來支援的事件型別非常豐富,基本滿足了我們對於檔案監聽的各種訴求。除了上述的事件型別外,在這一小節我們會簡單描述一下inotify
的event
結構,通過事件的資料結構可以看出,從事件中我們可以獲取到哪些資訊。事件的具體資料結構如下:
struct inotify_event {
int wd; //監控描述符號,唯一指代一個監控專案
uint32_t mask; //監控的事件型別
uint32_t cookie; //只有重新命名才會使用到該欄位
uint32_t len; //下面 name 陣列的尺寸
char name[]; //當受控目錄下的檔案有變更時,該字串會記錄發生變更的檔案的檔名
};
使用示例
inotify demo 該示例註釋非常詳細,同時使用到了上述的三個系統呼叫,具體程式碼如下:
#include <errno.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys inotify.h="">
#include <unistd.h>
#include <string.h>
/* Read all available inotify events from the file descriptor 'fd'.
wd is the table of watch descriptors for the directories in argv.
argc is the length of wd and argv.
argv is the list of watched directories.
Entry 0 of wd and argv is unused. */
static void
handle_events(int fd, int *wd, int argc, char* argv[])
{
/* Some systems cannot read integer variables if they are not
properly aligned. On other systems, incorrect alignment may
decrease performance. Hence, the buffer used for reading from
the inotify file descriptor should have the same alignment as
struct inotify_event. */
char buf[4096]
__attribute__ ((aligned(__alignof__(struct inotify_event))));
const struct inotify_event *event;
ssize_t len;
/* Loop while events can be read from inotify file descriptor. */
for (;;) {
/* Read some events. */
// fd 為 inotfy 例項檔案描述符
len = read(fd, buf, sizeof(buf)); //讀取事件
if (len == -1 && errno != EAGAIN) {
perror("read");
exit(EXIT_FAILURE);
}
/* If the nonblocking read() found no events to read, then
it returns -1 with errno set to EAGAIN. In that case,
we exit the loop. */
if (len <= 0)
break;
/* Loop over all events in the buffer. */
for (char *ptr = buf; ptr < buf + len;
ptr += sizeof(struct inotify_event) + event->len) {
event = (const struct inotify_event *) ptr;
/* Print event type. */
//通過事件 mask 掩碼獲取當前事件是哪一型別的事件
if (event->mask & IN_OPEN)
printf("IN_OPEN: ");
if (event->mask & IN_CLOSE_NOWRITE)
printf("IN_CLOSE_NOWRITE: ");
if (event->mask & IN_CLOSE_WRITE)
printf("IN_CLOSE_WRITE: ");
/* Print the name of the watched directory. */
for (int i = 1; i < argc; ++i) {
if (wd[i] == event->wd) {
printf("%s/", argv[i]);
break;
}
}
/* Print the name of the file. */
if (event->len)
printf("%s", event->name);
/* Print type of filesystem object. */
if (event->mask & IN_ISDIR)
printf(" [directory]\n");
else
printf(" [file]\n");
}
}
}
int
main(int argc, char* argv[])
{
char buf;
int fd, i, poll_num;
int *wd;
nfds_t nfds;
struct pollfd fds[2];
if (argc < 2) {
printf("Usage: %s PATH [PATH ...]\n", argv[0]);
exit(EXIT_FAILURE);
}
printf("Press ENTER key to terminate.\n");
/* Create the file descriptor for accessing the inotify API. */
fd = inotify_init1(IN_NONBLOCK); //建立 inotify 例項,fd檔案控制代碼指代 inotify 例項
if (fd == -1) {
perror("inotify_init1");
exit(EXIT_FAILURE);
}
/* Allocate memory for watch descriptors. */
wd = calloc(argc, sizeof(int));
if (wd == NULL) {
perror("calloc");
exit(EXIT_FAILURE);
}
/* Mark directories for events
- file was opened
- file was closed */
//新增檔案監控項,這裡支援註冊多個監聽目錄,同時只監聽了 IN_OPEN 和 IN_CLOSE事件型別
for (i = 1; i < argc; i++) {
wd[i] = inotify_add_watch(fd, argv[i],
IN_OPEN | IN_CLOSE);
if (wd[i] == -1) {
fprintf(stderr, "Cannot watch '%s': %s\n",
argv[i], strerror(errno));
exit(EXIT_FAILURE);
}
}
/* Prepare for polling. */
//同時監聽終端個 inotify 訊息,當終端回車時,該監聽程式退出,當監聽到檔案變更事件後處理事件
nfds = 2;
fds[0].fd = STDIN_FILENO; /* Console input */
fds[0].events = POLLIN;
fds[1].fd = fd; /* Inotify input */
fds[1].events = POLLIN;
/* Wait for events and/or terminal input. */
printf("Listening for events.\n");
while (1) {
poll_num = poll(fds, nfds, -1);
if (poll_num == -1) {
if (errno == EINTR)
continue;
perror("poll");
exit(EXIT_FAILURE);
}
if (poll_num > 0) {
if (fds[0].revents & POLLIN) {
/* Console input is available. Empty stdin and quit. */
while (read(STDIN_FILENO, &buf, 1) > 0 && buf != '\n')
continue;
break;
}
if (fds[1].revents & POLLIN) {
/* Inotify events are available. */
handle_events(fd, wd, argc, argv);
}
}
}
printf("Listening for events stopped.\n");
/* Close inotify file descriptor. */
close(fd);
free(wd);
exit(EXIT_SUCCESS);
}
其他細節
inotify
的事件佇列並不是無限大的,因為佇列也是需要消耗核心記憶體的,因此會設定一些上限加以限制,具體的配置可以通過修改對/proc/sys/fs/inotify
下的三個檔案來達到控制檔案事件監聽的目的。
max_queued_events
: 規定了inotify
事件佇列的數量上限,一旦超出這個限制,系統就會生成IN_Q_OVERFLOW
事件,該事件上文有詳細描述,這裡不再贅述。max_user_instances
:對由每個真實使用者 ID 建立的inotify
例項數的限制值。max_user_watchers
:對由每個真實使用者 ID 建立的監控項數量的限制值。
總結
最近在學習 <<linux/unix系統程式設計手冊>> 這本書,同時在專案中有需要使用檔案監聽機制的場景。所以就詳細瞭解了檔案事件監聽的概念和具體用法。希望通過撰寫本文幫自己理清思路,加深對這個知識點的理解,也希望能夠對正在瞭解這一塊的同學有所幫助。