开发常识 | 彻底理清 CreateFile 读写权限与共享模式的关系

前言

前一阵子,我在编写文件变化监控程序的时候遇到了文件被占用的问题。很早之前写过一篇关于 CreateFile 函数的 dwDesiredAccessdwShareMode 参数的笔记。我发现之前的理解不够全面、准确。为了更好的理解这两个参数的作用,我搜索了大量资料,编写了测试程序及测试脚本,参考了 xp 源码,终于搞清楚这两个参数的作用。简而言之,需要遵循以下两个规则:

规则 1:后续的访问权限与先前的共享模式不能冲突。

规则 2:后续的共享模式与先前的访问权限不能冲突。

如果你对下面的几个问题有明确的答案并且清楚的知道原因,那么可以跳过本文了。

  1. 第一次以访问权限,共享模式打开文件,会成功吗?
  2. 如果第一次打开成功了,第二次以访问权限,共享模式打开。会成功吗?
  3. 如果第二次打开成功了,第三次以 / / 读写访问权限,读写共享模式打开,会成功吗?
  4. 第一次以访问权限,共享模式打开文件,第二次以访问权限,读写共享模式打开。第三次以访问权限,读写共享模式打开,会成功吗?

在总结之前,先看一下关键的权限检查代码。

参考源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
NTSTATUS IoCheckShareAccess(
IN ACCESS_MASK DesiredAccess,
IN ULONG DesiredShareAccess,
IN OUT PFILE_OBJECT FileObject,
IN OUT PSHARE_ACCESS ShareAccess,
IN BOOLEAN Update
)
{
PAGED_CODE();

// 获取本次调用时,指定的 读/写/删除 访问权限标志
FileObject->ReadAccess = (BOOLEAN) ((DesiredAccess & (FILE_EXECUTE | FILE_READ_DATA)) != 0);
FileObject->WriteAccess = (BOOLEAN) ((DesiredAccess & (FILE_WRITE_DATA | FILE_APPEND_DATA)) != 0);
FileObject->DeleteAccess = (BOOLEAN) ((DesiredAccess & DELETE) != 0);

if (FileObject->ReadAccess || FileObject->WriteAccess || FileObject->DeleteAccess)
{
// 获取本次调用时,指定的 读/写/删除 共享模式标志
FileObject->SharedRead = (BOOLEAN) ((DesiredShareAccess & FILE_SHARE_READ) != 0);
FileObject->SharedWrite = (BOOLEAN) ((DesiredShareAccess & FILE_SHARE_WRITE) != 0);
FileObject->SharedDelete = (BOOLEAN) ((DesiredShareAccess & FILE_SHARE_DELETE) != 0);

if (FileObject->Flags & FO_FILE_OBJECT_HAS_EXTENSION)
{
PIOP_FILE_OBJECT_EXTENSION fileObjectExtension =(PIOP_FILE_OBJECT_EXTENSION)(FileObject + 1);
if (fileObjectExtension->FileObjectExtensionFlags & FO_EXTENSION_IGNORE_SHARE_ACCESS_CHECK)
return STATUS_SUCCESS;
}

ULONG ocount = ShareAccess->OpenCount;

if ( // 本次调用时 DesiredAccess 包含了读/写/删除标志,并且
// 在之前的调用中,DesiredShareAccess 缺少对应的读/写/删除标志(ShareXXX < ocount)
(FileObject->ReadAccess && (ShareAccess->SharedRead < ocount))
|| (FileObject->WriteAccess && (ShareAccess->SharedWrite < ocount))
|| (FileObject->DeleteAccess && (ShareAccess->SharedDelete < ocount))
// 之前的调用中 DesiredAccess 包含了读/写/删除标志,并且
// 本次调用时 DesiredShareAccess 缺少对应读/写/删除标志
|| ((ShareAccess->Readers != 0) && !FileObject->SharedRead)
|| ((ShareAccess->Writers != 0) && !FileObject->SharedWrite)
|| ((ShareAccess->Deleters != 0) && !FileObject->SharedDelete)
)
{
return STATUS_SHARING_VIOLATION;
}
else if (Update)
{
ShareAccess->OpenCount++; // 每次权限检查通过后,打开计数 +1

// 本次调用时 DesiredAccess 包含了读/写/删除标志,对应的计数 +1
ShareAccess->Readers += FileObject->ReadAccess;
ShareAccess->Writers += FileObject->WriteAccess;
ShareAccess->Deleters += FileObject->DeleteAccess;

// 本次调用时 DesiredShareAccess 包含了读/写/删除标志,对应的计数 +1
ShareAccess->SharedRead += FileObject->SharedRead;
ShareAccess->SharedWrite += FileObject->SharedWrite;
ShareAccess->SharedDelete += FileObject->SharedDelete;
}
}
return STATUS_SUCCESS;
}

