inotify

 

Linux 2.6內核中的文件系統變化通知機制

1、 引言node

    衆所周知,Linux 桌面系統與 MAC 或 Windows 相比有許多不如人意的地方,爲了改善這種情況,開源社區提出用戶態須要內核提供一些機制,以便用戶態可以及時地得知內核或底層硬件設備發生了什麼,從而能 夠更好地管理設備,給用戶提供更好的服務,如 hotplug、udev 和 inotify 就是這種需求催生的。Hotplug 是一種內核向用戶態應用通報關於熱插拔設備一些事件發生的機制,桌面系統可以利用它對設備進行有效的管理,udev 動態地維護 /dev 下的設備文件,inotify 是一種文件系統的變化通知機制,如文件增長、刪除等事件能夠馬上讓用戶態得知,該機制是著名的桌面搜索引擎項目 beagle 引入的,並在 Gamin 等項目中被應用。linux

    事實上,在 inotify 以前已經存在一種相似的機制叫 dnotify,可是它存在許多缺陷:數據庫

    1. 對於想監視的每個目錄,用戶都須要打開一個文件描述符,所以若是須要監視的目錄較多,將致使打開許多文件描述符,特別是,若是被監視目錄在移動介質上 (如光盤和 USB 盤),將致使沒法 umount 這些文件系統,由於使用 dnotify 的應用打開的文件描述符在使用該文件系統。windows

    2. dnotify 是基於目錄的,它只能獲得目錄變化事件,固然在目錄內的文件的變化會影響到其所在目錄從而引起目錄變化事件,可是要想經過目錄事件來得知哪一個文件變化,須要緩存許多 stat 結構的數據。數組

    3. Dnotify 的接口很是不友好,它使用 signal.緩存

    Inotify 是爲替代 dnotify 而設計的,它克服了 dnotify 的缺陷,提供了更好用的,簡潔而強大的文件變化通知機制:cookie

    1. Inotify 不須要對被監視的目標打開文件描述符,並且若是被監視目標在可移動介質上,那麼在 umount 該介質上的文件系統後,被監視目標對應的 watch 將被自動刪除,而且會產生一個 umount 事件。網絡

    2. Inotify 既能夠監視文件,也能夠監視目錄。app

    3. Inotify 使用系統調用而非 SIGIO 來通知文件系統事件。函數

    4. Inotify 使用文件描述符做爲接口,於是可使用一般的文件 I/O 操做select 和 poll 來監視文件系統的變化。

    Inotify 能夠監視的文件系統事件包括:

    IN_ACCESS,即文件被訪問IN_MODIFY,文件被 write IN_ATTRIB,文件屬性被修改,如 chmod、chown、touch 等IN_CLOSE_WRITE,可寫文件被 close IN_CLOSE_NOWRITE,不可寫文件被 close IN_OPEN,文件被 open IN_MOVED_FROM,文件被移走,如 mv IN_MOVED_TO,文件被移來,如 mv、cp IN_CREATE,建立新文件IN_DELETE,文件被刪除,如 rm IN_DELETE_SELF,自刪除,即一個可執行文件在執行時刪除本身IN_MOVE_SELF,自移動,即一個可執行文件在執行時移動本身 IN_UNMOUNT,宿主文件系統被 umount IN_CLOSE,文件被關閉,等同於(IN_CLOSE_WRITE | IN_CLOSE_NOWRITE)

    IN_MOVE,文件被移動,等同於(IN_MOVED_FROM | IN_MOVED_TO)

    注:上面所說的文件也包括目錄。

2、用戶接口

    在用戶態,inotify 經過三個系統調用和在返回的文件描述符上的文件 I/ 操做來使用,使用 inotify 的第一步是建立 inotify 實例:

 

 

 


 

                int fd = inotify_init ();
        

 

    每個 inotify 實例對應一個獨立的排序的隊列。

    文件系統的變化事件被稱作 watches 的一個對象管理,每個 watch 是一個二元組(目標,事件掩碼),目標能夠是文件或目錄,事件掩碼錶示應用但願關注的 inotify 事件,每個位對應一個 inotify 事件。Watch 對象經過 watch描述符引用,watches 經過文件或目錄的路徑名來添加。目錄 watches 將返回在該目錄下的全部文件上面發生的事件。

    下面函數用於添加一個 watch:


                int wd = inotify_add_watch (fd, path, mask);
        

 

    fd 是 inotify_init() 返回的文件描述符,path 是被監視的目標的路徑名(即文件名或目錄名),mask 是事件掩碼, 在頭文件 linux/inotify.h 中定義了每一位表明的事件。可使用一樣的方式來修改事件掩碼,即改變但願被通知的inotify 事件。Wd 是 watch 描述符。

    下面的函數用於刪除一個 watch:


        int ret = inotify_rm_watch (fd, wd);
        

 

    fd 是 inotify_init() 返回的文件描述符,wd 是 inotify_add_watch() 返回的 watch 描述符。Ret 是函數的返回值。

    文件事件用一個 inotify_event 結構表示,它經過由 inotify_init() 返回的文件描述符使用一般文件讀取函數 read 來得到


struct inotify_event {
        __s32           wd;             /* watch descriptor */
        __u32           mask;           /* watch mask */
        __u32           cookie;         /* cookie to synchronize two events */
        __u32           len;            /* length (including nulls) of name */
        char            name[0];        /* stub for possible name */
};

 

    結構中的 wd 爲被監視目標的 watch 描述符,mask 爲事件掩碼,len 爲 name字符串的長度,name 爲被監視目標的路徑名,該結構的 name 字段爲一個樁,它只是爲了用戶方面引用文件名,文件名是變長的,它實際緊跟在該結構的後面,文件名將被 0 填充以使下一個事件結構可以 4 字節對齊。注意,len 也把填充字節數統計在內。

    經過 read 調用能夠一次得到多個事件,只要提供的 buf 足夠大。


                size_t len = read (fd, buf, BUF_LEN);
        

 

    buf 是一個 inotify_event 結構的數組指針,BUF_LEN 指定要讀取的總長度,buf 大小至少要不小於 BUF_LEN,該調用返回的事件數取決於 BUF_LEN 以及事件中文件名的長度。Len 爲實際讀去的字節數,即得到的事件的總長度。

    能夠在函數 inotify_init() 返回的文件描述符 fd 上使用 select() 或poll(), 也能夠在 fd 上使用 ioctl 命令 FIONREAD 來獲得當前隊列的長度。close(fd)將刪除全部添加到 fd 中的 watch 並作必要的清理。


                int inotify_init (void);
        int inotify_add_watch (int fd, const char *path, __u32 mask);
        int inotify_rm_watch (int fd, __u32 mask);
        

 

3、內核實現機理

    在內核中,每個 inotify 實例對應一個 inotify_device 結構:

 

 

 

 

 

 

 


 

struct inotify_device {
        wait_queue_head_t       wq;             /* wait queue for i/o */
        struct idr              idr;            /* idr mapping wd -> watch */
        struct semaphore        sem;            /* protects this bad boy */
        struct list_head        events;         /* list of queued events */
        struct list_head        watches;        /* list of watches */
        atomic_t                count;          /* reference count */
        struct user_struct      *user;          /* user who opened this dev */
        unsigned int            queue_size;     /* size of the queue (bytes) */
        unsigned int            event_count;    /* number of pending events */
        unsigned int            max_events;     /* maximum number of events */
        u32                     last_wd;        /* the last wd allocated */
};

 

    d_list 指向全部 inotify_device 組成的列表的,i_list 指向全部被監視 inode 組成的列表,count 是引用計數,dev 指向該 watch 所在的 inotify 實例對應的 inotify_device 結構,inode 指向該 watch 要監視的 inode,wd 是分配給該 watch 的描述符,mask 是該 watch 的事件掩碼,表示它對哪些文件系統事件感興趣。

    結構 inotify_device 在用戶態調用 inotify_init() 時建立,當關閉 inotify_init()返回的文件描述符時將被釋放。結構 inotify_watch 在用戶態調用 inotify_add_watch()時建立,在用戶態調用 inotify_rm_watch() 或 close(fd) 時被釋放。

    不管是目錄仍是文件,在內核中都對應一個 inode 結構,inotify 系統在 inode 結構中增長了兩個字段:

 

struct inotify_watch {
        struct list_head        d_list; /* entry in inotify_device's list */
        struct list_head        i_list; /* entry in inode's list */
        atomic_t                count;  /* reference count */
        struct inotify_device   *dev;   /* associated device */
        struct inode            *inode; /* associated inode */
        s32                     wd;     /* watch descriptor */
        u32                     mask;   /* event mask for this watch */
};

 

    d_list 指向全部 inotify_device 組成的列表的,i_list 指向全部被監視 inode 組成的列表,count 是引用計數,dev 指向該 watch 所在的 inotify 實例對應的 inotify_device 結構,inode 指向該 watch 要監視的 inode,wd 是分配給該 watch 的描述符,mask 是該 watch 的事件掩碼,表示它對哪些文件系統事件感興趣。

    結構 inotify_device 在用戶態調用 inotify_init() 時建立,當關閉 inotify_init()返回的文件描述符時將被釋放。結構 inotify_watch 在用戶態調用 inotify_add_watch()時建立,在用戶態調用 inotify_rm_watch() 或 close(fd) 時被釋放。

    不管是目錄仍是文件,在內核中都對應一個 inode 結構,inotify 系統在 inode 結構中增長了兩個字段:

 

#ifdef CONFIG_INOTIFY
	struct list_head	inotify_watches; /* watches on this inode */
	struct semaphore	inotify_sem;	/* protects the watches list */
#endif

 

    inotify_watches 是在被監視目標上的 watch 列表,每當用戶調用 inotify_add_watch()時,內核就爲添加的 watch 建立一個 inotify_watch 結構,並把它插入到被監視目標對應的 inode 的 inotify_watches 列表。inotify_sem 用於同步對 inotify_watches 列表的訪問。當文件系統發生第一部分提到的事件之一時,相應的文件系統代碼將顯示調用fsnotify_* 來把相應的事件報告給 inotify 系統,其中*號就是相應的事件名,目前實現包括:

    fsnotify_move,文件從一個目錄移動到另外一個目錄fsnotify_nameremove,文件從目錄中刪除 fsnotify_inoderemove,自刪除fsnotify_create,建立新文件fsnotify_mkdir,建立新目錄 fsnotify_access,文件被讀fsnotify_modify,文件被寫fsnotify_open,文件被打開 fsnotify_close,文件被關閉fsnotify_xattr,文件的擴展屬性被修改fsnotify_change,文件被修改或原數據被修 改有一個例外狀況,就是 inotify_unmount_inodes,它會在文件系統被 umount 時調用來通知 umount 事件給 inotify 系統。

    以上提到的通知函數最後都調用 inotify_inode_queue_event(inotify_unmount_inodes直接調用 inotify_dev_queue_event ),該函數首先判斷對應的inode是否被監視,這經過查看 inotify_watches 列表是否爲空來實現,若是發現 inode 沒有被監視,什麼也不作,馬上返回,反之,遍歷 inotify_watches 列表,看是否當前的文件操做事件被某個 watch 監視,若是是,調用 inotify_dev_queue_event,不然,返回。函數inotify_dev_queue_event 首先判斷該事件是不是上一個事件的重複,若是是就丟棄該事件並返回,不然,它判斷是否 inotify 實例即 inotify_device 的事件隊列是否溢出,若是溢出,產生一個溢出事件,不然產生一個當前的文件操做事件,這些事件經過kernel_event 構建,kernel_event 將建立一個 inotify_kernel_event 結構,而後把該結構插入到對應的 inotify_device 的 events 事件列表,而後喚醒等待在inotify_device 結構中的 wq 指向的等待隊列。想監視文件系統事件的用戶態進程在inotify 實例(即 inotify_init() 返回的文件描述符)上調用 read 時但沒有事件時就掛在等待隊列 wq 上。

