Linux 内核 2.6.13 以后,引入了 inotify 文件系统监控功能,通过 inotify 可以对敏感目录设置事件监听。这样的功能被也被包装成了一个文件监控神器 inotify-tools。
使用 inotify 进行文件监控的过程:
- 创建 inotify 实例,获取 inotify 事件队列文件描述符
- 为监控的文件逐一添加 watch,绑定 inotify 事件队列文件描述符,确定监控事件
- 使用 inotify 事件队列文件描述符读取产生的监控事件
- 完成以上操作后,关闭inotify事件队列文件描述符
除了以上的核心过程,一个文件监控系统还需要包含:监控文件的获取、监控事件的解析和数据补充。
inotify 文件事件监控核心部分所涉及的 API 如下(包含在 <sys/inotify.h> 中):
read 每次通过文件描述符读取的 inotify 事件队列中一个事件,事件的 mask 标记了文件发生的事件。inotify 事件的数据结构如下:
cpp
/* 创建 inotify 实例,获取文件描述符 fd */
int inotify_init(void);//初始化一个新的 inotify 实例,返回一个与新的 inotify 事件队列关联的文件描述符
int inotify_init1(int flags);//如果flags为0,功能与inotify_init()相同
/* 添加 watch */
int inotify_add_watch(int fd, const char *pathname, uint32_t mask); //对于在pathname 中指定位置的文件,添加一个新的 watch,或者修改一个现有的 watch
int inotify_rm_watch(int fd, int wd);//从 inotify 中删除现有 watch 实例
/* 读取文件事件 */
ssize_t read(int fd, void *buf, size_t count);//尝试从inotify 事件队列关联的文件描述符fd读取多达count个字节到从buf开始的缓冲区中。成功时,返回读取的字节数(零表示文件结尾),文件位置按此数字前进。
/* 关闭文件描述符 */
int close(int fd);//关闭一个inotify 事件队列关联的文件描述符,使其不再引用任何文件
read 每次通过文件描述符读取的 inotify 事件队列中一个事件,事件的 mask 标记了文件发生的事件。inotify 事件的数据结构如下:
cpp
struct inotify_event {
int wd; /* 文件的监控描述符 */
uint32_t mask; /* 文件事件的掩码 */
uint32_t cookie; /* 重命名事件相关的唯一整数。对于所有其他事件类型,cookie 设置为 0 */
uint32_t len; /* 文件名称的长度 */
char name[]; /* 被监控的文件名称 */
};
inotify 事件 mask 的宏定义:
cpp
#define IN_ACCESS 0x00000001 /* File was accessed. */
#define IN_MODIFY 0x00000002 /* File was modified. */
#define IN_ATTRIB 0x00000004 /* Metadata changed. */
#define IN_CLOSE_WRITE 0x00000008 /* Writtable file was closed. */
#define IN_CLOSE_NOWRITE 0x00000010 /* Unwrittable file closed. */
#define IN_OPEN 0x00000020 /* File was opened. */
#define IN_MOVED_FROM 0x00000040 /* File was moved from X. */
#define IN_MOVED_TO 0x00000080 /* File was moved to Y. */
#define IN_CREATE 0x00000100 /* Subfile was created. */
#define IN_DELETE 0x00000200 /* Subfile was deleted. */
#define IN_DELETE_SELF 0x00000400 /* Self was deleted. */
#define IN_MOVE_SELF 0x00000800 /* Self was moved. */
inotify 没有实现对目录的递归监控,需要自己添加这部分的功能,因此要判断文件类型,对于常规文件和目录文件分别进行处理。
Linux 下的文件元信息,可以通过 stat() 读取,st_mode 字段记录了文件的类型,取值 S_IFDIR、S_IFREG 分别表示目录文件和常规文件。
cpp
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* Inode number */
mode_t st_mode; /* File type and mode */
nlink_t st_nlink; /* Number of hard links */
/* 此处省略部分数据 */
};
Linux 下 使用 readdir 打开目录获取目录信息,此函数返回一个 dirent 结构体,它的 d_type 字段记录了打开目录下的子文件的类型
cpp
struct dirent {
ino_t d_ino; /* Inode number */
off_t d_off; /* Not an offset; see below */
unsigned short d_reclen; /* Length of this record */
unsigned char d_type; /* Type of file; not supported by all filesystem types */
char d_name[256]; /* Null-terminated filename */
};
d_type 字段取值如下:
cpp
enum
{
DT_UNKNOWN = 0,
# define DT_UNKNOWN DT_UNKNOWN
DT_FIFO = 1,
# define DT_FIFO DT_FIFO
DT_CHR = 2,
# define DT_CHR DT_CHR
DT_DIR = 4,
# define DT_DIR DT_DIR //目录文件
DT_BLK = 6,
# define DT_BLK DT_BLK
DT_REG = 8,
# define DT_REG DT_REG //常规文件
DT_LNK = 10,
# define DT_LNK DT_LNK
DT_SOCK = 12,
# define DT_SOCK DT_SOCK
DT_WHT = 14
# define DT_WHT DT_WHT
};
文件监控 demo:
cpp
#include <errno.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/inotify.h>
#include <unistd.h>
#include <dirent.h>
#include <string.h>
#include <sys/stat.h>
#include<iostream>
using std::string;
string event_str[12] =
{
"IN_ACCESS", //文件被访问
"IN_MODIFY", //文件修改
"IN_ATTRIB", //文件元数据修改
"IN_CLOSE_WRITE",
"IN_CLOSE_NOWRITE",
"IN_OPEN",
"IN_MOVED_FROM", //文件移动from
"IN_MOVED_TO", //文件移动to
"IN_CREATE", //文件创建
"IN_DELETE", //文件删除
"IN_DELETE_SELF",
"IN_MOVE_SELF"
};
class FileMonitor
{
public:
void start_watch(int size, char *file_list[]);
int watch_dir(const char *dir_path);
FileMonitor();
~FileMonitor();
private:
int fd;
};
FileMonitor::FileMonitor()
{
fd = inotify_init1(IN_NONBLOCK);//创建inotify实例,返回与该实例相关的文件描述符 fd
if (fd == -1) {
std::cerr<<"Error: inotifiy initial failed !"<<std::endl;
exit(EXIT_FAILURE);
}
}
FileMonitor::~FileMonitor()
{
if (fd > 0)
close(fd);
}
void FileMonitor::start_watch(int size, char *file_list[])
{
struct stat file_info;
int wd, file_type, event_list_len;
struct inotify_event *event;
char buf[8192] __attribute__ ((aligned(__alignof__(struct inotify_event))));
for (int i=1; i < size; i++)
{
stat(file_list[i], &file_info);
if (file_info.st_mode & S_IFREG)//普通文件直接添加 watch
{
wd = inotify_add_watch(fd, file_list[i], IN_ALL_EVENTS);
if (wd == -1)
{
std::cerr<<"Error: cannot watch "<<file_list[i]<<" !"<<std::endl;
exit(EXIT_FAILURE);
}
}
if (file_info.st_mode & S_IFDIR) //目录文件需要遍历,为目录中的所有文件添加 watch
watch_dir(file_list[i]);
}
//std::cout<<"start listening for events"<<std::endl;
int event_len = sizeof(struct inotify_event);
//读取inotify事件队列中的事件
while (1)
{
if ((event_list_len = read(fd, buf, sizeof(buf))) > 0)
for (char *ptr = buf; ptr < buf+event_list_len; ptr += event_len + event->len){
event = (struct inotify_event *)ptr;
//解析文件事件
for (int i = 1; i < 12; i++){
if ((event->mask >> i) & 1){
std::cout<<event->name<<": "<<event_str[i-1]<<std::endl;
/*
* event->name获取的是相对路径,获取绝对路径需要额外进行路径存储
*
* 这里可以针对敏感文件设置告警
*/
}
}
}
}
}
int FileMonitor::watch_dir(const char *dir_path)
{
DIR *dir;//打开的目录
struct dirent *dir_container;//打开目录内容
int wd, path_len, count = 0;
string path = dir_path, path_str, prnt_path[1000];//当前目录、临时变量、子目录数组
if ((dir = opendir(dir_path)) == NULL)
{
std::cerr<<"Error: cannot open directory '"<<dir_path<<"' !"<<std::endl;
return -1;
}
wd = inotify_add_watch(fd, dir_path, IN_ALL_EVENTS);//为目录添加监控
if (wd == -1)
{
std::cerr<<"Error: cannot watch '"<<dir_path<<"' !"<<std::endl;
return -1;
}
while((dir_container = readdir(dir)) != NULL)
{
path_len = path.length();
path_str = path[path_len-1];
if (path_str.compare( "/") != 0)
path += "/";
path_str = path + (string)dir_container->d_name;//子文件绝对路径
//std::cout<<"path: "<<path_str<<std::endl;
if (dir_container->d_type == DT_REG)
{
inotify_add_watch(fd, (char *)path_str.c_str(), IN_ALL_EVENTS);//常规文件直接添加监控
continue;
}
if (dir_container->d_type == DT_DIR
&& strcmp(".", dir_container->d_name) != 0
&& strcmp(dir_container->d_name,"..") != 0)
{//目录文件加入子目录数组,等待递归遍历
prnt_path[count] = path_str;
count++;
}
}
closedir(dir);
while (count > 0){
count--;
watch_dir(prnt_path[count].c_str());//递归遍历目录,添加监控
}
return 0;
}
int main(int argc, char* argv[])
{
if (argc < 2){
std::cerr<<"Error: no watching file!"<<std::endl;
exit(1);
}
FileMonitor monitor;
monitor.start_watch(argc, argv);
return 0;
}
编译执行:
bash
c++ -o test -std=c++11 test.cpp && ./test /var/log
参考:
inotify(7) - Linux manual page