说明: DesiredAccess 表示 访问权限,DesiredShareAccess 表示 共享模式。

代码中的注释已经写的很清楚了,再整体梳理一下:

更新逻辑(else if 分支):

每次权限检查成功后,如果指定了 Update 参数,SharedAccess->OpenCount 计数会加一。

DesiredAccess 包含 / / 删除标志的时候,SharedAccess->Readers / Writers / Deleters 计数会加一。

DesiredShareAccess 包含 / / 删除标志的时候,ShareAccess->SharedRead / SharedWrite / SharedDelete 计数会加一。

检查逻辑(if 分支):

  • 如果本次调用时 DesiredAccess 包含了 / / 删除标志(FileObject->ReadAccess / WriteAccess / DeleteAccess 为真)并且在之前的调用中 DesiredShareAccess 缺少对应的 / / 删除标志(ShareAccess->SharedRead / SharedWrite / SharedDelete < ocount),违反规则 1,权限检查会失败。

  • 如果在之前的调用中 DesiredAccess 包含了 / / 删除标志(ShareAccess->Readers / Writers / Deleters != 0),并且本次调用时 DesiredShareAccess 缺少对应 / / 删除标志(FileObject->SharedRead / SharedWrite / SharedDelete 为假),违反规则 2,权限检查会失败。

我把各种情况下的打开结果整理成了表格,供大家参考。

结果表

访问权限 1 共享模式 1 访问权限 2 共享模式 2 结果 说明
N R / W / RW 失败 违反了 规则1
R W / RW 失败 违反了 规则1
W R / RW 失败 违反了 规则1
R / W / RW N 失败 违反了 规则2
W / RW R 失败 违反了 规则2
R / RW W 失败 违反了 规则2
R R R R / RW 成功 第二次的访问权限与第一次的共享模式不冲突。
第二次的共享模式与第一次的访问权限不冲突。
R W W R / RW 成功 同上
R RW R / W / RW R / RW 成功 同上
W W W W / RW 成功 同上
W R R W / RW 成功 同上
W RW R / W / RW W / RW 成功 同上
RW R R RW 成功 同上
RW W W RW 成功 同上
RW RW R / W / RW RW 成功 同上

各项的意义解释如下:

  • 访问权限 代表 dwDesiredAccess 参数,共享模式 代表 dwShareAccess 参数。1 表示第一次调用,2 表示第二次调用。

  • R Read,表示W Write,表示RW ReadWrite,表示读写N None, 表示独占

  • / 表示或者。为了减少组合数量。比如第一行中的 访问权限 2 可以是 / / 读写中的任意一种。

  • --- 表示对应位置是什么都可以,不影响结果。比如,第一行的 访问权限 1 可以是 / / 读写中的任意一种,不论是哪种都会打开失败。

  • 结果列只统计了第二次的结果,因为第一次总是成功的。

以上结论我在 win10 系统上亲自验证过,整体验证思路是用不同的参数调用 CreateFile 打开同一个文件。关键验证代码如下:

