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.h
、vswriter.h
、vsbackup.h
。由于VSS API依赖COM库相关结构,还需要包含comdef.h
// VssClient.h
前文用vssadmin
列出所有创建的VSS快照,可以看到形如{a501f5cc-311e-423c-bc58-94a6c1b6b509}
这样的副本/副本集ID。VSS API使用VSS_ID
类型的结构体来描述这类ID,它是一种Windows GUID类型。为了减少头文件污染,这里统一用字符串来标记这类ID的字面量。于是首先要提供VSS_ID
和std::string
的转化操作:
// VssClient.h |
// VssClient.cpp |
可以看到这里额外定义了CAutoComPointer
类型,因为这里及后续的很多VSS API都会用参数返回堆上分配的空间地址,并要求调用者手动调用::CoTaskMemFree(LPVOID)
释放分配的资源,这里用RAII的机制可以有效避免内存泄漏。VSS_ID
和std::string
的互转是通过::StringFromIID
和::IIDFromString
实现的。
这两个API都返回
HRESULT
类型,这是一类Win32错误码返回类型,后续其余VSS API也会相继用到它,可以用FAILED
宏判断这类返回值是否成功。HRESULT
详见Common HRESULT Values
定义两个和宏CHECK_HR_RETURN
和CHECK_BOOL_RETURN
来判断操作结果并返回// VssClient.cpp
给出类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的集中操作了。
创建快照的步骤如下:
- 先用
IVssBackupComponents::StartSnapshotSet
初始化一个卷影副本集,获取副本集ID - 然后用
IVssBackupComponents::AddToSnapshotSet
依次传入要打快照的卷对应的驱动器路径 - 然后
IVssBackupComponents::PrepareForBackup
准备创建快照需要的资源 - 最后
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
是创建快照指定的上下文,用于创建持久化的快照(不会定期清理,需要用户程序自行删除)。PrepareForBackupSync
和DoSnapshotSetSync
对应3、4两步:
bool VssClient::PrepareForBackupSync() |
这里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,可以结合官方文档学习,这里就不再赘述了。