4、使用示例

    下面是一個使用 inotify 來監視文件系統事件的例子:

 

 

 

 

 

 

 


 

#include 
#include 
#include 

_syscall0(int, inotify_init)
_syscall3(int, inotify_add_watch, int, fd, const char *, path, __u32, mask)
_syscall2(int, inotify_rm_watch, int, fd, __u32, mask)

char * monitored_files[] = {
	"./tmp_file",
	"./tmp_dir",
	"/mnt/sda3/windows_file"
};

struct wd_name {
	int wd;
	char * name;
};

#define WD_NUM 3
struct wd_name wd_array[WD_NUM];

char * event_array[] = {
	"File was accessed",
	"File was modified",
	"File attributes were changed",
	"writtable file closed",
	"Unwrittable file closed",
	"File was opened",
	"File was moved from X",
	"File was moved to Y",
	"Subfile was created",
	"Subfile was deleted",
	"Self was deleted",
	"Self was moved",
	"",
	"Backing fs was unmounted",
	"Event queued overflowed",
	"File was ignored"
};
#define EVENT_NUM 16
#define MAX_BUF_SIZE 1024
	
int main(void)
{
	int fd;
	int wd;
	char buffer[1024];
	char * offset = NULL;
	struct inotify_event * event;
	int len, tmp_len;
	char strbuf[16];
	int i = 0;
	
	fd = inotify_init();
	if (fd < 0) {
		printf("Fail to initialize inotify.\n");
		exit(-1);
	}

	for (i=0; imask & IN_ISDIR) {
				memcpy(strbuf, "Direcotory", 11);
			}
			else {
				memcpy(strbuf, "File", 5);
			}
			printf("Object type: %s\n", strbuf);
			for (i=0; iwd != wd_array[i].wd) continue;
				printf("Object name: %s\n", wd_array[i].name);
				break;
			}
			printf("Event mask: %08X\n", event->mask);
			for (i=0; imask & (1<len;
			event = (struct inotify_event *)(offset + tmp_len); 
			offset += tmp_len;
		}
	}
}

 

    該程序將監視發生在當前目錄下的文件 tmp_file 與當前目錄下的目錄 tmp_dir 上的全部文件系統事件, 同時它也將監視發生在文件 /mnt/sda3/windows_file 上的文件系統事件,注意,/mnt/sda3 是 SATA 硬盤分區 3 的掛接點。

    細心的讀者可能注意到,該程序首部使用 _syscallN 來聲明 inotify 系統調用,緣由是這些系統調用是在最新的穩定內核 2.6.13 中引入的,glibc 並無實現這些系統調用的庫函數版本,所以,爲了能在程序中使用這些系統調用,必須經過 _syscallN 來聲明這些新的系統,其中的 N 爲要聲明的系統調用實際的參數數。還有須要注意的地方是系統的頭文件必須與被啓動的內核匹配,爲了讓上面的程序可以成功編譯,必須讓 2.6.13 的內核頭文件(包括 include/linux/*, include/asm/* 和 include/asm-generic/*)在頭文件搜索路徑內,而且是第一優先搜索的頭文件路徑,由於 _syscallN 須要用到這些頭文件中的 linux/unistd.h 和 asm/unistd.h,它們包含了 inotify 的三個系統調用的系統調用號 __NR_inotify_init、__NR_inotify_add_watch 和 __NR_inotify_rm_watch.

    所以,要想成功編譯此程序,只要把用戶編譯好的內核的頭文件拷貝到該程序所在的路徑,並使用以下命令編譯便可:

 

$gcc -o inotify_example  -I. inotify_example.c

 

    注意:當前目錄下應當包含 linux、asm 和 asm-generic 三個已編譯好的 2.6.13 內核的有文件目錄,asm 是一個連接,所以拷貝 asm 頭文件的時候須要拷貝 asm 與 asm-ARCH(對於 x86 平臺應當是 asm-i386)。而後,爲了運行該程序,須要在當前目錄下建立文件 tmp_file 和目錄 tmp_dir,對於/mnt/sda3/windows_file 文件,用戶須要依本身的實際狀況而定,多是/mnt/dosc/windows_file,即 /mnt/dosc 是一個 FAT32 的 windows 硬盤,所以用戶在編譯該程序時須要根據本身的實際狀況來修改 /mnt/sda3.Windows_file 是在被 mount 硬盤上建立的一個文件,爲了運行該程序,它必須被建立。

    如下是做者在 redhat 9.0 上運行此程序獲得的一些結果:

    當運行此程序的時候在另外一個虛擬終端執行 cat ./tmp_file,此程序的輸出爲:

 

Some event happens, len = 48.
Object type: File
Object name: ./tmp_file
Event mask: 00000020
Event: File was opened
Object type: File
Object name: ./tmp_file
Event mask: 00000001
Event: File was accessed
Object type: File
Object name: ./tmp_file
Event mask: 00000010
Event: Unwrittable file closed

 

    以上事件清楚地說明了 cat 指令執行了文件 open 和 close 操做,固然 open 和 close操做都屬於 access 操做,任何對文件的操做都是 access 操做。

    此外,運行 vi ./tmp_file,發現 vi實際在編輯文件時複製了一個副本,在未保存以前是對副本進行操做。運行 vi ./tmp_file, 修改並保存退出時,發現 vi 實際在保存修改時刪除了最初的文件並把那個副本文件名更改成最初的文件的名稱。注意,事件"File was ignored"表示系統把該文件對應的 watch 從 inotify 實例的 watch 列表中刪除,由於文件已經被刪除。讀者能夠本身分別執行命令:echo "abc" > ./tmp_file 、rm -f tmp_file、 ls tmp_dir、 cd tmp_dir;touch c.txt、 rm c.txt 、 umount /mnt/sda3(實際用戶須要使用本身當時的 mount 點路徑名),而後分析一下結果。Umount 觸發兩個事件,一個表示文件已經被刪除或不在存在,另外一個表示該文件的 watch被從 watch 列表中刪除。

5、典型應用

    beagle 是 GNOME 的桌面搜索引擎項目,inotify 的引入就是徹底受它的驅動而作的。對於桌面搜索引擎,它通常做爲一個優先級很低的後臺進程運行, 只有在系統沒有其餘任務可運行時才被調度執行,桌面搜索引擎的主要用途就是爲系統的文件系統的文件創建索引數據庫,以便用戶在須要某文件但又想不起存放在 哪裏時可以根據某些關鍵字或特徵快速地搜索到須要的文件,就象使用網絡搜索引擎 google 同樣便捷。文件系統有個特色就是隻有某些文件會變化,所以桌面搜索引擎在第一次創建完索引數據庫後,不必重複遍歷全部的文件創建新的索引,它只須要更新 修改了的文件的索引,創建新增長的文件的索引,刪除已經刪除的文件的索引就足夠了,這樣桌面搜索引擎須要作的工做就大大地減小。Inotify 就是爲這一意圖專門設計的,beagle 爲須要監視的目錄或文件建立了inotify 實例,而後它就等待該 inotify 上發生文件系統事件,若是沒有任何文件變化,beagle 將不須要任何開銷,只有在有被監視的事件發生時,beagle 才被喚醒並根據實際事件來更新對應的文件的索引,而後繼續睡眠等待下一個文件系統事件發生。在 SuSe 9.3 和即將發佈的 10.0 中就包含了該桌面搜索引擎,它可以爲文檔、email、音樂、圖象和應用等創建索引。使用過 windows 下的桌面搜索引擎的讀者對 google 和 yahoo 以及 Microsoft 的桌面搜索引擎有深入的體會,感興趣讀者能夠安裝 SuSe 使用一下。

    6、小結

    inotify 是在 2.6.13 中引入的新功能,它爲用戶態監視文件系統的變化提供了強大的支持,本文詳盡地介紹了其起源、內核實現、用戶接口以及使用,有興趣的讀者能夠讀 2.6.13的相關源碼來進一步瞭解其實現細節。

相關文章
相關標籤/搜索