验证代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CreateFile
{
class Program
{
// ref https://learn.microsoft.com/en-us/dotnet/standard/commandline/define-commands
static Command SetupCommandHandler()
{
var filePathOption = new Option<string>(name: "--path", getDefaultValue: () => "test.txt", description: "file path");
filePathOption.AddAlias("-f");
filePathOption.AddAlias("-p");

var fileModeOption = new Option<string>(name: "--mode", getDefaultValue: () => "Open", description: "file mode")
.FromAmong("CreateNew", "Create", "Open", "OpenOrCreate", "Truncate", "Append");
fileModeOption.AddAlias("-m");

var fileShareOption = new Option<string>("--share", "file share") { IsRequired = true }
.FromAmong("None", "Read", "Write", "ReadWrite", "Delete", "Inheritable");
fileShareOption.AddAlias("-s");

var fileAccessOption = new Option<string>("--access", "file access") { IsRequired = true }
.FromAmong("Read", "Write", "ReadWrite");
fileAccessOption.AddAlias("-a");

var autoQuitOption = new Option<bool>(name: "--autoquit", getDefaultValue: () => false, description: "auto quit");
autoQuitOption.AddAlias("-q");

var command = new RootCommand();
command.Add(filePathOption);
command.Add(fileModeOption);
command.Add(fileShareOption);
command.Add(fileAccessOption);
command.Add(autoQuitOption);

command.SetHandler((filePath, fileMode, fileShare, fileAccess, autoQuit) =>
{
OpenFileAndWait(filePath, fileMode, fileShare, fileAccess, autoQuit);
}, filePathOption, fileModeOption, fileShareOption, fileAccessOption, autoQuitOption);

return command;
}

static void Main(string[] args)
{
var command = SetupCommandHandler();
command.Invoke(args);
}

static void OpenFileAndWait(string strFilePath, string strFileMode, string strFileShare, string strFileAccess, bool autoQuit)
{
FileStream stream = null;
try
{
var fileMode = (FileMode)System.Enum.Parse(typeof(FileMode), strFileMode);
var fileShare = (FileShare)System.Enum.Parse(typeof(FileShare), strFileShare);
var fileAccess = (FileAccess)System.Enum.Parse(typeof(FileAccess), strFileAccess);

System.Console.WriteLine(string.Format("[{0}] file:{1}, mode: {2}, share: {3}, access: {4}!"
, System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), strFilePath, strFileMode, strFileShare, strFileAccess));

stream = File.Open(strFilePath, fileMode, fileAccess, fileShare);
}
catch (Exception ex)
{
System.Console.WriteLine(string.Format("opening file [{0}] failed with {1}!", strFilePath, ex));
}

if (!autoQuit)
{
System.Console.WriteLine("press any key to continue...");
System.Console.ReadKey();
}

if (stream != null)
{
stream.Dispose();
}
}
}
}

生成的程序名是 CreateFile.exe,该程序可以接收命令行参数,通过 -f 指定文件名,通过 -a 指定访问权限,通过 -s 指定共享模式, 通过 -h 显示帮助。

验证脚本

为了更方便的验证,我又写了批处理脚本,关键脚本如下:

1
2
:: read-readwrite-write-none.bat
CreateFileBatchCaller.bat %~n0%
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
:: CreateFileBatchCaller.bat
@echo off
cd /d %~dp0

setlocal enabledelayedexpansion

set AccessParams=%1%
set AccessParams=%AccessParams:read=Read%
set AccessParams=%AccessParams:write=Write%
set AccessParams=%AccessParams:none=None%

For /f "tokens=1-4 delims=_/. " %%i In ("%AccessParams%") do (
set Access1=%%i
set SharedAccess1=%%j
set Access2=%%k
set SharedAccess2=%%l
)

start CreateFile -f test.txt -a %Access1% -s %SharedAccess1%

:: will this success?
timeout /T 1
start CreateFile -f test.txt -a %Access2% -s %SharedAccess2%

脚本 CreateFileBatchCaller.bat 接收一个参数,内部会根据 - 分割参数,前四项有固定意义,分别表示第一次调用 CreateFile.exe 的访问权限和共享模式、第二次调用 CreateFile.exe 的访问权限和共享模式。

read-readwrite-write-none-failed.bat 是众多调用脚本中的一个,内部会把当前脚本的文件名(不包括扩展名)当作参数调用 CreateFileBatchCaller.bat 。 该脚本可以验证第一次以访问权限、读写共享模式打开文件,第二次以访问权限、独占共享模式打开文件的情况。

亲自动手

所有脚本及源码我已经上传到我的个人仓库了。如果你也想亲自动手验证一下,可以从如下位置获取测试代码,编译好的程序及测试脚本。

github:

https://github.com/BianChengNan/MyBlogStuff/tree/master/review-CreateFile-DesireAccess-ShareMode

gitee:

https://gitee.com/bianchengnan/my-blog-stuff/tree/master/review-CreateFile-DesireAccess-ShareMode

百度云盘:

https://pan.baidu.com/s/10BMMhPGiiBYjlMFrbQH-3g?pwd=tibm

至此,文章开头的几个问题的答案应该已经很明显了。一起来看一下。

