6

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

 8 months ago
source link: https://bianchengnan.gitee.io//articles/review-CreateFile-DesireAccess-ShareMode/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

2023-12-23

|

2024-01-06

| 开发

| 热度: 1℃

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

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

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

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

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

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

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 打开同一个文件。关键验证代码如下:

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 显示帮助。

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

:: read-readwrite-write-none.bat
CreateFileBatchCaller.bat %~n0%
:: 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 参数。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK