前言
前一阵子,我在编写文件变化监控程序的时候遇到了文件被占用的问题。很早之前写过一篇关于 CreateFile 函数的 dwDesiredAccess 和 dwShareMode 参数的笔记。我发现之前的理解不够全面、准确。为了更好的理解这两个参数的作用,我搜索了大量资料,编写了测试程序及测试脚本,参考了 xp 源码,终于搞清楚这两个参数的作用。简而言之,需要遵循以下两个规则:
规则 1:后续的访问权限与先前的共享模式不能冲突。
规则 2:后续的共享模式与先前的访问权限不能冲突。
如果你对下面的几个问题有明确的答案并且清楚的知道原因,那么可以跳过本文了。
- 第一次以读访问权限,写共享模式打开文件,会成功吗?
- 如果第一次打开成功了,第二次以写访问权限,读共享模式打开。会成功吗?
- 如果第二次打开成功了,第三次以读 / 写 / 读写访问权限,读写共享模式打开,会成功吗?
- 第一次以读访问权限,写共享模式打开文件,第二次以写访问权限,读写共享模式打开。第三次以写访问权限,读写共享模式打开,会成功吗?
在总结之前,先看一下关键的权限检查代码。
参考源码
| 1 | NTSTATUS IoCheckShareAccess( | 
说明:
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 | using System; | 
生成的程序名是 CreateFile.exe,该程序可以接收命令行参数,通过 -f 指定文件名,通过 -a 指定访问权限,通过 -s 指定共享模式, 通过 -h 显示帮助。
验证脚本
为了更方便的验证,我又写了批处理脚本,关键脚本如下:
| 1 | :: read-readwrite-write-none.bat | 
| 1 | :: CreateFileBatchCaller.bat | 
脚本 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
至此,文章开头的几个问题的答案应该已经很明显了。一起来看一下。
解惑
- 第一次尝试以读访问权限,写共享模式打开文件,会成功吗? - 答:会成功。 - 第一次打开时总会成功。 
- 如果第一次打开成功了,第二次尝试以写访问权限,读共享模式打开。会成功吗? - 答:会成功。 - 第一次的共享模式是写,第二次的访问权限是写,第二次的访问权限与第一次的共享模式不冲突。 - 第二次的共享模式是读,第一次的访问权限是读,第二次的共享模式与第一次的访问权限不冲突。  
- 如果第二次打开成功了,第三次尝试以读/写/读写访问权限,读写共享模式打开,会成功吗? - 答:不会成功。 - 第三次的访问权限是读的话,与第一次的共享模式(写)冲突。 - 第三次的访问权限是写的话,与第二次的共享模式(读)冲突。 - 第三次的访问权限是读写的话,既与第一次的共享模式(写)冲突,又与第二次的共享模式(读)冲突。  - 这里只贴了第三次的访问权限是写的情况,其它两种情况也会失败。 
- 第一次尝试以读访问权限,写共享模式打开文件,第二次尝试以写访问权限,读写共享模式打开。第三次尝试以写访问权限,读写共享模式打开,会成功吗? - 答:会成功。 - 第三次的访问权限(写),既不与第一次的共享模式(写)冲突,又不与第二次的共享模式(读写)冲突。 - 第三次的共享模式(读写),既不与第一次的访问权限(读)冲突,又不与第二次的访问权限(写)冲突。  
最后,贴一下之前整理的笔记,基本正确,但是不够全面,不够深刻。
CreateFile 参数
一直对 CreateFile 的参数 dwDesiredAccess 和 dwShareMode 的具体作用不是很清楚,今天重读《windows 核心编程》的时候有了一些新感悟。 简要总结如下:
- dwDesiredAccess表示本次- CreateFile想要获取的权限: 只读(- GENERIC_READ),只写(- GENERIC_WRITE),可读写 (- GENERIC_READ | GENERIC_WRITE)。
- dwShareMode表示后续- CreateFile可以取得什么权限。
对 dwDesiredAccess 各种值及含义抄录如下(摘自 《Windows核心编程》第 5 版 第10 章 p279):
| 值 | 含义 | 
|---|---|
| 0 | 我们不希望从设备读取数据或向设备写入数据。如果只想改变设备的配置(比如只是修改文件的时间戳),那么可以传 0 | 
| GENERIC_READ | 允许对设备进行只读访问 | 
| GENERIC_WRITE | 允许对设备进行只写访问。例如,备份软件会用到这个标志,如果想把数据发送到打印机,也可以使用这个标志。注意, GENERIC_WRITE标志并没有隐式地包含GENERIC_READ标志 | 
| GENERIC_READ | GENERIC_WRITE | 允许对设备进行读写操作。由于这个标志允许我们和设备之间自由地交换数据,因此最为常用 | 
对 dwShareMode 的各种值及含义抄录如下(摘自 《Windows核心编程》第 5 版 第10 章 p279):
| 值 | 含义 | 
|---|---|
| 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参数。
