inotify -- Linux 2.6 核心中的檔案系統變化通知機制
一、 引言
眾所周知,Linux 桌面系統與 MAC 或 Windows 相比有許多不如人意的地方,為了改善這種狀況,開源社群提出使用者態需要核心提供一些機制,以便使用者態能夠及時地得知核心或底層硬體裝置發生了什麼,從而能夠更好地管理裝置,給使用者提供更好的服務,如 hotplug、udev 和 inotify 就是這種需求催生的。Hotplug 是一種核心向用戶態應用通報關於熱插拔裝置一些事件發生的機制,桌面系統能夠利用它對裝置進行有效的管理,udev 動態地維護 /dev 下的裝置檔案,inotify 是一種檔案系統的變化通知機制,如檔案增加、刪除等事件可以立刻讓使用者態得知,該機制是著名的桌面搜尋引擎專案 beagle 引入的,並在 Gamin 等專案中被應用。
事實上,在 inotify 之前已經存在一種類似的機制叫 dnotify,但是它存在許多缺陷:
1. 對於想監視的每一個目錄,使用者都需要開啟一個檔案描述符,因此如果需要監視的目錄較多,將導致開啟許多檔案描述符,特別是,如果被監視目錄在移動介質上(如光碟和 USB 盤),將導致無法 umount 這些檔案系統,因為使用 dnotify 的應用開啟的檔案描述符在使用該檔案系統。
2. dnotify 是基於目錄的,它只能得到目錄變化事件,當然在目錄內的檔案的變化會影響到其所在目錄從而引發目錄變化事件,但是要想通過目錄事件來得知哪個檔案變化,需要快取許多 stat 結構的資料。
3. Dnotify 的介面非常不友好,它使用 signal。
Inotify 是為替代 dnotify 而設計的,它克服了 dnotify 的缺陷,提供了更好用的,簡潔而強大的檔案變化通知機制:
1. Inotify 不需要對被監視的目標開啟檔案描述符,而且如果被監視目標在可移動介質上,那麼在 umount 該介質上的檔案系統後,被監視目標對應的 watch 將被自動刪除,並且會產生一個 umount 事件。
2. Inotify 既可以監視檔案,也可以監視目錄。
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)
注:上面所說的檔案也包括目錄。
二、使用者介面
在使用者態,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);
三、核心實現機理
在核心中,每一個 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 */ };
wq 是等待佇列,被 read 呼叫阻塞的程序將掛在該等待佇列上,idr 用於把 watch 描述符對映到對應的 inotify_watch,sem 用於同步對該結構的訪問,events 為該 inotify 例項上發生的事件的列表,被該 inotify 例項監視的所有事件在發生後都將插入到這個列表,watches 是給 inotify 例項監視的 watch 列表,inotify_add_watch 將把新新增的 watch 插入到該列表,count 是引用計數,user 用於描述建立該 inotify 例項的使用者,queue_size 表示該 inotify 例項的事件佇列的位元組數,event_count 是 events 列表的事件數,max_events 為最大允許的事件數,last_wd 是上次分配的 watch 描述符。
每一個 watch 對應一個 inotify_watch 結構:
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 上。
四、使用示例
1.我的使用例項 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <errno.h> #include <poll.h> #include <sys/inotify.h> #include <string.h>//#define FILENAME "./test.txt" #define FILENAME "/mnt/hgfs/share_1/tmp/a.txt" #define BUF_MAX_SIZE 1024
void relay(int fd, int wd) { char buf[1024]; char namebuf[16]; int len; struct inotify_event *event; char cmd[] = "notify OPEN -t 2";
//初始化inotify事件 fd = inotify_init(); if (fd < 0) { printf("Fail to init inotify.\n"); exit(1); } //新增watch事件 wd = inotify_add_watch(fd, FILENAME, IN_ALL_EVENTS); if (wd < 0) { printf("Fail to add watchinotify.\n"); exit(1); }
while(len = read(fd, buf, BUF_MAX_SIZE)) { event = (struct inotify_event *)buf; //printf("len = %d\n", len); memcpy(namebuf, event->name, event->len); namebuf[len+1] = 0; printf("len = %d\n", event->len); //printf("FileName = %s\n", namebuf); if (event->mask & IN_OPEN) { printf("file is read OPEN!\n"); system(cmd); }
if (event->mask & IN_ACCESS) printf("file is ACCESS!\n");
if (event->mask & IN_CLOSE_WRITE) printf("file is CLOSE_WRITE.\n"); } }
int main() { int fd, wd;
relay(fd, wd);
close(fd); close(wd); exit(0); }
下面是一個使用 inotify 來監視檔案系統事件的例子:
#include <linux/unistd.h> #include <linux/inotify.h> #include <errno.h> _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; i<WD_NUM; i++) { wd_array[i].name = monitored_files[i]; wd = inotify_add_watch(fd, wd_array[i].name, IN_ALL_EVENTS); if (wd < 0) { printf("Can't add watch for %s.\n", wd_array[i].name); exit(-1); } wd_array[i].wd = wd; } while(len = read(fd, buffer, MAX_BUF_SIZE)) { offset = buffer; printf("Some event happens, len = %d.\n", len); event = (struct inotify_event *)buffer; while (((char *)event - buffer) < len) { if (event->mask & IN_ISDIR) { memcpy(strbuf, "Direcotory", 11); } else { memcpy(strbuf, "File", 5); } printf("Object type: %s\n", strbuf); for (i=0; i<WD_NUM; i++) { if (event->wd != 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; i<EVENT_NUM; i++) { if (event_array[i][0] == '\0') continue; if (event->mask & (1<<i)) { printf("Event: %s\n", event_array[i]); } } tmp_len = sizeof(struct inotify_event) + event->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 列表中刪除。
五、典型應用
beagle 是 GNOME 的桌面搜尋引擎專案,inotify 的引入就是完全受它的驅動而做的。對於桌面搜尋引擎,它一般作為一個優先順序很低的後臺程序執行, 只有在系統沒有其他任務可執行時才被排程執行,桌面搜尋引擎的主要用途就是為系統的檔案系統的檔案建立索引資料庫,以便使用者在需要某檔案但又想不起存放在哪裡時能夠根據某些關鍵字或特徵快速地搜尋到需要的檔案,就象使用網路搜尋引擎 google 一樣便捷。檔案系統有個特點就是隻有某些檔案會變化,因此桌面搜尋引擎在第一次建立完索引資料庫後,沒必要重複遍歷所有的檔案建立新的索引,它只需要更新修改了的檔案的索引,建立新增加的檔案的索引,刪除已經刪除的檔案的索引就足夠了,這樣桌面搜尋引擎需要做的工作就大大地減少。Inotify 就是為這一意圖專門設計的,beagle 為需要監視的目錄或檔案建立了inotify 例項,然後它就等待該 inotify 上發生檔案系統事件,如果沒有任何檔案變化,beagle 將不需要任何開銷,只有在有被監視的事件發生時,beagle 才被喚醒並根據實際事件來更新對應的檔案的索引,然後繼續睡眠等待下一個檔案系統事件發生。在 SuSe 9.3 和即將釋出的 10.0 中就包含了該桌面搜尋引擎,它能夠為文件、email、音樂、圖象和應用等建立索引。使用過 windows 下的桌面搜尋引擎的讀者對 google 和 yahoo 以及 Microsoft 的桌面搜尋引擎有深刻的體會,感興趣讀者可以安裝 SuSe 使用一下。
六、小結
inotify 是在 2.6.13 中引入的新功能,它為使用者態監視檔案系統的變化提供了強大的支援,本文詳盡地介紹了其起源、核心實現、使用者介面以及使用,有興趣的讀者可以讀 2.6.13的相關原始碼來進一步瞭解其實現細節。