Giới thiệu về inotify
Từ kernel Linux 2.6.13, Linux đã giới thiệu inotify – cơ chế cho phép theo dõi một tập hợp các sự kiện được chỉ định trên tệp hoặc thư mục, ví dụ như mở, đóng, di chuyển/đổi tên, xóa, tạo mới hoặc thay đổi thuộc tính.
inotify cung cấp các API sau:
inotify_init
#include <sys/inotify.h>
int inotify_init(void);
int inotify_init1(int flags);
Tạo một instance inotify và trả về bộ mô tả tệp tương ứng.
inotify_add_watch
#include <sys/inotify.h>
int inotify_add_watch(int fd, const char *pathname, uint32_t mask);
Thao tác trên "danh sách theo dõi" (watch list) liên kết với instance inotify (tạo mới hoặc cập nhật mục đã tồn tại).
Khi các sự kiện xảy ra trên tệp/thư mục được theo dõi, ta có thể dùng read để đọc thông tin sự kiện từ bộ mô tả tệp inotify.
inotify_rm_watch
#include <sys/inotify.h>
int inotify_rm_watch(int fd, int wd);
Xóa một mục khỏi danh sách theo dõi inotify.
Khi tất cả các bộ mô tả tệp liên quan đến một instance inotify đều bị đóng (bằng close), kernel sẽ giải phóng các đối tượng bên dưới cùng tài nguyên tương ứng để tái sử dụng; mọi mục theo dõi liên quan sẽ được tự động giải phóng.
Như vậy, một chương trình giám sát điển hình cần thực hiện theo các bước sau:
- Gọi
inotify_inithoặcinotify_init1để lấy bộ mô tả tệp. - Gọi
inotify_add_watchđể thêm một hoặc nhiều tệp/thư mục cần theo dõi. - Chờ cho sự kiện xảy ra.
- Xử lý sự kiện và tiếp tục chờ sự kiện tiếp theo.
- Khi không còn nhu cầu giám sát, đóng bộ mô tả tệp, dọn dẹp và thoát.
Sử dụng
Đọc sự kiện từ bộ mô tả tệp inotify
Để biết sự kiện nào đã xảy ra, ta đọc bộ mô tả tệp inotify tương ứng bằng read. Nếu bộ mô tả tệp ở chế độ blocking, read sẽ chờ cho đến khi có sự kiện xảy ra.
Cấu trúc dữ liệu sự kiện nhận được từ bộ mô tả tệp inotify:
struct inotify_event
{
int wd; /* Bộ mô tả theo dõi */
uint32_t mask; /* Mặt nạ mô tả loại sự kiện */
uint32_t cookie; /* Cookie duy nhất liên kết các sự kiện có quan hệ
(dành cho rename(2)) */
uint32_t len; /* Kích thước trường name */
char name[]; /* Tên tùy chọn kết thúc bằng null */
};
wd: định danh đối tượng đang được theo dõi đã xảy ra sự kiện; đây chính là bộ mô tả theo dõi trả về từ inotify_add_watch trước đó.
mask: chứa các cờ tương ứng với sự kiện đã xảy ra.
cookie: số nguyên duy nhất dùng để liên kết các sự kiện có quan hệ với nhau. Hiện tại chỉ dùng cho sự kiện đổi tên, giúp nối sự kiện IN_MOVED_FROM với sự kiện IN_MOVED_TO tương ứng. Đối với mọi loại sự kiện khác, cookie được đặt bằng 0.
name: chỉ được trả về khi theo dõi thư mục; chứa tên tệp nằm trong thư mục đó (chuỗi kết thúc bằng ký tự null '\0', có thể có nhiều ký tự null để căn chỉnh bộ nhớ).
len: tổng số ký tự trong name, bao gồm tất cả các ký tự null. Do đó, kích thước đầy đủ của một inotify_event là sizeof(struct inotify_event) + len.
Các sự kiện inotify
Tham số mask trong inotify_add_watch cũng như trong inotify_event đều là mặt nạ bit định nghĩa các sự kiện inotify.
Các sự kiện sau có thể đồng thời được dùng làm tham số mask cho inotify_add_watch và xuất hiện trong mask của inotify_event:
IN_ACCESS (+)
Tệp được truy cập (ví dụ: read(2), execve(2)).
IN_ATTRIB (*)
Siêu dữ liệu thay đổi — ví dụ: quyền truy cập (chmod(2)), dấu thời gian
(utimensat(2)), thuộc tính mở rộng (setxattr(2)), số liên kết (từ Linux 2.6.25;
ví dụ: với đích của link(2) và với unlink(2)), và ID người dùng/nhóm (chown(2)).
IN_CLOSE_WRITE (+)
Tệp được mở để ghi đã bị đóng.
IN_CLOSE_NOWRITE (*)
Tệp hoặc thư mục không được mở để ghi đã bị đóng.
IN_CREATE (+)
Tệp/thư mục được tạo trong thư mục được theo dõi (ví dụ: open(2) với O_CREAT,
mkdir(2), link(2), symlink(2), bind(2) trên socket UNIX).
IN_DELETE (+)
Tệp/thư mục bị xóa khỏi thư mục được theo dõi.
IN_DELETE_SELF
Bản thân tệp/thư mục được theo dõi đã bị xóa. (Sự kiện này cũng xảy ra khi đối tượng
bị di chuyển sang hệ thống tệp khác, vì mv(1) thực chất sao chép tệp sang hệ thống
tệp mới rồi xóa nó khỏi hệ thống tệp cũ.) Ngoài ra, một sự kiện IN_IGNORED sẽ được
sinh ra sau đó cho bộ mô tả theo dõi này.
IN_MODIFY (+)
Tệp bị sửa đổi (ví dụ: write(2), truncate(2)).
IN_MOVE_SELF
Bản thân tệp/thư mục được theo dõi đã bị di chuyển.
IN_MOVED_FROM (+)
Được sinh ra cho thư mục chứa tên cũ khi một tệp bị đổi tên.
IN_MOVED_TO (+)
Được sinh ra cho thư mục chứa tên mới khi một tệp bị đổi tên.
IN_OPEN (*)
Tệp hoặc thư mục đã được mở.
inotify theo dõi dựa trên inode, vì vậy khi một tệp được theo dõi xảy ra sự kiện, các liên kết khác trỏ đến cùng tệp đó cũng sẽ phát sinh sự kiện, ngay cả khi chúng nằm ở các thư mục khác nhau.
Khi theo dõi một thư mục:
- Các sự kiện đánh dấu * ở trên có thể xảy ra đồng thời trên chính thư mục được theo dõi và trên các đối tượng bên trong thư mục đó.
- Các sự kiện đánh dấu + chỉ xảy ra trên các đối tượng bên trong thư mục, không xảy ra trên chính thư mục.
Khi theo dõi một thư mục, nếu một tệp bên trong thư mục là liên kết trỏ đến tệp nằm ngoài thư mục, inotify sẽ không phát sinh sự kiện tương ứng cho tệp được trỏ đến đó.
Khi một tệp bên trong thư mục được theo dõi phát sinh sự kiện, trường name trong inotify_event sẽ chứa tên của tệp đó.
Hằng số IN_ALL_EVENTS có thể dùng để chỉ tất cả các sự kiện nêu trên khi gọi inotify_add_watch.
Ngoài ra còn có hai hằng số thường dùng:
IN_MOVE
Tương đương IN_MOVED_FROM | IN_MOVED_TO.
IN_CLOSE
Tương đương IN_CLOSE_WRITE | IN_CLOSE_NOWRITE.
Các cờ đặc biệt sau đây có thể sử dụng trong inotify_add_watch:
IN_DONT_FOLLOW (từ Linux 2.6.15)
Không giải tham chiếu pathname nếu nó là liên kết tượng trưng.
IN_EXCL_UNLINK (từ Linux 2.6.36)
Theo mặc định, khi theo dõi sự kiện trên các phần tử con của một thư mục, sự kiện
vẫn được sinh ra cho các phần tử con ngay cả sau khi chúng đã bị hủy liên kết khỏi
thư mục. Điều này có thể dẫn đến một lượng lớn sự kiện không cần thiết đối với một số
ứng dụng (ví dụ: khi theo dõi /tmp, nơi nhiều ứng dụng tạo tệp tạm rồi hủy liên kết
tên tệp ngay lập tức). Chỉ định IN_EXCL_UNLINK sẽ thay đổi hành vi mặc định: không
còn sinh ra sự kiện cho các phần tử con sau khi chúng bị hủy liên kết khỏi thư mục
được theo dõi.
IN_MASK_ADD
Nếu một mục theo dõi đã tồn tại cho đối tượng hệ thống tệp tương ứng với pathname,
các sự kiện trong mask sẽ được cộng (phép OR) vào mặt nạ theo dõi hiện tại (thay vì
thay thế toàn bộ mặt nạ); lỗi EINVAL xảy ra nếu đồng thời chỉ định IN_MASK_CREATE.
IN_ONESHOT
Theo dõi đối tượng hệ thống tệp tương ứng với pathname chỉ trong một sự kiện, sau đó
tự xóa khỏi danh sách theo dõi.
IN_ONLYDIR (từ Linux 2.6.15)
Chỉ theo dõi pathname nếu nó là thư mục; lỗi ENOTDIR xảy ra nếu pathname không phải
là thư mục. Cờ này giúp ứng dụng đảm bảo một cách an toàn (không bị race condition)
rằng đối tượng được theo dõi là một thư mục.
IN_MASK_CREATE (từ Linux 4.18)
Chỉ theo dõi pathname nếu nó chưa có mục theo dõi nào liên kết; lỗi EEXIST xảy ra nếu
pathname đã được theo dõi trước đó. Cờ này giúp ứng dụng đảm bảo các mục theo dõi mới
không sửa đổi các mục đã tồn tại. Điều này hữu ích vì nhiều đường dẫn khác nhau có thể
trỏ đến cùng một inode, và nhiều lần gọi inotify_add_watch(2) mà không dùng cờ này có
thể ghi đè mặt nạ của các mục theo dõi đã tồn tại.
Các cờ sau có thể xuất hiện trong inotify_event:
IN_IGNORED
Mục theo dõi bị xóa một cách tường minh (inotify_rm_watch(2)) hoặc tự động
(tệp đã bị xóa, hoặc hệ thống tệp đã được gỡ bỏ). Xem thêm phần BUGS.
IN_ISDIR
Đối tượng của sự kiện này là một thư mục.
IN_Q_OVERFLOW
Hàng đợi sự kiện bị tràn (wd bằng -1 đối với sự kiện này).
IN_UNMOUNT
Hệ thống tệp chứa đối tượng được theo dõi đã bị gỡ bỏ. Ngoài ra, một sự kiện
IN_IGNORED sẽ được sinh ra sau đó cho bộ mô tả theo dõi này.
Ví dụ về sự kiện
Giả sử một ứng dụng đang theo dõi thư mục dir và tệp dir/myfile với tất cả các sự kiện. Các ví dụ dưới đây minh họa những sự kiện sẽ được sinh ra cho hai đối tượng này.
fd = open("dir/myfile", O_RDWR);
Sinh ra sự kiện IN_OPEN cho cả dir lẫn dir/myfile.
read(fd, buf, count);
Sinh ra sự kiện IN_ACCESS cho cả dir lẫn dir/myfile.
write(fd, buf, count);
Sinh ra sự kiện IN_MODIFY cho cả dir lẫn dir/myfile.
fchmod(fd, mode);
Sinh ra sự kiện IN_ATTRIB cho cả dir lẫn dir/myfile.
close(fd);
Sinh ra sự kiện IN_CLOSE_WRITE cho cả dir lẫn dir/myfile.
Giả sử một ứng dụng đang theo dõi các thư mục dir1, dir2 và tệp dir1/myfile. Các ví dụ dưới đây cho thấy những sự kiện có thể được sinh ra.
link("dir1/myfile", "dir2/new");
Sinh ra sự kiện IN_ATTRIB cho myfile và sự kiện IN_CREATE cho dir2.
rename("dir1/myfile", "dir2/myfile");
Sinh ra sự kiện IN_MOVED_FROM cho dir1, sự kiện IN_MOVED_TO cho dir2 và sự kiện
IN_MOVE_SELF cho myfile. Sự kiện IN_MOVED_FROM và IN_MOVED_TO sẽ có cùng giá trị cookie.
Giả sử dir1/xx và dir2/yy là hai liên kết (duy nhất) trỏ đến cùng một tệp, và một ứng dụng đang theo dõi dir1, dir2, dir1/xx và dir2/yy. Thực hiện các lời gọi sau theo đúng thứ tự sẽ sinh ra các sự kiện tương ứng:
unlink("dir2/yy");
Sinh ra sự kiện IN_ATTRIB cho xx (vì số liên kết của nó thay đổi) và sự kiện
IN_DELETE cho dir2.
unlink("dir1/xx");
Sinh ra các sự kiện IN_ATTRIB, IN_DELETE_SELF và IN_IGNORED cho xx, cùng sự kiện
IN_DELETE cho dir1.
Giả sử một ứng dụng đang theo dõi thư mục dir và thư mục con (rỗng) dir/subdir. Các ví dụ dưới đây cho thấy những sự kiện có thể được sinh ra.
mkdir("dir/new", mode);
Sinh ra sự kiện IN_CREATE | IN_ISDIR cho dir.
rmdir("dir/subdir");
Sinh ra các sự kiện IN_DELETE_SELF và IN_IGNORED cho subdir, cùng sự kiện
IN_DELETE | IN_ISDIR cho dir.
Cấu hình hệ thống
Các giao diện sau được dùng để giới hạn lượng bộ nhớ kernel mà inotify có thể tiêu thụ:
/proc/sys/fs/inotify/max_queued_events
Giá trị trong tệp này được dùng khi ứng dụng gọi inotify_init() để đặt giới hạn trên cho số lượng sự kiện có thể xếp hàng đợi vào instance inotify tương ứng. Các sự kiện vượt quá giới hạn sẽ bị loại bỏ, nhưng sự kiện IN_Q_OVERFLOW luôn được sinh ra.
/proc/sys/fs/inotify/max_user_instances
Giới hạn trên cho số lượng instance inotify có thể được tạo ra trên mỗi ID người dùng thực.
/proc/sys/fs/inotify/max_user_watches
Giới hạn trên cho số lượng mục theo dõi có thể được tạo ra trên mỗi ID người dùng thực.
Mã nguồn
// Tham khảo: https://man7.org/linux/man-pages/man7/inotify.7.html
#include <errno.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/inotify.h>
#include <unistd.h>
/* Đọc và xử lý tất cả các sự kiện inotify có sẵn từ bộ mô tả tệp 'notify_fd'.
watch_descriptors là mảng các bộ mô tả theo dõi tương ứng với từng thư mục
trong 'paths'.
'count' là số lượng phần tử của watch_descriptors và paths.
'paths' là danh sách các thư mục đang được theo dõi.
Phần tử 0 của cả hai mảng không được sử dụng. */
static void process_inotify_events(int notify_fd, int *watch_descriptors,
int count, char *paths[])
{
/* Một số hệ thống không thể đọc biến số nguyên nếu chúng không được
căn chỉnh đúng. Ở các hệ thống khác, việc căn chỉnh sai có thể làm
giảm hiệu năng. Do đó, bộ đệm dùng để đọc từ bộ mô tả tệp inotify
nên có cùng độ căn chỉnh với struct inotify_event. */
char event_buffer[4096]
__attribute__((aligned(__alignof__(struct inotify_event))));
const struct inotify_event *cur_event;
ssize_t bytes_read;
/* Lặp trong khi vẫn còn sự kiện có thể đọc từ bộ mô tả tệp inotify. */
for (;;) {
/* Đọc một số sự kiện. */
bytes_read = read(notify_fd, event_buffer, sizeof(event_buffer));
if (bytes_read == -1 && errno != EAGAIN) {
perror("read");
exit(EXIT_FAILURE);
}
/* Nếu read() không chặn không tìm thấy sự kiện nào để đọc, nó
trả về -1 với errno được đặt thành EAGAIN. Trong trường hợp đó,
ta thoát khỏi vòng lặp. */
if (bytes_read <= 0)
break;
/* Duyệt qua tất cả các sự kiện trong bộ đệm. */
for (char *ptr = event_buffer;
ptr < event_buffer + bytes_read;
ptr += sizeof(struct inotify_event) + cur_event->len) {
cur_event = (const struct inotify_event *)ptr;
/* In loại sự kiện. */
if (cur_event->mask & IN_OPEN)
printf("IN_OPEN: ");
if (cur_event->mask & IN_CLOSE_NOWRITE)
printf("IN_CLOSE_NOWRITE: ");
if (cur_event->mask & IN_CLOSE_WRITE)
printf("IN_CLOSE_WRITE: ");
/* In tên thư mục đang được theo dõi. */
for (int idx = 1; idx < count; ++idx) {
if (watch_descriptors[idx] == cur_event->wd) {
printf("%s/", paths[idx]);
break;
}
}
/* In tên tệp. */
if (cur_event->len)
printf("%s", cur_event->name);
/* In loại đối tượng của hệ thống tệp. */
if (cur_event->mask & IN_ISDIR)
printf(" [thư mục]\n");
else
printf(" [tệp]\n");
}
}
}
int main(int argc, char *argv[])
{
char input_char;
int notify_fd, idx, poll_result;
int *watch_descriptors;
nfds_t num_fds;
struct pollfd pfds[2];
if (argc < 2) {
printf("Cách dùng: %s ĐƯỜNG_DẪN [ĐƯỜNG_DẪN ...]\n", argv[0]);
exit(EXIT_FAILURE);
}
printf("Nhấn phím ENTER để kết thúc.\n");
/* Tạo bộ mô tả tệp để truy cập API inotify. */
notify_fd = inotify_init1(IN_NONBLOCK);
if (notify_fd == -1) {
perror("inotify_init1");
exit(EXIT_FAILURE);
}
/* Cấp phát bộ nhớ cho mảng các bộ mô tả theo dõi. */
watch_descriptors = calloc(argc, sizeof(int));
if (watch_descriptors == NULL) {
perror("calloc");
exit(EXIT_FAILURE);
}
/* Đăng ký theo dõi các thư mục với các sự kiện:
- tệp được mở
- tệp được đóng */
for (idx = 1; idx < argc; idx++) {
watch_descriptors[idx] =
inotify_add_watch(notify_fd, argv[idx], IN_OPEN | IN_CLOSE);
if (watch_descriptors[idx] == -1) {
fprintf(stderr, "Không thể theo dõi '%s': %s\n", argv[idx],
strerror(errno));
exit(EXIT_FAILURE);
}
}
/* Chuẩn bị cho poll. */
num_fds = 2;
pfds[0].fd = STDIN_FILENO; /* Dữ liệu nhập từ bàn phím */
pfds[0].events = POLLIN;
pfds[1].fd = notify_fd; /* Đầu vào inotify */
pfds[1].events = POLLIN;
/* Chờ sự kiện và/hoặc tín hiệu từ bàn phím. */
printf("Đang lắng nghe sự kiện.\n");
while (1) {
poll_result = poll(pfds, num_fds, -1);
if (poll_result == -1) {
if (errno == EINTR)
continue;
perror("poll");
exit(EXIT_FAILURE);
}
if (poll_result > 0) {
if (pfds[0].revents & POLLIN) {
/* Có dữ liệu từ bàn phím: đọc hết stdin và thoát. */
while (read(STDIN_FILENO, &input_char, 1) > 0 &&
input_char != '\n')
continue;
break;
}
if (pfds[1].revents & POLLIN) {
/* Có sự kiện inotify cần xử lý. */
process_inotify_events(notify_fd, watch_descriptors,
argc, argv);
}
}
}
printf("Đã dừng lắng nghe sự kiện.\n");
/* Đóng bộ mô tả tệp inotify. */
close(notify_fd);
free(watch_descriptors);
exit(EXIT_SUCCESS);
}
Kết quả chạy thử:
$ ./a.out /tmp /home/user/temp
Nhấn phím ENTER để kết thúc.
Đang lắng nghe sự kiện.
IN_OPEN: /home/user/temp/foo [tệp]
IN_CLOSE_WRITE: /home/user/temp/foo [tệp]
IN_OPEN: /tmp/ [thư mục]
IN_CLOSE_NOWRITE: /tmp/ [thư mục]
Đã dừng lắng nghe sự kiện.
Tham khảo
Filesystem notification, part 1: An overview of dnotify and inotify
Filesystem notification, part 2: A deeper investigation of inotify
inotify — a powerful yet simple file change notification system
Monitor Linux file system events with inotify