0%

Windows创建卷影副本

Windows Server 2008以上的版本,管理员可以通过卷影副本服务(Volume Shadow Service)在指定的卷上创建备份的卷影副本。卷影副本类似于快照(Snapshot),用户可以基于快照卷恢复到之前某个时间的版本,备份软件也可基于快照做一致性备份。因此,它可以极大保护服务器的数据安全。

本文就介绍VSS相关操作以及如何用C++调用相关API。

项目完整地址见https://github.com/XUranus/Win32VSSWrapper
下文所说的卷影副本快照属于一个概念

vssadmin卷操作

所有支持VSS的Windows系统已经提供了一个管理卷影副本的命令行工具vssadmin。使用管理员权限进入cmd,执行卷影副本命令vssadmin,可以看到所有支持的命令:

C:\WINDOWS\system32>vssadmin
vssadmin 1.1 - 卷影复制服务管理命令行工具
(C) 版权所有 2001-2013 Microsoft Corp.

---- 支持的命令 ----

Delete Shadows - 删除卷影副本
List Providers - 列出已注册的卷影副本提供程序
List Shadows - 列出现有卷影副本
List ShadowStorage - 列出卷影副本存储关联
List Volumes - 列出可以进行卷影副本处理的卷
List Writers - 列出订阅的卷影副本写入程序
Resize ShadowStorage - 调整卷影副本存储关联的大小

这里我们主要讨论副本的创建、删除、查询。首先用vssadmin list shadows列出所有卷影副本:

C:\WINDOWS\system32>vssadmin list shadows
vssadmin 1.1 - 卷影复制服务管理命令行工具
(C) 版权所有 2001-2013 Microsoft Corp.

卷影副本集 ID: {d85162e7-db49-4035-956e-465e1a7f3747} 的内容
在创建时间: 2023/1/17 13:46:05 含有 1 个卷影副本
卷影副本 ID: {f8bf1cfc-ddab-47ad-8156-3f1f45f70641}
原始卷: (C:)\\?\Volume{a501f5cc-311e-423c-bc58-94a6c1b6b509}\
卷影副本卷: \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy1
源起机器: DESKTOP-TODPNBO
服务机器: DESKTOP-TODPNBO
提供程序: 'Microsoft Software Shadow Copy provider 1.0'
类型: ClientAccessibleWriters
属性: 持续, 客户端可访问, 无自动释放, 差异, 自动还原

卷影副本集 ID: {1abe303d-eb32-445a-ae78-72432980191b} 的内容
在创建时间: 2023/1/18 23:38:42 含有 1 个卷影副本
卷影副本 ID: {5fee2842-be4e-4477-86c6-1c0c28221a4f}
原始卷: (C:)\\?\Volume{a501f5cc-311e-423c-bc58-94a6c1b6b509}\
卷影副本卷: \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy2
源起机器: DESKTOP-TODPNBO
服务机器: DESKTOP-TODPNBO
提供程序: 'Microsoft Software Shadow Copy provider 1.0'
类型: ClientAccessibleWriters
属性: 持续, 客户端可访问, 无自动释放, 差异, 自动还原

vssadmin list shadows可以展开每个副本的详细信息,包含:

  • 卷影副本ID:这是每个VSS快照的唯一GUID标识符
  • 卷影副本集ID:一个副本集可以是一个或多个同时创建的快照集合,一个副本集也有一个GUID标识符。一个快照只对应一个副本集,而一个副本集可能会包含多个快照。
  • 属性:快照可能是持久的、也可能会在创建后一段时间内自动释放。

vssadmin list shadowstorage用于展示所有支持快照的卷的空间分配情况。

C:\WINDOWS\system32>vssadmin list shadowstorage
vssadmin 1.1 - 卷影复制服务管理命令行工具
(C) 版权所有 2001-2013 Microsoft Corp.

卷影副本存储关联
卷: (C:)\\?\Volume{a501f5cc-311e-423c-bc58-94a6c1b6b509}\
卷影副本存储卷: (C:)\\?\Volume{a501f5cc-311e-423c-bc58-94a6c1b6b509}\
已用卷影副本存储空间: 1.80 GB (0%)
分配的卷影副本存储空间: 2.16 GB (0%)
最大卷影副本存储空间: 4.75 GB (2%)

使用vssadmin add shadowstorage /for=<Volume1> /on=<Volume2>可以创建卷影副本。其中<Volume1>指定了要保护的卷,<Volume2>指定了副本存放的卷。副本存放的卷可以是要保护的本地卷,也可以是其他卷。,例如:

