0%

Windows文件系统概述(二)指向点

上一篇文件我们主要介绍了Windows文件系统的基本特性及文档阅读方式,本文将介绍Windows文件系统中一种特殊的概念:指向点(Reparse Point)

回顾上一章节,我们在文件dwFileAttributes依据符号位FILE_ATTRIBUTE_REPARSE_POINT实现了一个方法:

bool StatResult::IsReparsePoint() const {
return (m_handleFileInformation.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0;
}

我们还没有说明这个方法的意义,该方法说明该文件是一个指向点(Reparse Point)。Reparse Point是一种特殊机制,用于实现Windows文件系统的重定向。Windows的挂载和软连接就是基于它实现的。在详解FILE_ATTRIBUTE_REPARSE_POINT之前,我们先从Windows软链接开始看起。

创建软连接

cmd.exe提供了mklink命令用于在Windows创建软链接:

C:\Users\XUranus>mklink
MKLINK [[/D] | [/H] | [/J]] Link Target
/D 创建目录符号链接。默认为文件符号链接。
/H 创建硬链接而非符号链接。
/J 创建目录联接。
Link 指定新的符号链接名称。
Target 指定新链接引用的路径 (相对或绝对)。

mklink创建的软链接有两种:

  • 符号链接(Symbolic)
  • 目录链接(Junction)

mklink不指定/D或者/J创建出来的默认是文件符号链接,例如:

C:\>mklink C:\symfile C:\file.txt
C:\symfile <<===>> C:\file.txt 创建的符号链接

此处C:\file.txt是一个文本文件,C:\symfile是指向它的文件符号链接。通过打开C:\symfile可以打开C:\file.txt

mklink指定/D可以创建目录的符号链接,例如:

C:\>mkdir dir1
C:\>mklink /D C:\symdir C:\dir1
C:\symdir <<===>> C:\dir1 创建的符号链接

此处C:\dir1是一个目录,创建一个指向它的C:\symdir。在资源浏览器中它表现为一个目录,打开C:\symdir目录可以重定向到C:\dir

mklink指定/J可以创建指向Target的目录链接(Junction)。例如:

C:\>mkdir dir2
C:\>mklink /J C:\junctiondir C:\dir2
为 C:\junctiondir <<===>> C:\dir2 创建的符号链接

此处C:\dir2依旧是一个目录,创建一个指向它的C:\junctiondir,在资源管理器中它依旧表现为一个目录,并且可以打开重定向到C:\dir2

以上就是mklink正常创建符号链接的使用方法了,我们用表格对其使用各个参数创建出的链接类型做个总结:

选项 创建链接表现类型
不填 文件
/D 目录
/J 目录

Symbolic和Junction在链接目录的时候表现相似,但他们并不是一类事物,区别主要反映在远端服务器的场景:Junction在服务端处理,而Symbolic在客户端处理。所以Junction不能指向远端文件,而Symbolic可以。同时创建Symbolic需要更高的权限,而创建Junction只需要文件系统的读写权限。详见:“directory junction” vs “directory symbolic link”?

失效软连接

需要注意的是,以上我们都只测试了目标路径存在,且目标类型和链接类型一致的正常场景。那么,如果参数声明和目标文件不一致会怎么样呢?如果目标文件根本不存在又会怎么样呢?

如果指向目标是一个目录,并且不指定/D/J,依然可以创建出一个指向目录的文件符号链接

C:\>mklink C:\sym1 C:\dir1
C:\sym1 <<===>> C:\dir1 创建的符号链接

此处C:\dir1是一个目录,创建一个指向它的C:\sym1。在资源浏览器中它依然表现为一个文件,然而打开C:\sym1并不能成功:

同理mklink可以创建一个指向文件的目录软连接或者一个指向文件的目录链接

C:\>mklink /D C:\sym2 C:\file.txt
为 C:\sym2 <<===>> C:\file.txt 创建的符号链接

C:\>mklink /D C:\junction2 C:\file.txt
为 C:\junction2 <<===>> C:\file.txt 创建的符号链接

C:\sym2C:\junction2在资源管理器中表现都是一个目录,但是由于它指向一个文件,他也不能被重定向打开目标路径。

更进一步,mklink可以任意指定Target来创建的符号链接,Target路径根本不存在,依然可以创建成功:

C:\>mklink C:\invalidsym1 C:\path_not_exists
C:\invalidsym1 <<===>> C:\path_not_exists 创建的符号链接

C:\>mklink C:\invalidsym2 /D C:\path_not_exists
C:\invalidsym2 <<===>> C:\path_not_exists 创建的符号链接

C:\>mklink C:\invalidjunction3 /D C:\path_not_exists
C:\invalidjunction3 <<===>> C:\path_not_exists 创建的符号链接

我们把这类不能正常起到重定向作用的软连接成为失效的软连接mklink创建软链接时候检查指向的目标路径是否存在,不会管目标路径是否是文件或目录。创建出的符号链接类型表现为目录还是文件只和创建时传入的参数有关。

级联软连接

由于软连接可以指任何路径,于是自然可以联想到软连接可以指向软连接:

C:\>mklink C:\cascadesym1 C:\file.txt
C:\cascadesym1 <<===>> C:\file.txt 创建的符号链接

C:\>mklink C:\cascadesym2 C:\cascadesym1
C:\cascadesym2 <<===>> C:\cascadesym1 创建的符号链接

C:\>mklink C:\cascadesym3 C:\cascadesym2
C:\cascadesym3 <<===>> C:\cascadesym2 创建的符号链接

C:\cascadesym3指向了C:\cascadesym2C:\cascadesym2指向了C:\cascadesym1C:\cascadesym1指向了C:\file.txt。这三个符号链接都可以用来打开C:\file.txt。说明Windows符号链接可以级联重定向

综上,Windows的软链接也可以理解为和Linux的Symlink一样,是一种存储了目标位置路径的特殊文件,通过重定向来访问指向的目标文件。

删除软链接

由于软连接只是一种存储目标文件的路径,于是删除和修改软连接不会影响到指向的目标文件。

Win32 API

了解了Windows软连接的基本概念后,我们从Win32 API的层面上来对其做进一步了解:
通过GetFileAttributesW查看软连接文件的dwFileAttributes属性:

// mklink /D C:\sym1 C:\dir
// C:\sym1 <==> C:\dir1
DWORD dwFileAttributes = ::GetFileAttributesW(LR"(C:\sym1)");
std::cout << (dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) << std::endl;; // 1
std::cout << (dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) << std::endl;; // 1

// mklink C:\symfile C:\file.txt
// C:\symfile <==> C:\file.txt
dwFileAttributes = ::GetFileAttributesW(LR"(C:\symfile)");
std::cout << (dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) << std::endl;; // 1
std::cout << (dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) << std::endl;; // 0

可以看到他们都有FILE_ATTRIBUTE_REPARSE_POINT符号位,每个软连接文件是否有FILE_ATTRIBUTE_DIRECTORY符号位只取决于创建他们的时候声明的参数。

在上一篇中我们封装了获取Windows文件信息的StatW()方法

std::optional<StatResult> StatW(const std::wstring& wPath)
{
BY_HANDLE_FILE_INFORMATION handleFileInformation{};
HANDLE hFile = ::CreateFileW(
wPath.c_str(),
GENERIC_READ,
FILE_SHARE_READ,
nullptr,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
0);
if (hFile == INVALID_HANDLE_VALUE) {
return std::nullopt;
}
if (::GetFileInformationByHandle(hFile, &handleFileInformation) == 0) {
::CloseHandle(hFile);
return std::nullopt;
}
::CloseHandle(hFile);
return std::make_optional<StatResult>(wPath, handleFileInformation);
}

CreateFileW()的第6个参数中我们传入了一个FILE_FLAG_OPEN_REPARSE_POINT符号位,该符号位用于告知系统:不要对Reparse Point重定向。传入该符号位,如果路径对应的文件是软连接文件,则会返回其自身的句柄。反之则会触发重定向,重定向会级联触发直到找到最终指向的Target文件的句柄并返回,如果中途失败则打开失败返回INVALID_HANDLE_VALUE

除了CreateFileW()还有一系列涉及路径的Windows API可能会因为软连接的重定向影响行为,详见Symbolic Link Effects on File Systems Functions

因此,在遍历并处理目录中文件的时候,建议对CreateFileW传入FILE_FLAG_OPEN_REPARSE_POINT符号位来保证获得的句柄一定是Reparse Point文件本身的句柄。如果启动了重定向,获得的句柄可能是指向最终文件的句柄,而GetFileInformationByHandle获取的信息自然也是最终文件的信息,handleFileInformation.dwFileAttributes也是最终文件的dwFileAttributes,就不一定含有FILE_ATTRIBUTE_REPARSE_POINT,则无法再判断文件是否是符号链接了。同时,由于dwFileAttributes是最终重定向后文件的dwFileAttributes,当前路径对应文件是否是一个目录/文件也无从判断。所以,正常的文件处理流程应该是:

  1. CreateFileW传入FILE_FLAG_OPEN_REPARSE_POINT符号位保证获得的句柄一定是Reparse Point文件本身
  2. GetFileInformationByHandle获取dwFileAttributes
  3. 判断文件是否有FILE_ATTRIBUTE_REPARSE_POINT符号位
  4. 进一步判断Reparse Point的类型,并决定是否重定向

要处理Reparse Point类型的文件就需要判断他们是哪一种Reparse Point。由于Reparse Point机制实现了多种文件的重定向能力,所以每一种Reparse Point类型都对应一种DWORD类型的Reparse Tag。再winnt.h中有形如IO_REPARSE_TAG_XXX的常量定义:

#define IO_REPARSE_TAG_MOUNT_POINT              (0xA0000003L)       
#define IO_REPARSE_TAG_HSM (0xC0000004L)
#define IO_REPARSE_TAG_HSM2 (0x80000006L)
#define IO_REPARSE_TAG_SIS (0x80000007L)
#define IO_REPARSE_TAG_WIM (0x80000008L)
#define IO_REPARSE_TAG_SYMLINK (0xA000000CL)
...

本文只介绍其中两种:IO_REPARSE_TAG_MOUNT_POINTIO_REPARSE_TAG_SYMLINK。从字面上看他们分别用于标记挂载点和符号链接。通过他们就可以判断部分Reparse Point文件的类型了。Windows要获取文件的Reparse Tag需要用FindFirstFileW API获取WIN32_FIND_DATAW结构体中的保留字段dwReserved0:(该方法只对dwFileAttributes含有FILE_ATTRIBUTE_REPARSE_POINT的文件有用)
DWORD ReparseTag(const std::wstring &wPath) const
{
const DWORD DEFAULT_REPARSE_TAG = 0;
WIN32_FIND_DATAW findFileData{};
HANDLE fileHandle = ::FindFirstFileW(wPath.c_str(), &findFileData);
if (fileHandle == INVALID_HANDLE_VALUE) {
return DEFAULT_REPARSE_TAG;
}
::FindClose(fileHandle);
return findFileData.dwReserved0;
}

判断软连接

所有用mklink创建出的指向文件的符号链接和mklink /D创建出的指向目录的符号链接,这里统称Symbolic文件。只有它们的Reparse Tag等于IO_REPARSE_TAG_SYMLINK,而mklink /J创建出的目录链接,其Reparse Tag等于IO_REPARSE_TAG_MOUNT_POINT

由于目录链接Reparse Tag表现为挂载点,所以判断文件是否是软连接不能只用Reparse Tag。
我们不妨先撇下判断软连接,转而寻求另一条道路:尝试获取软连接指向的目标路径,如果成功获得,就说明这是个软连接。

获取软连接的目标路径是一个比较麻烦的事,查到现在也没查到什么适合的官方API,但是我翻到了DeviceIoControl API传入FSCTL_GET_REPARSE_POINT调用似乎可以查到Reparse Point的元数据。其结果存放在一个结构体_REPARSE_DATA_BUFFER中:

/*
* These structure is used for Interal Windows API.
* There's no associated import library to define them, so developers must define them maunally
*/

/* https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_reparse_data_buffer?redirectedfrom=MSDN */
typedef struct _REPARSE_DATA_BUFFER {
ULONG ReparseTag;
USHORT ReparseDataLength;
USHORT Reserved;
union {
struct {
USHORT SubstituteNameOffset;
USHORT SubstituteNameLength;
USHORT PrintNameOffset;
USHORT PrintNameLength;
ULONG Flags;
WCHAR PathBuffer[1];
} SymbolicLinkReparseBuffer;
struct {
USHORT SubstituteNameOffset;
USHORT SubstituteNameLength;
USHORT PrintNameOffset;
USHORT PrintNameLength;
WCHAR PathBuffer[1];
} MountPointReparseBuffer;
struct {
UCHAR DataBuffer[1];
} GenericReparseBuffer;
} DUMMYUNIONNAME;
} REPARSE_DATA_BUFFER, * PREPARSE_DATA_BUFFER;

Win32 API文档只给出了结构体_REPARSE_DATA_BUFFER的定义但无法再任何公开的官方头文件中引入这个结构体,因为它是一个Windows内部API用的结构体,需要我们手动在自己的代码中定义。接着就是用FSCTL_GET_REPARSE_POINT获得该结构体的内容:

/* 
* return pointer to REPARSE_DATA_BUFFER which store target info of file with reparse attribute,
* need to free memory if return non-nullptr value
*/
static REPARSE_DATA_BUFFER* GetReparseDataBufferW(const std::wstring& wPath)
{
REPARSE_DATA_BUFFER* pReparseBuffer = nullptr;
DWORD dwSize;
/* Open the file for read */
HANDLE hFile = ::CreateFileW(
wPath.c_str(),
GENERIC_READ,
FILE_SHARE_READ,
nullptr,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
0);
if (hFile == INVALID_HANDLE_VALUE) {
/* failed */
return nullptr;
}
/* Allocated areas info */
pReparseBuffer = (REPARSE_DATA_BUFFER*)::malloc(MAXIMUM_REPARSE_DATA_BUFFER_SIZE);
if (pReparseBuffer == nullptr) {
/* malloc failed */
::CloseHandle(hFile);
return nullptr;
}
bool ret = ::DeviceIoControl(
hFile,
FSCTL_GET_REPARSE_POINT,
nullptr,
0,
pReparseBuffer,
MAXIMUM_REPARSE_DATA_BUFFER_SIZE,
&dwSize,
nullptr);
if (!ret) {
/* failed */
::free(pReparseBuffer);
::CloseHandle(hFile);
return nullptr;
}
::CloseHandle(hFile);
return pReparseBuffer;
}

通过REPARSE_DATA_BUFFER获取链接目标路径:

std::optional<std::wstring> SymlinkTargetPathW(const std::wstring& m_wPath) const
{
REPARSE_DATA_BUFFER* pReparseBuffer = GetReparseDataBufferW(m_wPath);
if (pReparseBuffer == nullptr) {
/* failed */
return std::nullopt;
}

REPARSE_DATA_BUFFER* pReparseBuffer = GetReparseDataBufferW(m_wPath);
USHORT targetNameIndex = pReparseBuffer->MountPointReparseBuffer.SubstituteNameOffset / sizeof(WCHAR);
USHORT targetNameLength = pReparseBuffer->MountPointReparseBuffer.SubstituteNameLength / sizeof(WCHAR);
USHORT displayNameIndex = pReparseBuffer->MountPointReparseBuffer.PrintNameOffset / sizeof(WCHAR);
USHORT displayNameLength = pReparseBuffer->MountPointReparseBuffer.PrintNameLength / sizeof(WCHAR);
WCHAR* targetName = &pReparseBuffer->MountPointReparseBuffer.PathBuffer[targetNameIndex];
WCHAR* displayName = &pReparseBuffer->MountPointReparseBuffer.PathBuffer[displayNameIndex];
wTarget.assign(targetName, targetName + targetNameLength);
wPrintName.assign(displayName, displayName + displayNameLength);
::free(pReparseBuffer);

return std::make_optional<std::wstring>(wPrintName);

通过实验发现无论Reparse Tag是IO_REPARSE_TAG_MOUNT_POINTJunction还是Reparse Tag是IO_REPARSE_TAG_SYMLINKSymbolic都可以通过这种方式拿到。所以可以通过尝试用这种方法获取链接目标路径来判断是否是一个软连接。

参考:

上述方法中我们获取的是软连接的立即指向路径、及存在级联重定向场景时下一个目标的路径。如果要获取最终指向的目标路径,可以在CreateFileW时候不传入FILE_FLAG_OPEN_REPARSE_POINT开启重定向,并用GetFinalPathNameByHandle API根据句柄拿到最终路径:

/* return final path of reparse target */
std::optional<std::wstring> StatResult::FinalPathW() const
{
BY_HANDLE_FILE_INFORMATION handleFileInformation{};
HANDLE hFile = ::CreateFileW(
m_wPath.c_str(),
GENERIC_READ,
FILE_SHARE_READ,
nullptr,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS,
0);
if (hFile == INVALID_HANDLE_VALUE) {
return std::nullopt;
}
WCHAR wPathBuff[MAX_PATH] = L"";
DWORD length = ::GetFinalPathNameByHandleW(hFile, wPathBuff, MAX_PATH, FILE_NAME_NORMALIZED);
if (length == 0) {
/* failed */
::CloseHandle(hFile);
return std::nullopt;
}
if (length >= MAX_PATH) {
DWORD extendLength = length + 1;
WCHAR* wPathExtendBuff = new WCHAR[extendLength];
if (::GetFinalPathNameByHandleW(hFile, wPathExtendBuff, extendLength, FILE_NAME_NORMALIZED) == 0) {
/* failed */
delete[] wPathExtendBuff;
::CloseHandle(hFile);
return std::nullopt;
}
/* succeed */
::CloseHandle(hFile);
std::wstring wTargetPath(wPathExtendBuff);
delete[] wPathExtendBuff;
return NormalizeWin32PathW(wTargetPath);
}
/* succeed */
::CloseHandle(hFile);
std::wstring wTargetPath(wPathBuff);
return NormalizeWin32PathW(wTargetPath);
}

判断挂载点

对于挂载点,它的Reparse Tag一定是IO_REPARSE_TAG_MOUNT_POINT,但是由于对于该Reparse Tag的还可能是目录链接Junction,则可以通过尝试获取路径对于的挂载设备名来判断该路径是不是一个挂载点。获取挂载点设备名可以用GetVolumeNameForVolumeMountPointW API,它接受一个\结尾的目录路径,返回对应的设备名。如果执行成功则返回TRUE,说吗这是个挂载点。

/*
* If this sparse point has IO_REPARSE_TAG_MOUNT_POINT tag, it maybe a device mount point or a junction link
* if it's device point, GetVolumeNameForVolumeMountPointW can accquire device name
* if it's junction link, GetVolumeNameForVolumeMountPointW will return false
*/
std::optional<std::wstring> MountedDeviceNameW(const std::wstring& wPath) const
{
WCHAR deviceNameBuff[MAX_PATH] = L"";
std::wstring wCanonicalPath = CanonicalPathW();
/* GetVolumeNameForVolumeMountPointW require input path to end with backslash */
if (wCanonicalPath.back() != L'\\') {
wCanonicalPath.push_back(L'\\');
}
if (::GetVolumeNameForVolumeMountPointW(wCanonicalPath.c_str(), deviceNameBuff, MAX_PATH)) {
return std::make_optional<std::wstring>(deviceNameBuff);
}
return std::nullopt;
}

如果路径是一个挂载点,则它一定不是一个软连接。如果是路径IO_REPARSE_TAG_MOUNT_POINT

flowchart TD
Open(输入文件) --> CreateFileW
CreateFileW[CreateFileW传入
FILE_FLAG_OPEN_REPARSE_POINT禁用重定向] --> GetFileInformationByHandle GetFileInformationByHandle[GetFileInformationByHandle
获取dwFileAttributes] --> Query Query{查询dwFileAttributes
是否有FILE_ATTRIBUTE_SPARSE_FILE
符号位} Query --Y--> TagQuery Query --N--> CommonFile(普通文件) TagQuery[查询Sparse Tag] --> CheckSymlinkTag CheckSymlinkTag{检查Sparse Tag
是否等于IO_REPARSE_TAG_SYMLINK} --Y--> Symbolic(符号链接) CheckSymlinkTag --N--> QuerySparseBuffer[查询_REPARSE_DATA_BUFFER
尝试获取链接目标信息] QuerySparseBuffer --> CheckQuerySparseBuffer{查询目标路径成功} CheckQuerySparseBuffer --Y--> Junction(目录链接) CheckQuerySparseBuffer --N--> CheckQueryMount{查询挂载点信息成功} CheckQueryMount --Y--> MountPoint(挂载点) CheckQueryMount --N--> Known(未知Reparse类型)
Disqus评论区没有正常加载,请使用科学上网