解惑

  1. 第一次尝试以访问权限,共享模式打开文件,会成功吗?

    答:会成功。

    第一次打开时总会成功。

  1. 如果第一次打开成功了,第二次尝试以访问权限,共享模式打开。会成功吗?

    答:会成功。

    第一次的共享模式是,第二次的访问权限是,第二次的访问权限与第一次的共享模式不冲突。

    第二次的共享模式是,第一次的访问权限是,第二次的共享模式与第一次的访问权限不冲突。

    open-result-2

  1. 如果第二次打开成功了,第三次尝试以//读写访问权限,读写共享模式打开,会成功吗?

    答:不会成功。

    第三次的访问权限是的话,与第一次的共享模式()冲突。

    第三次的访问权限是的话,与第二次的共享模式()冲突。

    第三次的访问权限是读写的话,既与第一次的共享模式()冲突,又与第二次的共享模式()冲突。
    open-result-3

    这里只贴了第三次的访问权限是的情况,其它两种情况也会失败。

  2. 第一次尝试以访问权限,共享模式打开文件,第二次尝试以访问权限,读写共享模式打开。第三次尝试以访问权限,读写共享模式打开,会成功吗?

    答:会成功。

    第三次的访问权限(),既不与第一次的共享模式()冲突,又不与第二次的共享模式(读写)冲突。

    第三次的共享模式(读写),既不与第一次的访问权限()冲突,又不与第二次的访问权限()冲突。
    open-result-4

最后,贴一下之前整理的笔记,基本正确,但是不够全面,不够深刻。

CreateFile 参数

一直对 CreateFile 的参数 dwDesiredAccessdwShareMode 的具体作用不是很清楚,今天重读《windows 核心编程》的时候有了一些新感悟。 简要总结如下:

  • dwDesiredAccess 表示本次 CreateFile 想要获取的权限: 只读(GENERIC_READ),只写(GENERIC_WRITE),可读写 (GENERIC_READ | GENERIC_WRITE)。
  • dwShareMode 表示后续 CreateFile 可以取得什么权限。

dwDesiredAccess 各种值及含义抄录如下(摘自 《Windows核心编程》第 5 版 第10p279):

含义
0 我们不希望从设备读取数据或向设备写入数据。如果只想改变设备的配置(比如只是修改文件的时间戳),那么可以传 0
GENERIC_READ 允许对设备进行只读访问
GENERIC_WRITE 允许对设备进行只写访问。例如,备份软件会用到这个标志,如果想把数据发送到打印机,也可以使用这个标志。注意,GENERIC_WRITE 标志并没有隐式地包含 GENERIC_READ 标志
GENERIC_READ | GENERIC_WRITE 允许对设备进行读写操作。由于这个标志允许我们和设备之间自由地交换数据,因此最为常用

dwShareMode 的各种值及含义抄录如下(摘自 《Windows核心编程》第 5 版 第10p279):

含义
0 要求独占对设备的访问。 如果设备己经打开,CreateFile 调用会失败。如果我们成功地打开了设备,那么后续的 CreateFile 调用会失败
FILE_SHARE_READ 如果有其他内核对象要使用该设备,我们要求它们不得修改设备的数据。 如果设备已经以写入方式或独占方式打开,那么我们的 CreateFile 会失败。 如果我们成功地打开了设备,那么后续的使用了 GENERIC_WRITE 访问标志的 CreateFile 调用会失败
FILE_SHARE_WRITE 如果有其他内核对象要使用该设备,我们要求它们不得读取设备的数据。 如果设备已经以读取方式或独占方式打开,那么我们的 CreateFile 调用会失败。 如果我们成功地打开了设备,那么后续的使用了GENERIC_READ 访问标志的 CreateFile 调用会失畋
FILE_SHARE_READ | FILE_SHARE_WRITE 如果有其他内核对象要使用该设备,我们不关心它们会从设备读取数据还是会向设备写入数据。如果设备已经以独占方式打开,那么我们的 CreateFile 调用会失败。如果我们成功地打开了设备,那么后续的要求独占读取访问、独占写入访问或独占读写访问的 CreateFile 调用会失败
FILE_SHARE_DELETE 当对文件进行操作的时候,我们不关心文件是否被逻辑删除或是被移动。在 Windows 内部,系统会先将文件标记为待删除,然后当该文件所有已打开的句柄都被关闭的时候,再将其真正的删除

友情提示: 上表中的 如果设备已经以 xxx 方式打开 指的是先前调用的 dwShareMode 参数。

参考资料

BianChengNan wechat
扫描左侧二维码关注公众号,扫描右侧二维码加我个人微信:)
0%