在Linux系统中,Inotify是内核提供的文件系统事件监控机制,能够实时捕获文件或目录的创建、修改、删除、移动等变更事件,相比轮询式文件监控,它的资源占用更低、响应速度更快。C#开发者在Linux环境下开发文件监控功能时,可以通过调用系统接口使用Inotify实现高效的文件变更感知。

Inotify核心概念
Inotify的核心能力由几个关键部分构成,理解这些概念能帮助我们更好地实现监控逻辑:
- inotify实例:通过系统调用创建的一个监控实例,后续所有监控操作都基于这个实例完成。
- 监控描述符:向inotify实例添加需要监控的文件或目录后,会返回一个唯一的监控描述符,用于标识该监控项。
- 事件类型:Inotify支持多种事件,比如IN_CREATE(文件创建)、IN_MODIFY(文件修改)、IN_DELETE(文件删除)、IN_MOVED_FROM(文件移出)、IN_MOVED_TO(文件移入)等。
C#调用Inotify的实现步骤
1. 初始化Inotify实例
在Linux中,创建inotify实例需要调用inotify_init系统调用,C#可以通过Interop的方式调用该系统调用,首先定义相关的系统调用方法:
using System;
using System.Runtime.InteropServices;
public class InotifyHelper
{
// 系统调用编号,不同Linux架构可能略有差异,x86_64下inotify_init的系统调用号为253
private const int SYS_INOTIFY_INIT = 253;
private const int SYS_INOTIFY_ADD_WATCH = 254;
private const int SYS_INOTIFY_RM_WATCH = 255;
[DllImport("libc", SetLastError = true)]
private static extern int syscall(int number, params object[] args);
// 创建inotify实例,返回实例的文件描述符
public static int InitInotify()
{
int fd = syscall(SYS_INOTIFY_INIT);
if (fd < 0)
{
throw new Exception($"初始化Inotify失败,错误码:{Marshal.GetLastWin32Error()}");
}
return fd;
}
}
2. 添加监控目录或文件
创建好inotify实例后,需要调用inotify_add_watch系统调用添加需要监控的路径,同时指定要监听的事件类型:
public class InotifyHelper
{
// 常用事件掩码定义
public const uint IN_CREATE = 0x00000100;
public const uint IN_MODIFY = 0x00000002;
public const uint IN_DELETE = 0x00000200;
public const uint IN_MOVED_FROM = 0x00000040;
public const uint IN_MOVED_TO = 0x00000080;
public const uint IN_ALL_EVENTS = 0xFFFFFFFF;
// 添加监控,返回监控描述符
public static int AddWatch(int inotifyFd, string path, uint mask)
{
int wd = syscall(SYS_INOTIFY_ADD_WATCH, inotifyFd, path, mask);
if (wd < 0)
{
throw new Exception($"添加监控路径{path}失败,错误码:{Marshal.GetLastWin32Error()}");
}
return wd;
}
}
3. 读取并处理监控事件
Inotify的事件会存储在内核缓冲区中,我们可以通过读取inotify实例的文件描述符获取事件数据,事件的结构体定义如下:
[StructLayout(LayoutKind.Sequential)]
public struct InotifyEvent
{
public int wd; // 监控描述符
public uint mask; // 事件类型掩码
public uint cookie; // 关联事件的cookie,比如移动事件的源和目标会共享同一个cookie
public uint len; // 事件名称的长度
// 事件名称紧跟在结构体后面,这里用Marshal读取的时候需要额外处理
}
读取事件的实现代码如下:
using System.IO;
using System.Text;
public class InotifyHelper
{
// 读取事件,返回事件列表
public static List<InotifyEventInfo> ReadEvents(int inotifyFd)
{
List<InotifyEventInfo> events = new List<InotifyEventInfo>();
byte[] buffer = new byte[4096];
// 读取内核缓冲区中的事件数据
int bytesRead = (int)syscall(0, inotifyFd, buffer, buffer.Length); // 0是read系统调用号
if (bytesRead <= 0)
{
return events;
}
int offset = 0;
while (offset < bytesRead)
{
// 读取事件基础结构体
IntPtr ptr = Marshal.UnsafeAddrOfArrayElement(buffer, offset);
InotifyEvent inotifyEvent = Marshal.PtrToStructure<InotifyEvent>(ptr);
offset += Marshal.SizeOf<InotifyEvent>();
// 读取事件对应的文件名称
string name = string.Empty;
if (inotifyEvent.len > 0)
{
name = Encoding.UTF8.GetString(buffer, offset, (int)inotifyEvent.len).TrimEnd(' ');
offset += (int)inotifyEvent.len;
}
events.Add(new InotifyEventInfo
{
WatchDescriptor = inotifyEvent.wd,
EventMask = inotifyEvent.mask,
Cookie = inotifyEvent.cookie,
Name = name
});
}
return events;
}
}
public class InotifyEventInfo
{
public int WatchDescriptor { get; set; }
public uint EventMask { get; set; }
public uint Cookie { get; set; }
public string Name { get; set; }
}
4. 移除监控与释放资源
当不需要监控某个路径时,可以调用inotify_rm_watch移除监控,程序退出时需要关闭inotify实例的文件描述符释放资源:
public class InotifyHelper
{
// 移除监控
public static void RemoveWatch(int inotifyFd, int watchDescriptor)
{
int result = syscall(SYS_INOTIFY_RM_WATCH, inotifyFd, watchDescriptor);
if (result < 0)
{
throw new Exception($"移除监控失败,错误码:{Marshal.GetLastWin32Error()}");
}
}
// 关闭inotify实例
public static void CloseInotify(int inotifyFd)
{
syscall(3, inotifyFd); // 3是close系统调用号
}
}
完整使用示例
下面是一个完整的监控示例,监控指定目录下的文件创建、修改、删除事件:
using System;
using System.Threading;
class Program
{
static void Main(string[] args)
{
string monitorPath = "/tmp/test_inotify"; // 要监控的目录,不存在可以先创建
if (!Directory.Exists(monitorPath))
{
Directory.CreateDirectory(monitorPath);
}
try
{
// 初始化inotify实例
int inotifyFd = InotifyHelper.InitInotify();
// 添加监控,监听创建、修改、删除事件
int wd = InotifyHelper.AddWatch(inotifyFd, monitorPath,
InotifyHelper.IN_CREATE | InotifyHelper.IN_MODIFY | InotifyHelper.IN_DELETE);
Console.WriteLine($"开始监控目录:{monitorPath}");
// 循环读取事件
while (true)
{
var events = InotifyHelper.ReadEvents(inotifyFd);
foreach (var evt in events)
{
string eventType = evt.EventMask switch
{
var m when (m & InotifyHelper.IN_CREATE) != 0 => "文件创建",
var m when (m & InotifyHelper.IN_MODIFY) != 0 => "文件修改",
var m when (m & InotifyHelper.IN_DELETE) != 0 => "文件删除",
_ => $"其他事件,掩码:{evt.EventMask}"
};
Console.WriteLine($"[{DateTime.Now}] 事件类型:{eventType},文件名称:{evt.Name}");
}
Thread.Sleep(100); // 降低CPU占用
}
}
catch (Exception ex)
{
Console.WriteLine($"监控发生错误:{ex.Message}");
}
}
}
注意事项
- 监控的目录或文件需要有对应的读取权限,否则添加监控会失败。
- Inotify的内核缓冲区大小有限,如果事件产生速度过快,可能会出现事件丢失的情况,需要根据业务场景调整缓冲区大小。
- 如果需要监控子目录下的文件变更,需要递归添加子目录的监控,Inotify本身不会自动监控子目录。
- 程序退出时一定要关闭inotify实例的文件描述符,避免资源泄漏。