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,可以结合官方文档学习,这里就不再赘述了。