在C#开发中,文件操作是非常常见的需求,但是很多时候我们会遇到文件被其他进程占用的情况,直接进行读写、删除、移动等操作会抛出IOException,导致程序运行异常。因此学会判断文件是否被占用是很有必要的。

C#判断文件占用的常见方式
方式一:尝试打开文件捕获异常
这是最常用的一种实现方式,核心逻辑是尝试以独占模式打开目标文件,如果打开失败并且异常类型为文件被占用的相关异常,就说明文件正在被其他进程使用。这种方式的实现逻辑比较直观,不需要依赖额外的系统API。
下面是具体的代码实现:
using System;
using System.IO;
using System.Runtime.InteropServices;
public class FileOccupyChecker
{
/// <summary>
/// 判断文件是否被其他进程占用
/// </summary>
/// <param name="filePath">目标文件路径</param>
/// <returns>true表示被占用,false表示未被占用</returns>
public static bool IsFileOccupied(string filePath)
{
// 判断文件是否存在,不存在则直接返回false
if (!File.Exists(filePath))
{
return false;
}
FileStream stream = null;
try
{
// 尝试以独占模式打开文件,读写权限都申请,不共享给其他进程
stream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
return false;
}
catch (IOException ex)
{
// 判断是否是文件被占用的异常,不同系统下错误码不同
var errorCode = Marshal.GetHRForException(ex) & 0xFFFF;
// 32表示文件被其他进程使用,33表示文件被其他进程锁定
return errorCode == 32 || errorCode == 33;
}
finally
{
// 确保文件流被释放
if (stream != null)
{
stream.Close();
stream.Dispose();
}
}
}
}
使用这个方法的时候,只需要传入目标文件的完整路径即可,示例如下:
string testFilePath = @"D:test.txt";
bool isOccupied = FileOccupyChecker.IsFileOccupied(testFilePath);
if (isOccupied)
{
Console.WriteLine("文件正在被其他进程占用");
}
else
{
Console.WriteLine("文件未被占用,可以正常操作");
}
方式二:使用系统API查询文件句柄
除了通过尝试打开文件判断,还可以使用Windows的API函数查询当前系统所有打开的文件句柄,匹配目标文件路径来判断是否被占用。这种方式的准确性更高,但是实现相对复杂,需要调用系统底层接口。
首先定义需要用到的API结构和函数:
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
public class FileHandleChecker
{
[StructLayout(LayoutKind.Sequential)]
public struct RM_UNIQUE_PROCESS
{
public int dwProcessId;
public System.Runtime.InteropServices.ComTypes.FILETIME ProcessStartTime;
}
[DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)]
public static extern int RmRegisterResources(
IntPtr sessionHandle,
uint nFiles,
string[] rgsFilenames,
uint nApplications,
[MarshalAs(UnmanagedType.LPArray)] RM_UNIQUE_PROCESS[] rgApplications,
uint nServices,
string[] rgsServiceNames
);
[DllImport("rstrtmgr.dll")]
public static extern int RmStartSession(out IntPtr sessionHandle, int sessionFlags, string strSessionKey);
[DllImport("rstrtmgr.dll")]
public static extern int RmEndSession(IntPtr sessionHandle);
[DllImport("rstrtmgr.dll")]
public static extern int RmGetList(
IntPtr sessionHandle,
out uint nProcInfo,
uint nProcInfoNeeded,
[MarshalAs(UnmanagedType.LPArray)] RM_PROCESS_INFO[] rgAffectedApps,
out uint lpdwRebootReasons
);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct RM_PROCESS_INFO
{
public RM_UNIQUE_PROCESS Process;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
public string strAppName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
public string strServiceShortName;
public RM_APP_TYPE ApplicationType;
public uint AppStatus;
public uint TSSessionId;
[MarshalAs(UnmanagedType.Bool)]
public bool bRestartable;
}
public enum RM_APP_TYPE
{
RmUnknownApp = 0,
RmMainWindow = 1,
RmOtherWindow = 2,
RmService = 3,
RmExplorer = 4,
RmConsole = 5,
RmCritical = 1000
}
}
然后实现判断逻辑:
public static List<int> GetFileOccupiedProcesses(string filePath)
{
List<int> processIds = new List<int>();
IntPtr sessionHandle = IntPtr.Zero;
try
{
// 启动重启管理器会话
int result = RmStartSession(out sessionHandle, 0, Guid.NewGuid().ToString());
if (result != 0)
{
throw new Exception("启动会话失败,错误码:" + result);
}
// 注册要检查的文件
string[] files = new string[] { filePath };
result = RmRegisterResources(sessionHandle, (uint)files.Length, files, 0, null, 0, null);
if (result != 0)
{
throw new Exception("注册文件资源失败,错误码:" + result);
}
// 获取占用文件的进程列表
uint nProcInfoNeeded = 0;
uint nProcInfo = 0;
uint lpdwRebootReasons = 0;
// 先获取需要的缓冲区大小
result = RmGetList(sessionHandle, out nProcInfo, nProcInfoNeeded, null, out lpdwRebootReasons);
if (result == 0)
{
// 没有进程占用文件
return processIds;
}
else if (result == 234) // ERROR_MORE_DATA
{
nProcInfoNeeded = nProcInfo;
RM_PROCESS_INFO[] processInfos = new RM_PROCESS_INFO[nProcInfoNeeded];
nProcInfo = nProcInfoNeeded;
result = RmGetList(sessionHandle, out nProcInfo, nProcInfoNeeded, processInfos, out lpdwRebootReasons);
if (result == 0)
{
for (int i = 0; i < nProcInfo; i++)
{
processIds.Add(processInfos[i].Process.dwProcessId);
}
}
}
}
finally
{
// 结束会话释放资源
if (sessionHandle != IntPtr.Zero)
{
RmEndSession(sessionHandle);
}
}
return processIds;
}
两种方式的对比
我们可以通过下面的表格对比两种实现方式的优缺点:
| 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 尝试打开文件捕获异常 | 实现简单,不需要调用系统底层API,兼容性好 | 只能判断文件是否被占用,无法获取占用文件的进程信息 | 只需要判断文件是否可操作,不需要知道占用进程的场景 |
| 系统API查询句柄 | 可以获取占用文件的所有进程ID,准确性更高 | 实现复杂,需要调用Windows API,仅支持Windows系统 | 需要知道具体是哪个进程占用了文件,需要结束占用进程的场景 |
注意事项
- 第一种方式判断文件是否存在的时候,如果文件不存在会直接返回false,不会抛出异常,符合实际使用逻辑。
- 使用第一种方式的时候,
FileShare.None参数表示不允许其他进程对文件进行任何读写操作,这样才能准确判断文件是否被独占占用。 - 第二种方式依赖Windows的
rstrtmgr.dll,所以只能在Windows系统下运行,跨平台场景不适用。 - 不管使用哪种方式,判断文件占用都只能作为操作前的预检查,因为判断完成后到实际操作的这段时间内,文件状态可能发生变化,所以实际操作时还是要做好异常处理。
总结
如果是简单的判断文件是否可以操作,优先使用尝试打开文件捕获异常的方式,实现简单且兼容性更好。如果需要获取占用文件的进程信息,再考虑使用系统API的方式。在实际开发中,可以根据具体的需求选择合适的实现方案,同时做好文件操作的异常处理,避免程序因为文件占用问题崩溃。
C#文件占用检测FileStream异常处理进程占用判断修改时间:2026-07-20 11:33:56