Linux 使用 inotify 監控檔案或目錄變化

林西索發表於2024-05-05

轉載:https://www.cnblogs.com/PikapBai/p/14480881.html

作者:PikapBai

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,將該檔案加入監控佇列。

相關文章