C:\WINDOWS\system32>vssadmin add shadowstorage /for=c: /on=c:

每一个创建完成的卷影副本都对应一个ID和设备名,可以根据其设备名创建符号链接来实现挂载。例如将\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy2挂载到C:\Users\XUranus\Desktop\vss1的位置上,该目录可以获得一个只读的文件系统:

C:\WINDOWS\system32>mklink /d C:\Users\XUranus\Desktop\vss1 \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy2
C:\Users\XUranus\Desktop\vss1 <<===>> \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy2 创建 的符号链接

删除快照也是使用卷影副本ID,例如删除之前创建的C:盘下的快照{f8bf1cfc-ddab-47ad-8156-3f1f45f70641}

vssadmin delete shadows /for=c:/shadow={f8bf1cfc-ddab-47ad-8156-3f1f45f70641}

详见vssadmin delete shadows

VSS Win32 API

调用VSS相关Win32 API需要依赖VssApi.lib,部分低版本的Windows可能需要安装相应的SDK。

我们将对VSS快照相关Win32 API做一个简单的封装。首先需要引入VSS相关头文件:vss.hvswriter.hvsbackup.h。由于VSS API依赖COM库相关结构,还需要包含comdef.h

// VssClient.h
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <objbase.h>
#include <vss.h>
#include <vswriter.h>
#include <vsbackup.h>
#include <comdef.h>

前文用vssadmin列出所有创建的VSS快照,可以看到形如{a501f5cc-311e-423c-bc58-94a6c1b6b509}这样的副本/副本集ID。VSS API使用VSS_ID类型的结构体来描述这类ID,它是一种Windows GUID类型。为了减少头文件污染,这里统一用字符串来标记这类ID的字面量。于是首先要提供VSS_IDstd::string的转化操作:

// VssClient.h
/**
* An Util Class, Used to automatically release a CoTaskMemAlloc allocated pointer
* when the instance of this class goes out of scope(RAII)
* (even if an exception is thrown)
**/
class CAutoComPointer
{
public:
CAutoComPointer(LPVOID ptr): m_ptr(ptr) {};
~CAutoComPointer() { ::CoTaskMemFree(m_ptr); }
private:
LPVOID m_ptr;
};
// VssClient.cpp
std::optional<std::wstring> VssID2WStr(const VSS_ID& vssID)
{
LPOLESTR wVssIDBuf = nullptr;
CAutoComPointer ptrAutoCleanUp(wVssIDBuf);
HRESULT hr = ::StringFromIID(vssID, &wVssIDBuf);
if (FAILED(hr)) {
return std::nullopt;
}
std::wstring wVssIDStr(wVssIDBuf);
return std::make_optional<std::wstring>(wVssIDStr);
}

std::optional<VSS_ID> VssIDfromWStr(const std::wstring& vssIDWstr)
{
VSS_ID vssID;
HRESULT hr = ::IIDFromString(vssIDWstr.c_str(), &vssID);
if (FAILED(hr)) {
return std::nullopt;
}
return std::make_optional<VSS_ID>(vssID);
}

可以看到这里额外定义了CAutoComPointer类型,因为这里及后续的很多VSS API都会用参数返回堆上分配的空间地址,并要求调用者手动调用::CoTaskMemFree(LPVOID)释放分配的资源,这里用RAII的机制可以有效避免内存泄漏。VSS_IDstd::string的互转是通过::StringFromIID::IIDFromString实现的。

这两个API都返回HRESULT类型,这是一类Win32错误码返回类型,后续其余VSS API也会相继用到它,可以用FAILED宏判断这类返回值是否成功。HRESULT详见Common HRESULT Values

定义两个和宏CHECK_HR_RETURNCHECK_BOOL_RETURN来判断操作结果并返回

