inotify事件驱动机制
前言
刚开始接触这个inotify机制时,是我在写我的云备份项目时,其中有一个文件压缩备份模块,当时需要对一个目录进行监控,压缩的逻辑是:如果目录中的文件它的最后一次修改时间超过我自定义的阈值时,也就是所说的冷文件,就对其进行压缩。
第一次实现时,我采用简单暴力的轮询方式:定期遍历备份目录文件,调用stat
获取每个文件的访问时间,如果超过自定义阈值,就压缩并移动到指定目录。这种做法冗余地遍历了目录与所有文件,效率很低。
后来,我优化为基于inotify的事件驱动方案:用inotify监控目录变化,只有在实际有文件发生访问、修改、新建或删除时才更新内存中的哈希表(映射文件名到访问时间)。后台压缩线程只需要遍历哈希表做冷文件检测和压缩。
对比来看,原始版需要频繁轮询目录和每个文件;inotify版则只在真正有变动时才感知和更新缓存,大幅减少了无用遍历和系统调用。
- V1
- 遍历目录,获取文件列表
- 依次
stat
获取访问时间并比较 - 超时则压缩
- V2
- inotify事件触发时更新哈希表
- 压缩线程定时遍历哈希表处理冷文件
通过 inotify 实现事件驱动监控,不仅提升效率,也让代码更优雅。掌握 inotify,对于今后涉及文件、目录监控的项目十分有价值!
inotify简介
inotify 是 Linux 内核从 2.6.13 版本后引入的一个文件系统事件监控机制。它允许用户空间的程序监控文件或目录的各种变化(如内容修改、新建、删除等),实现事件驱动的高效监控。
inotify 运行于内核空间,内核为每个监控点注册感兴趣的事件(如文件被修改、读、删等)。当这些事件发生时,内核会生成一个事件结构体,并放进 inotify 文件描述符关联的事件队列中。
用户程序通过 inotify 的系统调用接口(如 inotify_init
、inotify_add_watch
)来创建监控实例和添加监控对象(文件/目录)。程序通常通过 read
、select
、poll
或 epoll
等同步/异步 I/O 方式监听 inotify 文件描述符。只有当被监视对象上发生感兴趣的事件时,read
调用才会返回,传递事件信息。
程序收到事件后,根据事件类型和发生对象等信息,完成后续处理(如重载配置、触发备份、自动同步等功能)。
inotify接口
inotify_init / inotify_init1
创建一个 inotify 实例,返回一个新的文件描述符,用于后续事件监听。
原型:
int inotify_init(void);
int inotify_init1(int flags);
参数:
inotify_init
无参数。inotify_init1
有flags
参数,一般可为 0 或使用以下选项:IN_NONBLOCK
:返回的 fd 是非阻塞的,read 若无事件会立刻返回 -1 并设置 errno 为 EAGAIN。IN_CLOEXEC
:fork 后自动关闭 fd。
返回值
- 成功,返回新的文件描述符。
- 失败,返回 -1,设置 errno。
示例:
int inotify_fd = inotify_init();
if (inotify_fd < 0) {perror("inotify_init");exit(1);
}
inotify_add_watch
为一个 inotify 实例添加一个监视点(watch),用于监听指定文件或目录的事件。
原型:
int inotify_add_watch(int fd, const char *pathname, uint32_t mask);
参数:
fd
:由 inotify_init 返回的文件描述符。pathname
:你要监听的文件或目录的路径。- mask:需要监听的事件类型,是一组位掩码。常见的 mask 常量有(可组合使用):
IN_ACCESS
:文件被访问(读取)IN_MODIFY
:文件内容被修改IN_ATTRIB
:文件属性被修改IN_CLOSE_WRITE
:可写文件被关闭IN_CLOSE_NOWRITE
:只读文件被关闭IN_OPEN
:文件被打开IN_MOVED_FROM
:被移出目录IN_MOVED_TO
:被移入目录IN_CREATE
:创建新文件或目录IN_DELETE
:文件或目录被删除IN_DELETE_SELF
:被监控对象自身被删除IN_ALL_EVENTS
:所有事件
IN_MOVED_TO 和 IN_CREATE 以及IN_MOVED_FROM 和 IN_DELETE 的区别?
- IN_CREATE
当目录中新建了一个文件或子目录时,触发(比如 touch a.txt)。- IN_MOVED_TO
当一个文件或子目录被移动到监控的目录时,触发(比如 mv /tmp/a.txt ./ 或者在同一目录下重命名文件也是一种“移动到”)。区别:
IN_CREATE 是“新建”,指创建文件本身;
IN_MOVED_TO 是“移入”,指把现有的文件从其他地方搬进来,文件本身其实早就已存在。
- IN_DELETE
当目录中的文件或子目录被删除(比如 rm a.txt),触发。- IN_MOVED_FROM
当目录中的文件或子目录被移出到别处时,触发(比如 mv ./a.txt /tmp/)。区别:
IN_DELETE 是“真删除”,文件内容没了;
IN_MOVED_FROM 是“移出”,文件内容还在,只不过从这个目录转移到别处了(并没有被物理删除)。示例:
假设当前目录是
/test
,监控它:
- touch b.txt 触发 IN_CREATE
- mv /tmp/c.txt ./ 触发 IN_MOVED_TO
- rm b.txt 触发 IN_DELETE
- mv c.txt /tmp/ 触发 IN_MOVED_FROM
总结
- “CREATE/DELETE” 表示在该目录中直接创建/删除文件
- “MOVED_TO/MOVED_FROM” 表示有文件搬进/搬出该目录
返回值:
- 成功,返回一个 watch 描述符(小整数)。
- 失败,返回 -1。
示例:
int wd = inotify_add_watch(inotify_fd, "/tmp", IN_CREATE | IN_DELETE | IN_MODIFY);
if (wd < 0) {perror("inotify_add_watch");exit(1);
}
inotify_rm_watch
移除之前注册的监视点,停止监听指定文件或目录。
原型:
int inotify_rm_watch(int fd, int wd);
参数:
fd
:inotify_init 返回的 fd。wd
:inotify_add_watch 返回的监视点描述符。
返回值:
- 成功,返回 0。
- 失败,返回 -1。
示例:
inotify_rm_watch(inotify_fd, wd);
读取inotify事件 — read
调用 read 从 inotify fd 读取事件,每次读取获得的内容是一个或多个 struct inotify_event
结构。(在阻塞模式下)
struct inotify_event 结构体
struct inotify_event {int wd; // watch 描述符uint32_t mask; // 事件掩码uint32_t cookie; // 联动事件编号(如rename)uint32_t len; // name 数组长度char name[]; // 文件名(可选,len>0时有效)
};
- int wd
监控项(watch descriptor)的描述符。每次你通过 inotify_add_watch 添加一个监控时,系统分配一个唯一的 wd,用来标识这个监控对象。通过它可以知道是哪一个监控点报告了当前事件。 - uint32_t mask
事件掩码。用二进制位表示发生了哪些事件,比如 IN_ACCESS(被访问)、IN_MODIFY(被修改)、IN_CREATE(创建新文件)、IN_DELETE(删除文件)等。可以通过与操作判断发生了什么类型的事件。 - uint32_t cookie
联动事件编号,主要用于标识那些需要成对出现的事件,比如重命名(rename)文件时,move-from 和 move-to 事件的 cookie 是相同的,可以通过这个字段将相关的两个事件关联起来。 - uint32_t len
name 字段的长度。如果对应的事件与某个具体的文件相关,则 name 表示文件名,这时 len 为非零。如果为零则 name 字段不存在。 - char name
可选字段——文件名,只在 len > 0 时有效。该字段不一定以 ‘\0’ 结尾,长度由 len 指定。用于指出触发事件的具体文件或子目录名。
简单的说就是:inotify_event 结构体告诉你“哪个监控点(wd)”、“发生了什么事件(mask)”、“有无联动事件(cookie)”、“事件关联的文件名(name)”。
示例:
char buffer[4096];
ssize_t length = read(inotify_fd, buffer, sizeof(buffer));
if (length < 0) {perror("read");// 处理错误
}
// 解析 buffer 为一组 struct inotify_event
for (char *ptr = buffer; ptr < buffer + length;) {struct inotify_event *event = (struct inotify_event *) ptr;// 检查 event->mask, event->name 等ptr += sizeof(struct inotify_event) + event->len;
}
注意:
- 每次 read 可能返回多个事件,要用循环解析。
- 名称字段仅在返回被监视目录中文件的事件时存在;它标识了被监视目录中的文件名。该文件名以空字符终止,并且可能包含额外的空字符 (‘\0’) 以便后续读取对齐到合适的地址边界。
- len 字段计算 name 中的所有字节,包括空字符;因此,inotify_event 结构的大小为 sizeof(struct inotify_event) + size。
- 当传递给 read 的缓冲区太小而无法返回下一个事件的信息时,行为取决于内核版本:在 Linux 2.6.21 之前,read返回 0;自 Linux 2.6.21 起,read 以错误 EINVAL 失败。
小结
inotify 一般流程如下:
- 调用
inotify_init
/inotify_init1
创建 fd; - 调用
inotify_add_watch
添加你关心的目录或文件和事件; - 不断用
read
从 fd 读取事件,处理文件变更通知; - 不再需要时用
inotify_rm_watch
移除监听点,用close
关闭 fd。
文件冷热分离
bool RunModule()
{running = true;std::thread listener([this](){EventListener();});listener.detach();while(running){ProcessHotFiles();std::this_thread::sleep_for(std::chrono::seconds(_hot_time));}return true;
}void EventListener()
{constexpr size_t BUF_LEN = 4096;char buffer[BUF_LEN];while (running) {ssize_t len = read(inotify_fd, buffer, BUF_LEN);if (len < 0) {perror("read");break;}// 处理事件std::lock_guard<std::mutex> lock(cache_mutex);for (char *ptr = buffer; ptr < buffer + len;) {struct inotify_event *event = reinterpret_cast<struct inotify_event*>(ptr);if (event->len > 0) {std::string filepath = _back_dir + event->name;if (event->mask & (IN_ACCESS | IN_MODIFY | IN_CREATE)) // 文件新建、被访问或修改,更新时间{last_access_map[filepath] = time(nullptr);} else if (event->mask & IN_DELETE) // 文件删除,移除记录{last_access_map.erase(filepath);}}ptr += sizeof(struct inotify_event) + event->len;}}
}void ProcessHotFiles()
{// 创建本地副本,避免长时间持有锁std::unordered_map<std::string, time_t> local_copy;{std::lock_guard<std::mutex> lock(cache_mutex);local_copy = last_access_map;}// 检查并压缩冷文件time_t now = time(nullptr);for (const auto &[filepath, last_access] : local_copy) {if (now - last_access > _hot_time) {CompressFile(filepath);{std::lock_guard<std::mutex> lock(cache_mutex);last_access_map.erase(filepath);}}}
}
代码原理
代码中创建了一个 inotify 监听线程(listener),一直 read inotify_fd。只要某个文件发生了访问、修改、新建或删除(被配置为监控的事件),就会在 EventListener 中抓到事件。监听到访问/修改/新建事件,代码就通过 last_access_map[filepath] = time(nullptr);
记录该文件“最近一次活跃”的时间戳(以文件路径为 key)。监听到删除事件,直接删掉它的活跃时间记录(因为文件都没了)。
主线程每隔 _hot_time 秒(假设为 60s)调用 ProcessHotFiles(即热文件巡检)。
ProcessHotFiles 拿到本地副本 last_access_map 的数据,遍历每个文件:
- 如果离现在的时间差大于 _hot_time
- 说明文件很久没动过:是“冷文件”
- 此时执行 CompressFile,把文件压缩、归档、删除,并更新持久化数据。
“冷热分离”原理
活跃(热)文件:刚被访问/修改/新建过,时间戳是最近,不会被归档。
冷文件:长时间没动,逐渐积攒后,被系统发现并压缩、归档、腾空间。
每当热文件“被访问/修改/新建”,就又刷新了“上次活跃时间”,从而一直保持活跃状态,不会被当作冷文件处理。
小结
- 热冷界限由 _hot_time 控制(比如 10 分钟没动就被当冷文件处理)。
- inotify 实现高效活跃性监控,只在有事件时才更新时间,不需频繁扫描全盘。
- 冷热算法简单高效,很适合归档、备份、自动清理等用途。
这个方案用 inotify 实时感知文件活跃状态,并定期检查,把长时间未活跃的文件自动归档压缩,实现了纯事件驱动的“文件冷热分离”机制。
活跃(热)文件:刚被访问/修改/新建过,时间戳是最近,不会被归档。
冷文件:长时间没动,逐渐积攒后,被系统发现并压缩、归档、腾空间。
每当热文件“被访问/修改/新建”,就又刷新了“上次活跃时间”,从而一直保持活跃状态,不会被当作冷文件处理。
小结
- 热冷界限由 _hot_time 控制(比如 10 分钟没动就被当冷文件处理)。
- inotify 实现高效活跃性监控,只在有事件时才更新时间,不需频繁扫描全盘。
- 冷热算法简单高效,很适合归档、备份、自动清理等用途。
这个方案用 inotify 实时感知文件活跃状态,并定期检查,把长时间未活跃的文件自动归档压缩,实现了纯事件驱动的“文件冷热分离”机制。