1 運行環境
- 操作系統:Ubuntu 18
2 inotify 簡介
-
inotify 是一個 Linux 內核特性(監視文件系統事件),它用於監控文件系統,比如刪除、讀、寫操作等,當發生對應事件時,則會觸發 inotify。當監控目錄時,與該目錄自身以及該目錄下面的文件都會被監控,其上有事件發生時都會通知給應用程序
-
inotify 監控機制為非遞歸,若想監控整個目錄子樹內的事件,則需對該樹中的每個目錄發起 inotify_add_watch() 調用
-
使用 inotify:創建一個文件描述符,附加一個或多個監視器(一個監視器 是一個路徑和一組事件),然后使用 read() 方法從描述符獲取事件信息。read() 並不會用光整個周期,它在事件發生之前是被阻塞的。
-
因為 inotify 通過傳統的文件描述符工作,可使用 select(),poll(),epoll() 以及由信號驅動的 I/O 來監控 inotify 文件描述符
-
要使用 inotify,必須具備一台帶有 2.6.13 或更新內核的 Linux 機器(以前的 Linux 內核版本使用更低級的文件監控器 dnotify)。如果您不知道內核的版本,請轉到 shell,輸入 uname -a
3 inotify API
3.1 inotify_init
創建一個 inotify 實例並返回一個引用 inotify 實例的文件描述符
函數原型:
#include<sys/inotify.h>
int inotify_init(void);
返回值:
-
成功:該函數的返回值為一個文件描述符,該文件描述符所指代的文件中將會保存所監控的 文件/目錄 所發生的 事件集。
-
失敗:返回 -1,並且將 errno 設置為對應錯誤。
使用及解釋:
int fd = inotify_init();
fd 為所指的 inotify 實例的 監控列表,系統調用 inotify_add_watch() 可以向該 fd 追加 新的監控項。
3.2 inotify_add_watch
針對 fd 所指的 inotify 實例的 監控列表 追加 新的監控項。
函數原型:
#include<sys/inotify.h>
int inotify_add_watch(int fd,const char *pathname,uint32_t mask);
返回值:
-
成功:返回值為一個用於 唯一指代此 監控項 的描述符
-
失敗:返回值 < 0 ,則代表添加該監控項失敗,需要檢測 pathname 是否有可讀權限,是否存在,系統的監控隊列是否已滿等
參數:
-
pathname 為想要創建的監控項所對應的文件,特別注意調用該接口必須要對該文件有讀權限,該函數只對文件做一次檢查,如果在監控時修改了所監控的文件讀權限,則不會影響繼續監控此文件
-
mask 為一位掩碼,針對 pathname 定義了想要監控的事件,此函數的返回值為一個用於唯一指代此監控項的描述符(將在 4 inotify 事件 中介紹)
4 inotify 常用監控事件
-
IN_ACCESS:文件 被訪問時 觸發事件,例如 read,execve
-
IN_ATTRIB:文件屬性 發生變化 觸發事件。例如 權限 chmod,時間戳 setxattr,鏈接數 link 等
-
IN_CLOSE_WRITE:一個文件被打開 寫入操作結束,文件被關閉時 觸發事件
-
IN_CLOSE_NOWRITE:一個文件被打開 沒有任何寫操作,文件被關閉時 觸發事件
-
IN_CREATE:在監控列表下 創建一個文件或目錄 時 觸發事件,例如 open O_CREAT,mkdir 等
-
IN_DELETE:在監控列表下 文件或目錄 被刪除時 觸發事件
-
IN_DELETE_SELF:監控文件或目錄 本身被刪除時 觸發事件,而且,如果一個文件或目錄被移到其它地方,比如使用 mv 命令,也會觸發該事件,因為 mv 命令本質上是拷貝一份當前文件,然后刪除當前文件的操作。此時監控終止,並且將收到一個 IN_IGNORED 事件。
-
IN_MODIFY:文件 被修改時 觸發事件,例如:有寫操作(write)或者文件內容被清空(truncate)操作。不過需要注意的是,IN_MODIFY 可能會連續觸發多次。
-
IN_MODIFY_SELF:所監控的文件或目錄本身 發生移動時 觸發事件
-
IN_MOVED_FROM:將文件或目錄 移除 監控列表 觸發事件
-
IN_MOVED_TO:將文件或目錄 移入 監控列表 觸發事件
-
IN_OPEN:文件被打開 觸發事件
-
IN_ALL_EVENTS:監控所有事件
-
IN_MOVE:IN_MOVED_FROM | IN_MOVED_TO 事件的統稱
5 存儲 inotify 事件 結構體 struct inotify_event
將 監控項 在 監控列表 中登記后,應用程序可以用 read() 從 inotify 的文件描述符 中讀取事件以判定發生了那些事件。若讀取之時還沒有發生任何事件,則 read() 會阻塞,直至有事件產生。事件發生后,每次調用 read() 會返回一個緩存區,內含一個或多個如下類型的結構體:
struct inotify_event
{
int wd; // 指向發生事件的監控項的文件描述符,該字段值由之前對 inotify_add_watch() 的調用返回。用於區分是哪個監控項觸發了該事件
uint32_t mask; // inotify 事件的一位掩碼
uint32_t cookie; // 唯一的關聯 inotify 事件的值
uint32_t len; // 分配給 name 的字節數
char name[]; // 標識觸發該事件的文件名
};
注意:
如果是監控目錄,此時目錄下的文件觸發事件,會輸出對應的文件名。但是如果只監控文件,則無法根據 event->name 輸出對應更改的文件名,原因參考 7.1 監控文件時,無法根據 event->name 輸出對應更改的文件名
6 inotify 示例
6.1 代碼
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/inotify.h>
#define EVENT_NUM 12
const char *event_str[EVENT_NUM] =
{
"IN_ACCESS",
"IN_MODIFY",
"IN_ATTRIB",
"IN_CLOSE_WRITE",
"IN_CLOSE_NOWRITE",
"IN_OPEN",
"IN_MOVED_FROM",
"IN_MOVED_TO",
"IN_CREATE",
"IN_DELETE",
"IN_DELETE_SELF",
"IN_MOVE_SELF"
};
int inotifyTask(char *argv[])
{
int errTimes = 0;
int fd = -1;
INIT_INOTIFY:
fd = inotify_init();
if(fd < 0)
{
fprintf(stderr, "inotify_init failed\n");
printf("Error no.%d: %s\n", errno, strerror(errno));
goto INOTIFY_FAIL;
}
int wd1 = -1;
int wd2 = -1;
struct inotify_event *event;
int length;
int nread;
char buf[BUFSIZ];
int i = 0;
buf[sizeof(buf) - 1] = 0;
INOTIFY_AGAIN:
wd1 = inotify_add_watch(fd, argv[1], IN_ALL_EVENTS);
if(wd1 < 0)
{
fprintf(stderr, "inotify_add_watch %s failed\n", argv[1]);
printf("Error no.%d: %s\n", errno, strerror(errno));
if(errTimes < 3)
{
goto INOTIFY_AGAIN;
}
else
{
goto INOTIFY_FAIL;
}
}
wd2 = inotify_add_watch(fd, argv[2], IN_ALL_EVENTS);
if(wd2 < 0)
{
fprintf(stderr, "inotify_add_watch %s failed\n", argv[2]);
printf("Error no.%d: %s\n", errno, strerror(errno));
if(errTimes < 3)
{
goto INOTIFY_AGAIN;
}
else
{
goto INOTIFY_FAIL;
}
}
length = read(fd, buf, sizeof(buf) - 1);
nread = 0;
// inotify 事件發生時
while(length > 0)
{
printf("\n");
event = (struct inotify_event *)&buf[nread];
// 遍歷所有事件
for(i = 0; i< EVENT_NUM; i++)
{
// 判斷事件是否發生
if( (event->mask >> i) & 1 )
{
// 該監控項為目錄或目錄下的文件時
if(event->len > 0)
{
fprintf(stdout, "%s --- %s\n", event->name, event_str[i]);
}
// 該監控項為文件時
else if(event->len == 0)
{
if(event->wd == wd1)
{
fprintf(stdout, "%s --- %s\n", argv[1], event_str[i]);
}
if(event->wd == wd2)
{
fprintf(stdout, "%s --- %s\n", argv[2], event_str[i]);
}
}
}
}
nread = nread + sizeof(struct inotify_event) + event->len;
length = length - sizeof(struct inotify_event) - event->len;
}
goto INOTIFY_AGAIN;
close(fd);
return 0;
INOTIFY_FAIL:
return -1;
}
int main(int argc, char *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s path path\n", argv[0]);
return -1;
}
if(inotifyTask(argv) == -1)
{
return -1;
}
return 0;
}
6.2 編譯
編譯命令:
gcc inotify.c -o out
如下圖所示:
6.3 運行截圖
6.3.1 不加任何參數
此時會提示信息,需要輸入兩個路徑用於監控,如下圖所示:
6.3.2 監控兩個文件
監控 /etc/a,/etc/b
如下圖所示:
6.3.3 監控兩個目錄
監控 /etc,/tmp
如下圖所示:
7 inotify 監控文件時常見問題
7.1 監控文件時,無法根據 event->name 輸出對應更改的文件名
原因:
在 linux 手冊中關於 inotify 的描述有對應解釋。 如果是監控目錄,此時目錄下的文件觸發事件,會輸出對應的文件名。但是如果只監控文件,則無法輸出對應更改的文件名。如下圖所示:
7.2 監控文件時,無法持續監控,第二次更改文件時,它沒有響應
原因:
這是由於 vim 的工作機制引起的,vim 會先將源文件復制為另一個文件,然后在另一文件基礎上編輯(后綴名為 swp),保存的時候再將這個文件覆蓋源文件。此時原來的文件已經被后來的新文件代替,因此監視對象所監視的文件已經不存在了,所以自然不會產生任何事件。
解決方法:
重新使用 inotify_add_watch,將該文件加入監控隊列。
8 參考資料
1、linux 手冊中關於 inotify 的描述 - https://man7.org/linux/man-pages/man7/inotify.7.html
2、Stack Overflow 中關於 inotify inotify_event event->name is empty Shay 的提問 - Will Chappell 的回答- https://stackoverflow.com/questions/7957132/inotify-inotify-event-event-name-is-empty
3、inotify 檢測文件被修改 - https://www.169it.com/tech-qa-linux/article-13284431940448660571.html
4、如何在C中使用inotify - http://www.voidcn.com/article/p-ntlqecbe-bwk.html
5、c使用inotify監控linux路徑下文件變化 - meccaendless(一江明澈的水)- https://blog.csdn.net/meccaendless/article/details/80238997
6、如何用c語言實現對目錄或是文件進行文件的添加,修改,刪除監控(inotify) - jenie - https://blog.csdn.net/jenie/article/details/106195668?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-3&spm=1001.2101.3001.4242