inotify 机制
inotify 是 Linux 平臺特有的一種文件系統事件監視機制。inotify API 提供了一種監視文件系統事件的機制。Inotify 可以被用于監視單獨的文件,或監視目錄。當監視一個目錄時,inotify 將返回目錄本身的事件,以及目錄內的文件的事件。這組 API 使用了如下這些系統調用:
-
inotify_init(2) 創建一個 inotify 實例并返回一個文件描述符指向該 inotify 實例。更近一些的 inotify_init1(2) 與 inotify_init(2) 類似,但它可以帶一個標記參數提供對一些額外功能的訪問。
-
inotify_add_watch(2) 管理與 inotify 實例關聯的 “觀察列表”。觀察列表中的每個項 (“watch”) 指定一個文件或目錄的路徑,以及內核應該監控的路徑指向的文件上發生的事件的集合。inotify_add_watch(2) 創建一個新的觀察項,或修改一個已經存在的觀察。每個觀察具有一個唯一的 “觀察描述符”,當觀察創建時 inotify_add_watch(2) 返回的一個整數。
-
當監測的文件或目錄的事件發生時,這些事件將以結構化數據的形式為應用程序所用,這些事件可以使用 read(2) 從 inotify 文件描述符讀出。
-
inotify_rm_watch(2) 從一個 inotify 觀察列表中移除一個項。
-
當指向 inotify 實例的所有文件描述符都已經被關閉了(使用 close(2)),底層對象及其資源將被內核釋放并重用;所有相關的觀察也被自動地釋放。
通過細心的編程,應用程序可以使用 inotify 高效地監視和緩存一系列文件系統對象的狀態。然而,健壯的應用程序應該允許這樣一個事實,即監控邏輯的缺陷,或下面描述的那種競爭可能會使緩存與文件系統狀態不一致。執行一些一致性檢查,并在探測到不一致時重建緩存可能是明智的。
從 inotify 文件描述符讀取事件
為了確定發生了什么事件,應用程序可以使用 read(2) 從 inotify 文件描述符讀取事件。如果截止目前還沒有事件發生,則,假設是一個阻塞的文件描述符,read(2) 將阻塞直到至少有一個事件發生(除非被一個信號中斷,在那種情況下調用失敗,錯誤碼為 EINTR)。
每個成功的 read(2) 調用返回一個包含了一個或多個如下結構的緩沖區:
struct inotify_event {int wd; /* Watch descriptor */uint32_t mask; /* Mask describing event */uint32_t cookie; /* Unique cookie associating relatedevents (for rename(2)) */uint32_t len; /* Size of name field */char name[]; /* Optional null-terminated name */};wd 是這個事件發生的觀察描述符。它是前面調用 inotify_add_watch(2) 返回的觀察描述符中的一個。
mask 包含描述了發生的事件的位。
cookie 是連接相關事件的唯一的整數。目前,它只被用于重命名事件,并允許應用程序將最終的 IN_MOVED_FROM 和 IN_MOVED_TO 事件對連接起來。對于所有其它的事件類型,cookie 被設置為 0。
name 字段只有當返回的是觀察的目錄內的一個文件的事件時才出現;它表示觀察的目錄內的文件名。這個文件名是 null 結尾的,且可能包含更多的 null 字節 (’\0’) 以對齊后續的讀到一個合適的地址邊界。
len 字段表示 name 中所有字節的個數,包括 null 字節;因此每個 inotify_event 結構的長度為 sizeof(struct inotify_event) + len。
給 read(2) 的緩沖區太小以至于無法返回下一個事件相關的信息時的行為依賴于內核版本:在 2.6.21 之前的內核中,read(2) 返回 0;自內核 2.6.21 開始,read(2) 讀取失敗,錯誤碼為 EINVAL。指定緩沖區大小為 sizeof(struct inotify_event) + NAME_MAX + 1 則最起碼讀取一個事件是足夠的。
inotify 事件
inotify_add_watch(2) 的 mask 參數,和讀取一個 inotify 文件描述符時返回的 inotify_event 結構的 mask 字段都是表示 inotify 事件的位掩碼。當調用 inotify_add_watch(2) 時可以在 mask 中指定,或者 read(2) 返回的 mask 字段中可能返回的,有如下位:
IN_ACCESS (+) 文件被訪問(比如,read(2),execve(2))。
IN_ATTRIB (*) 元數據被修改 - 比如,權限 (如,chmod(2)),時間戳 (如,utimensat(2)),擴展屬性 (setxattr(2)),鏈接計數 (自 Linux 2.6.25;比如,作為 link(2) 和 unlink(2) 的目標),以及用戶/組 ID (比如,chown(2))。
IN_CLOSE_WRITE (+) 為寫入而打開的文件已關閉。
IN_CLOSE_NOWRITE (*) 為文件或目錄的非寫入打開已關閉。
IN_CREATE (+) 在觀察的目錄中創建了文件/目錄 (比如,open(2) O_CREAT,mkdir(2),link(2),symlink(2),UNIX 域 socket 上的 bind(2))。
IN_DELETE (+) 從監視目錄中刪除的文件/目錄。
IN_DELETE_SELF 觀察的文件/目錄本身被刪除。(這個事件也發生在一個對象被移動到另一個文件系統時,因為 mv(1) 在效果上等于將文件拷貝到另一個文件系統,然后將它從最初的文件系統中刪除)。此外,后續還會為觀察描述符生成一個 IN_IGNORED 事件。
IN_MODIFY (+) 文件別修改 (比如,write(2),truncate(2))。
IN_MOVE_SELF 觀察的文件/目錄本身被移動。
IN_MOVED_FROM (+) 當一個文件被重命名時目錄包含舊的文件名時生成。
IN_MOVED_TO (+) 當一個文件被重命名時目錄包含新的文件名時生成。
IN_OPEN (*) 文件或目錄被打開。
Inotify 監視是基于 inode 的:當監視一個文件時(但監視包含一個文件的目錄不是),對于文件的任何鏈接(在相同或不同的目錄中)上的活動都會生成一個事件。
當監視一個目錄時:
-
上面標有星號 (*) 的事件既可以發生在目錄本身,也可以發生在目錄內的對象上;以及
-
標有加號 (+) 的事件僅發生在目錄內的對象(而不是目錄本身)。
注意:監視目錄時,如果事件是通過位于被監視目錄之外的路徑名(即鏈接)執行的,則不會為目錄內的文件生成事件。
當為被監視目錄中的對象生成事件時,返回的 inotify_event 結構中的 name 字段標識目錄中文件的名稱。
IN_ALL_EVENTS 宏被定義為所有上述事件的位掩碼。這個宏也可以在調用 inotify_add_watch(2) 時用作掩碼參數。
還定義了兩個額外的便利宏:
IN_MOVE 等于 IN_MOVED_FROM | IN_MOVED_TO。
IN_CLOSE 等于 IN_CLOSE_WRITE | IN_CLOSE_NOWRITE。
調用 inotify_add_watch(2) 時,還可以在 mask 中指定以下位:
IN_DONT_FOLLOW (自 Linux 2.6.15) 如果它是符號鏈接,就不要解引用路徑名。
IN_EXCL_UNLINK (自 Linux 2.6.36) 默認情況下,在觀察目錄的子文件或目錄的事件時,即使子文件或目錄已從目錄中取消鏈接,也會為子文件或目錄生成事件。對于某些應用程序,這可能會導致大量不感興趣的事件(例如,如果觀察 /tmp,其中許多應用程序會創建名稱將立即被取消鏈接的臨時文件)。指定 IN_EXCL_UNLINK 改變默認行為,以使被觀察的目錄的子目錄或文件被從被觀察目錄取消鏈接后不會再生成事件。
IN_MASK_ADD 如果對應于路徑名的文件系統對象的監視實例已經存在,則將掩碼中的事件添加(或)到監視掩碼(而不是替換掩碼); 如果還指定了 IN_MASK_CREATE,則會導致錯誤 EINVAL。
IN_ONESHOT 監視路徑名對應的文件系統對象的一個事件,然后從監視列表中刪除。
IN_ONLYDIR (自 Linux 2.6.15) 僅當它是目錄時才觀察路徑名; 如果路徑名不是目錄,則會導致錯誤 ENOTDIR。 使用此標志為應用程序提供了一種確保被監視對象是目錄的無競爭方式。
IN_MASK_CREATE (自 Linux 4.18) 僅當它還沒有與之關聯的監視時才監視路徑名; 如果路徑名已被監視,則會導致錯誤 EEXIST。
使用此標志為應用程序提供了一種確保新監視不會修改現有監視的方法。 這很有用,因為多個路徑可能引用同一個 inode,并且在沒有此標志的情況下多次調用 inotify_add_watch(2) 可能會破壞現有的監視掩碼。
在 read(2) 返回的 mask 字段中可能設置以下位:
IN_IGNORED 監視被顯式刪除(inotify_rm_watch(2))或自動刪除(文件被刪除,或文件系統被卸載)。
IN_ISDIR 此事件的主題是一個目錄。
IN_Q_OVERFLOW 事件隊列溢出(此事件的 wd 為 -1)。
IN_UNMOUNT 包含被監視對象的文件系統已卸載。 此外,隨后將為監視描述符生成一個 IN_IGNORED 事件。
例子
假設應用程序正在監視目錄 dir 和文件 dir/myfile 中的所有事件。 下面的示例顯示了將為這兩個對象生成的一些事件。
fd = open("dir/myfile", O_RDWR);
為 dir 和 dir/myfile 生成 IN_OPEN 事件。
read(fd, buf, count);
為 dir 和 dir/myfile 生成 IN_ACCESS 事件。
write(fd, buf, count);
為 dir 和 dir/myfile 生成 IN_MODIFY 事件。
fchmod(fd, mode);
為 dir 和 dir/myfile 生成 IN_ATTRIB 事件。
close(fd);
為 dir 和 dir/myfile 生成 IN_CLOSE_WRITE 事件。
假設一個應用程序正在監視目錄 dir1 和 dir2,以及文件 dir1/myfile。 以下示例顯示了可能生成的一些事件。
link("dir1/myfile", "dir2/new");
為 myfile 生成一個 IN_ATTRIB 事件,為 dir2 生成一個 IN_CREATE 事件。
rename("dir1/myfile", "dir2/myfile");
為 dir1 生成一個 IN_MOVED_FROM 事件,為 dir2 生成一個 IN_MOVED_TO 事件,為 myfile 生成一個 IN_MOVE_SELF 事件。 IN_MOVED_FROM 和 IN_MOVED_TO 事件將具有相同的 cookie 值。
假設 dir1/xx 和 dir2/yy 是(唯一的)指向同一個文件的鏈接,并且應用程序正在監視 dir1、dir2、dir1/xx 和 dir2/yy。 按照下面給出的順序執行以下調用將生成以下事件:
unlink("dir2/yy");
為 xx 生成一個 IN_ATTRIB 事件(因為它的鏈接計數發生變化),為 dir2 生成一個 IN_DELETE 事件。
unlink("dir1/xx");
為 xx 生成 IN_ATTRIB、IN_DELETE_SELF 和 IN_IGNORED 事件,為 dir1 生成 IN_DELETE 事件。
假設一個應用程序正在監視目錄 dir 和(空的)目錄 dir/subdir。 以下示例顯示了可能生成的一些事件。
mkdir("dir/new", mode);
為 dir 生成一個 IN_CREATE | IN_ISDIR 事件。
rmdir("dir/subdir");
為 subdir 生成 IN_DELETE_SELF 和 IN_IGNORED 事件,為 dir 生成一個 IN_DELETE | IN_ISDIR 事件。
/proc 接口
以下接口可用于限制 inotify 消耗的內核內存量:
/proc/sys/fs/inotify/max_queued_events
當應用程序調用 inotify_init(2) 設置可以排隊到相應 inotify 實例的事件數上限時,將使用此文件中的值。超過此限制的事件將被丟棄,但始終會生成 IN_Q_OVERFLOW 事件。
/proc/sys/fs/inotify/max_user_instances
這指定了每個真實用戶 ID 可以創建的 inotify 實例數的上限。
/proc/sys/fs/inotify/max_user_watches
這指定了每個真實用戶 ID 可以創建的監視數量的上限。
版本
Inotify 被合并到 2.6.13 Linux 內核中。 所需的庫接口已添加到 glibc 2.4 版中。(在 glibc 2.5 版中添加了 IN_DONT_FOLLOW、IN_MASK_ADD 和 IN_ONLYDIR。)
Inotify API 是 Linux 特有的。
注意
可以使用 select(2)、poll(2) 和 epoll(7) 監視 Inotify 文件描述符。 當事件可用時,文件描述符指示為可讀。
從 Linux 2.6.25 開始,信號驅動的 I/O 通知可用于 inotify 文件描述符; 請參閱 fcntl(2) 中對 F_SETFL(用于設置 O_ASYNC 標志)、F_SETOWN 和 F_SETSIG 的討論。傳遞給信號處理程序的 siginfo_t 結構(在 sigaction(2) 中描述)具有以下字段集: si_fd 設置為 inotify 文件描述符編號; si_signo 設置為信號編號; si_code 設置為 POLL_IN; 并且 POLLIN 設置在 si_band 中。
如果在 inotify 文件描述符上產生的連續輸出 inotify 事件是相同的(相同的 wd、掩碼、cookie 和名稱),則如果尚未讀取較舊的事件,則它們將合并為單個事件。這減少了事件隊列所需的內核內存量,但也意味著應用程序不能使用 inotify 來可靠地計算文件事件。
從 inotify 文件描述符讀取返回的事件形成有序隊列。因此,例如,可以保證當從一個目錄重命名到另一個目錄時,事件將在 inotify 文件描述符上以正確的順序生成。
通過 inotify 文件描述符監視的監視描述符集可以通過進程的 /proc/[pid]/fdinfo 目錄中的 inotify 文件描述符條目查看。有關詳細信息,請參閱 proc(5)。 FIONREAD ioctl(2) 返回可從 inotify 文件描述符讀取的字節數。
限制和警告
Inotify API 不提供關于觸發 inotify 事件的用戶或進程的信息。特別是,對于通過 inotify 監視事件的進程來說,沒有簡單的方法來區分它自己觸發的事件和其他進程觸發的事件。
Inotify 只報告用戶空間程序通過文件系統 API 觸發的事件。因此,它不捕獲發生在網絡文件系統上的遠程事件。(應用程序必須退回到輪詢文件系統以捕獲此類事件。)此外,各種偽文件系統(例如 /proc、/sys 和 /dev/pts)無法使用 inotify 進行監控。
Inotify API 不會報告由于 mmap(2)、msync(2) 和 munmap(2) 而可能發生的文件訪問和修改。
Inotify API 通過文件名識別受影響的文件。但是,當應用程序處理 inotify 事件時,文件名可能已經被刪除或重命名。
Inotify API 通過監視描述符識別事件。緩存監視描述符和路徑名之間的映射(如果需要)是應用程序的責任。請注意,目錄重命名可能會影響多個緩存路徑名。
Inotify 監視目錄不是遞歸的:要監視目錄下的子目錄,必須創建額外的監視。對于大型目錄樹,這可能會花費大量時間。
如果監視整個目錄子樹,并且在該樹中創建了一個新的子目錄,或者將現有目錄重命名到該樹下,請注意,當你為新子目錄創建監視時,新文件(和子目錄)可能已經存在子目錄里面了。因此,你可能希望在添加監視后立即掃描子目錄的內容(如果需要,可以遞歸地為其包含的任何子目錄添加監視)。
請注意,事件隊列可能會溢出。在這種情況下,事件會丟失。健壯的應用程序應該優雅地處理丟失事件的可能性。例如,可能需要重建部分或全部應用程序緩存。(一種簡單,但可能代價高昂的方法是關閉 inotify 文件描述符,清空緩存,創建新的 inotify 文件描述符,然后為要監視的對象重新創建監視和緩存條目。)
如果文件系統掛載在受監視目錄的頂部,則不會生成任何事件,并且不會為新掛載點下的對象生成任何事件。如果隨后卸載文件系統,則隨后將為目錄及其包含的對象生成事件。
處理 rename() 事件
如上所述,由 rename(2) 生成的 IN_MOVED_FROM 和 IN_MOVED_TO 事件對可以通過它們的共享 cookie 值進行匹配。然而,匹配的任務有一些挑戰。
在從 inotify 文件描述符讀取時,這兩個事件在可用的事件流中通常是連續的。但是,這不能保證。如果多個進程正在觸發受監視對象的事件,那么(在極少數情況下)IN_MOVED_FROM 和 IN_MOVED_TO 事件之間可能會出現任意數量的其他事件。此外,不能保證事件對被原子地插入到隊列中:可能經過一個短暫的間隔 IN_MOVED_FROM 就出現了,但 IN_MOVED_TO 沒有出現。
因此,匹配由 rename(2) 生成的 IN_MOVED_FROM 和 IN_MOVED_TO 事件對本質上是活潑的。(不要忘記,如果一個對象在受監視目錄之外被重命名,甚至可能沒有 IN_MOVED_TO 事件。)在大多數情況下,啟發式方法(例如,假設事件總是連續的)可用于確保匹配,但不可避免地會錯過某些情況,導致應用程序將 IN_MOVED_FROM 和 IN_MOVED_TO 事件視為不相關。如果監視描述符因此被銷毀并重新創建,那么這些監視描述符將與任何未決事件中的監視描述符不一致。(重新創建 inotify 文件描述符并重建緩存可能有助于處理這種情況。)
應用程序還應該允許 IN_MOVED_FROM 事件是可以放入當前調用 read(2) 返回的緩沖區中的最后一個事件,并且可能在下一次 read(2) 時獲取隨附的 IN_MOVED_TO 事件,這應該使用(小的)超時來完成,以允許插入 IN_MOVED_FROM-IN_MOVED_TO 事件對不是原子的,并且也可能沒有任何 IN_MOVED_TO 事件。
缺陷
在 Linux 3.19 之前,fallocate(2) 沒有創建任何 inotify 事件。 從 Linux 3.19 開始,對 fallocate(2) 的調用會生成 IN_MODIFY 事件。
在 2.6.16 之前的內核中,IN_ONESHOT 掩碼標志不起作用。
按照最初的設計和實現,當在一個事件之后丟棄一個監視時,IN_ONESHOT 標記不導致生成一個 IN_IGNORED 事件。但是,由于其他更改的意外影響,從 Linux 2.6.36 開始,在這種情況下會生成 IN_IGNORED 事件。
在內核 2.6.25 之前,旨在合并連續相同事件的內核代碼(即,如果尚未讀取較舊的事件,則可能合并兩個最近的事件)代替了檢查最近的事件是否可以與最舊的未讀事件。
當通過調用 inotify_rm_watch(2) 刪除監視描述符(或因為刪除監視的文件或卸載包含它的文件系統)時,該監視描述符的任何未決未讀事件仍然可供讀取。由于監視描述符是隨后使用 inotify_add_watch(2) 分配的,因此內核在可能的監視描述符(0 到 INT_MAX)的范圍內逐步循環。在分配空閑監視描述符時,不會檢查該監視描述符編號是否在 inotify 隊列中有任何未決的未讀事件。因此,即使一個監視描述符號的前一個化身存在未決未讀事件,也可能會重新分配該監視描述符,結果應用程序可能會讀取這些事件,并將它們解釋為屬于與新的回收的監視描述符關聯的文件。實際上,遇到此錯誤的可能性可能非常低,因為它要求應用程序循環遍歷INT_MAX 個監視描述符,釋放監視描述符,同時將該監視描述符的未讀事件留在隊列中,然后回收該監視描述符。出于這個原因,并且由于沒有關于實際應用程序中出現該錯誤的報告,截至 Linux 3.15,尚未進行內核更改以消除此可能的錯誤。
示例
下面的程序演示了 inotify API 的用法。它標記作為命令行參數傳遞的目錄并等待 IN_OPEN、IN_CLOSE_NOWRITE 和 IN_CLOSE_WRITE 類型的事件。
程序源碼:
#include <errno.h>#include <poll.h>#include <stdio.h>#include <stdlib.h>#include <sys/inotify.h>#include <unistd.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 voidhandle_events(int fd, int *wd, int argc, char* argv[]){/* Some systems cannot read integer variables if they are notproperly aligned. On other systems, incorrect alignment maydecrease performance. Hence, the buffer used for reading fromthe inotify file descriptor should have the same alignment asstruct inotify_event. */char buf[4096]__attribute__ ((aligned(__alignof__(struct inotify_event))));const struct inotify_event *event;int i;ssize_t len;char *ptr;/* Loop while events can be read from inotify file descriptor. */for (;;) {/* Read some events. */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, thenit 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 (ptr = buf; ptr < buf + len;ptr += sizeof(struct inotify_event) + event->len) {event = (const struct inotify_event *) ptr;/* Print event type */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 (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");elseprintf(" [file]\n");}}}intmain(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);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 */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 */nfds = 2;/* Console input */fds[0].fd = STDIN_FILENO;fds[0].events = POLLIN;/* Inotify input */fds[1].fd = fd;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);}示例輸出:
$ ./a.out /tmp /home/user/tempPress enter key to terminate.Listening for events.IN_OPEN: /home/user/temp/foo [file]IN_CLOSE_WRITE: /home/user/temp/foo [file]IN_OPEN: /tmp/ [directory]IN_CLOSE_NOWRITE: /tmp/ [directory]Listening for events stopped.該輸出是在編輯文件 /home/user/temp/foo 和列出目錄 /tmp 時記錄的。文件和目錄被打開前,IN_OPEN 事件發生。文件被關閉后,發生了一個 IN_CLOSE_WRITE 事件。目錄被關閉后,發生了一個 IN_CLOSE_NOWRITE 事件。當用戶按下 ENTER 鍵時程序執行結束。
總結
以上是生活随笔為你收集整理的inotify 机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Windows 平台编译 WebRTC
- 下一篇: 图像操作入门