// VssClient.cpp
#define CHECK_HR_RETURN(HR, FUNC, RET) \
do { \
_com_error err(HR); \
if (HR != S_OK) { \
::fprintf(stderr, "HRESULT Return FAILED, Function: " ## FUNC ## ", Error: %s\n", err.ErrorMessage()); \
return RET; \
} \
} \
while (0)

#define CHECK_BOOL_RETURN(BOOLVALUE, FUNC, RET) \
do { \
if ((!BOOLVALUE)) { \
::fprintf(stderr, "Boolean Return False, Function: " ## FUNC ## "\n"); \
return RET; \
} \
} \
while (0)

给出类VssClient的定义:

// VssClient.h
/**
* The class providing snapshot creation/delete/query
* Not thread-safe
*/
class VssClient {
public:
bool VssClient::InitializeCom();
private:
bool m_comInitialized = false;
IVssBackupComponents* m_pVssObject = nullptr;
}

VSS API需要依赖一个核心类IVssBackupComponents,所有创建、查询、删除等逻辑都是这个类提供的。在创建IVssBackupComponents之前需要先初始化COM库。COM库初始化详见https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-coinitializeex

首先实现COM库的初始化方法和取消初始化方法:

// VssClient.cpp
/* initialzie COM */
bool VssClient::InitializeCom()
{
if (m_comInitialized) {
return true;
}
HRESULT hr = ::CoInitializeEx(nullptr, COINIT::COINIT_MULTITHREADED);
CHECK_HR_RETURN(hr, "CoInitializeEx", false);
m_comInitialized = true;
hr = CoInitializeSecurity(
NULL, // Allow *all* VSS writers to communicate back!
-1, // Default COM authentication service
NULL, // Default COM authorization service
NULL, // reserved parameter
RPC_C_AUTHN_LEVEL_PKT_PRIVACY, // Strongest COM authentication level
RPC_C_IMP_LEVEL_IMPERSONATE, // Minimal impersonation abilities
NULL, // Default COM authentication settings
EOAC_DYNAMIC_CLOAKING, // Cloaking
NULL // Reserved parameter
);
CHECK_HR_RETURN(hr, "CoInitializeSecurity", false);
return true;
}

void VssClient::UninitializeCom()
{
if (!m_comInitialized) {
return;
}
::CoUninitialize();
m_comInitialized = false;
}

由于所有VSS操作都需要在初始化COM库后进行,在VssClient构造时就执行相关操作,并在析构时取消初始化并析构IVssBackupComponents* m_pVssObject

// VssClient.cpp
VssClient::VssClient()
{
InitializeCom();
}

VssClient::~VssClient()
{
if (m_pVssObject != nullptr) {
m_pVssObject->Release();
m_pVssObject = nullptr;
}
UninitializeCom();
}

之前说到VSS操作依赖一个核心类IVssBackupComponents实现,这里我们将其指针作为VssClient的成员,在InitializeVssComponent()中使用APICreateVssBackupComponents()初始化,由于每种VSS操作(创建、删除、查询)可能需要用到不同的上下文,而IVssBackupComponents* m_pVssObject指向唯一的上下文,所以每次操作我们选择用其Release()方法释放资源,再重新创建实例:

// VssClient.cpp
bool VssClient::InitializeVssComponent()
{
if (m_pVssObject != nullptr) {
m_pVssObject->Release();
m_pVssObject = nullptr;
}
HRESULT hr = ::CreateVssBackupComponents(&m_pVssObject);
CHECK_HR_RETURN(hr, "CreateVssBackupComponents", false);
return true;
}

由于每次操作VSS需要不同的上下文,这里定义一个InitializeBackupContect(const VSS_SNAPSHOT_CONTEXT& context)方法用于依据不同上下文初始化对应的IVssBackupComponents实例并设置其上下文:
// VssClient.cpp
bool VssClient::InitializeBackupContect(const VSS_SNAPSHOT_CONTEXT& context)
{
CHECK_BOOL_RETURN(InitializeVssComponent(), "InitializeVssComponent", false);

HRESULT hr = m_pVssObject->InitializeForBackup();
CHECK_HR_RETURN(hr, "InitializeForBackup", false);

hr = m_pVssObject->SetContext(context);
CHECK_HR_RETURN(hr, "SetContext", false);

hr = m_pVssObject->SetBackupState(true, false, VSS_BT_FULL, false);
CHECK_HR_RETURN(hr, "SetBackupState", false);

return true;
}

好了,至此我们已经完成了核心类IVssBackupComponents实例的创建与初始化,接下来就是封装VSS的集中操作了。

创建快照的步骤如下:

  1. 先用IVssBackupComponents::StartSnapshotSet初始化一个卷影副本集,获取副本集ID
  2. 然后用IVssBackupComponents::AddToSnapshotSet依次传入要打快照的卷对应的驱动器路径
  3. 然后IVssBackupComponents::PrepareForBackup准备创建快照需要的资源
  4. 最后IVssBackupComponents::DoSnapshotSet为每个卷生成卷影副本,每个卷影副本对应唯一的副本ID,它们属于共同的卷影副本集

创建快照实现如下:

// VssClient.cpp
std::optional<SnapshotSetResult> VssClient::CreateSnapshotsW(const std::vector<std::wstring>& wVolumePathList)
{
InitializeBackupContect(VSS_CTX_APP_ROLLBACK);
SnapshotSetResult result;
/* no need to call GatherWriterMetadata due to no writers involved */
VSS_ID snapshotSetID;
HRESULT hr = m_pVssObject->StartSnapshotSet(&snapshotSetID);
CHECK_HR_RETURN(hr, "StartSnapshotSet", std::nullopt);

result.m_wSnapshotSetID = VssID2WStr(snapshotSetID).value();

for (const std::wstring& wVolumePath: wVolumePathList) {
VSS_ID snapshotID;
WCHAR volume[MAX_PATH] = { L'\0' };
wcscpy_s(volume, MAX_PATH, wVolumePath.c_str());
hr = m_pVssObject->AddToSnapshotSet(volume, GUID_NULL, &snapshotID);
CHECK_HR_RETURN(hr, "AddToSnapshotSet", std::nullopt);
result.m_wSnapshotIDList.emplace_back(VssID2WStr(snapshotID).value());
}
CHECK_BOOL_RETURN(PrepareForBackupSync(), "PrepareForBackupSync", std::nullopt);
CHECK_BOOL_RETURN(DoSnapshotSetSync(), "DoSnapshotSetSync", std::nullopt);

/* no need to call BackupComplete due to no writers involved */
return std::make_optional<SnapshotSetResult>(result);
}

其中VSS_CTX_APP_ROLLBACK是创建快照指定的上下文,用于创建持久化的快照(不会定期清理,需要用户程序自行删除)。PrepareForBackupSyncDoSnapshotSetSync对应3、4两步:

bool VssClient::PrepareForBackupSync()
{
CComPtr<IVssAsync> pAsync;
HRESULT hr = m_pVssObject->PrepareForBackup(&pAsync);
CHECK_HR_RETURN(hr, "PrepareForBackup", false);
CHECK_BOOL_RETURN(WaitAndCheckForAsyncOperation(pAsync), "PrepareForBackup Wait", false);
return true;
}

bool VssClient::DoSnapshotSetSync()
{
CComPtr<IVssAsync> pAsync;
HRESULT hr = m_pVssObject->DoSnapshotSet(&pAsync);
CHECK_HR_RETURN(hr, "DoSnapshotSet", false);
CHECK_BOOL_RETURN(WaitAndCheckForAsyncOperation(pAsync), "DoSnapshotSet Wait", false);
return true;
}

bool VssClient::WaitAndCheckForAsyncOperation(IVssAsync* pAsync)
{
HRESULT hr = pAsync->Wait();
CHECK_HR_RETURN(hr, "WaitAndCheckForAsyncOperation pAsync->Wait", false);

/* Check the result of the asynchronous operation */
HRESULT hrReturned = S_OK;
hr = pAsync->QueryStatus(&hrReturned, nullptr);
CHECK_HR_RETURN(hr, "WaitAndCheckForAsyncOperation pAsync->QueryStatus", false);

/* Check if the async operation succeeded... */
if (hrReturned != VSS_S_ASYNC_FINISHED) {
return false;
}
return true;
}

这里IVssBackupComponents::DoSnapshotSet是一个异步API,通过参数返回IVssAsync类型。这里通过WaitAndCheckForAsyncOperation将这类异步操作封装为同步操作,利用IVssAsync::Wait阻塞调用直到结果返回,并通过IVssAsync::QueryStatus查询结果是否成功。

CComPtr是一种用于释放COM资源的RAII机制的智能指针,详见CComPtr Class

最后写一个Demo测试快照的创建:

// Create snapshots for C: and D:
std::vector<std::wstring> wVolumePathList { L"C:\\", L"D:\\" };
VssClient vssClient;
std::optional<SnapshotSetResult> result = vssClient.CreateSnapshotsW(wVolumePathList);
if (!result) {
std::wcout << L"Failed to Create Snapshots" << std::endl;
return -1;
} else {
std::wcout << L"Create Snapshots Success" << std::endl;
}
std::wcout << L"Shadow Set ID: " << result->SnapshotSetIDW() << std::endl;
int index = 0;
for (const std::wstring& wSnapshotID: result->SnapshotIDListW()) {
std::wcout << L"snapshot ID[" << ++index << L"]: " << wSnapshotID << std::endl;
}

此时创建的快照已经可以用之前提到的vssadmin工具查询到了。有关快照的删除、查询、挂载也是基于IVssBackupComponents* m_pVssObject实现,详见https://github.com/XUranus/Win32VSSWrapper,可以结合官方文档学习,这里就不再赘述了。

参考资料

Disqus评论区没有正常加载,请使用科学上网