2013. 5. 16. 17:43

volume shadow copy(vss) 컴파일(compile) 방법과 snapshot을 이용한 백업(backup)

머릿말

norton의 ghost와 같은 일반적인 백업 제품들은, live backup, 즉, 이미 운영중인 disk 백업을 지원합니다. 다시 말하자면, "읽고 쓰기가 진행되고 있는 disk"를 백업한다는 뜻입니다. 만일 한시간동안 백업을 진행한다면, 한시간 전 백업을 시작할 싯점과 지금의 백업 원본은 많이 다를 것인데, 이미 변경된 부분에 대한 추적과 저장은 백업의 안정성과도 직결되는 중요한 문제가 됩니다.

이러한 문제를 해결하기 위해, Windows에서는 OS 차원에서 해결 방법을 제공하고 있습니다. 이것이 바로 volume shadow copy입니다.

1. C:\와 같은 Disk를 vss로 snapshot 생성 요청한다.
2. vss는 해당 싯점의 C:\의 snapshot 경로를 전달한다. (\\?\GLOBALROOT\Device\....)
3. 백업 프로그램은 2.에서 알려준 경로로 백업을 진행한다.
   즉, \\?\GLOBALROOT\Device\...\Temp\a.txt는 실제로 C:\Temp\a.txt에 해당된다.
4. vss를 종료 처리한다.

위와 같은 절차로 안전하게 백업을 진행할 수 있습니다. 즉, 백업을 진행하는 프로세스는 C:\ 대신 \\?\GLOBALROOT\Device\...와 같은 snapshot 경로를 이용하여 파일을 접근하도록 합니다. 이런 경우, 다른 프로세스에서 C:\... 와 같은 어떤 파일을 변경하더라도, \\?\GLOBALROOT\Device\...에는 변경이 적용되지 않습니다. 즉, 백업 프로세스는 위 절차중 2.의 싯점에 해당되는 C:\를 백업하게 됩니다.

예제

(주의 : 해당 소스는 CCL 라이선스로, 상업용 목적은 허락하지 않습니다.)

우선, vss api를 이용하기엔, 컴파일 과정이 다소 까다롭습니다. 우선 주어진 shadow.zip은, 개발 pc와 테스트 환경 모두 Windows 7, 그리고 컴파일러는 Visual studio 2008을 기준으로 하였습니다.
만일, 테스트 환경이 XP이라면, 코드 중간에,

...
// #define VSS_PLATFORM_XP
// #define VSS_PLATFORM_WIN2003
...
==>
...
#define VSS_PLATFORM_XP
// #define VSS_PLATFORM_WIN2003
...

와 같이 define을 활성화 시키십시요. 이런 경우, XP에서는 실행되지만, Vista나 Windows 7에서는 실행이 안 될 수 있습니다.
일단, 컴파일후 실행을 합니다.(Vista 이상에서는 관리자 권한으로 실행하셔야 합니다. 아예, Visual Studio를 관리자 권헌으로 실행하시는편이 편합니다.) 그럼 아래와 같은 메시지가 발생합니다.
우선, 실행하면, 다음과 같은 메시지창이 발생합니다.


그럼, 다음과 같이 C:\Temp\a.txt를 저장해 보겠습니다.

Hello, world!!!!
greenfish !!!!

그럼다음, 위 메시지 창에 "확인"을 누릅니다.
그럼 조금만 기다리면 다음과 같은 창이 뜹니다.

즉, 이말인즉, 이제부터 a.txt의 내용을 변경해라라는 뜻입니다.
이제 다음과 같이 a.txt를 변경하도록 하겠습니다.

abcdef
123456
foobar

변경후 위 메시지창을 닫으면, 아래와 같은 결과를 보여줍니다.

-----------------------------------------------
 Original C:\Temp\a.txt
 Snapshot
\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy40\Temp\a.txt, 32 bytes
-----------------------------------------------
Hello, world!!!!
greenfish !!!!

(EOF)
It is old file, isn't it?
이 내용은, 현재 C:\Temp\a.txt의 snapshot 경로인 \\?\GLOBAL...\a.txt의 파일내용을 보여준 것입니다. 중간에 보시면 알겠지만, 직전에 분명 a.txt 내용을 변경하였는데, \\?\GLOBAL...\a.txt는 과거 파일 내용을 보여주고 있습니다. 즉, C:\Temp\a.txt의 내용이 아무리 변경되더라도, \\?\GLOBAL...\a.txt는 snapshot 당시의 파일 내용을 보여주게 됩니다. 그렇기 때문에, \\?\GLOBAL...\ 경로를 가지고 백업하게 되면 안전하게 백업할 수 있습니다. 이것이 바로 vss의 가장 중요한 기능이 아닐까 합니다.

VSS 빌드

VSS는 COM으로 이뤄져 있는데, 특이하게도 XP/Windows 2003은 그 이후의 OS, 즉, Vista와 내부적인 API가 호환되지 않습니다. 예를 들어, Vista 이상에서 사용할 수 있는 바이너리를 XP에서 실행하면 다음과 같이, "프로시저 시작 지점 CreateVssBackupComponentsInternal을(를) DLL VSSAPI.DLL에서 찾을 수 없습니다."와 같은 오류가 발생하게 됩니다.

일단, Visual studio 2008로 빌드하면 기본적으로 Vista, Windows 7에 해당되는 바이너리를 구할 수 있습니다. 만일, XP에서 실행될 바이너리를 만들기 위해서는 SDK를 설치하여 과거의 header와 lib를 링크해야 합니다. 이는 꽤나 귀찮은 일인데, 왜냐하면, Windows 7, XP, 그리고 Windows 2003 용 바이너리를 빌드해야 하기 때문입니다. 좀더 용이한 접근을 위해 아래와 같은 DEFINE을 가지고 구별하도록 하였습니다. 즉, 아래 두개 모두 define이 안되어 있다면, Windows 7 혹은 Vista, VSS_PLATFORM_XP만 define하는 경우는 XP, 그리고 VSS_PLATFORM_WIN2003만 define하는 경우는 Windows 2003 빌드가 나오도록 하였습니다. 물론, source 코드 이외 Project setting에서 해당 define을 추하하는 것이 좀더 옳은 방법일 것입니다.

// #define VSS_PLATFORM_XP
// #define VSS_PLATFORM_WIN2003

첨부된 shadow.cpp의 내용은 다음과 같습니다.

// shadow.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <conio.h>
#include <assert.h>
#include <strsafe.h>

//////////////////////////////////////////////////////////////////////////
//
// License : CCL (CC BY-NC-SA)
// author : greenfish, greenfish@gmail.com | greenfishblog.tistory.com
// [warning] this source is under Noncommercial.
//
//////////////////////////////////////////////////////////////////////////

// Compile 환경 (Windows 7)
//
// 실행 환경이, Windows XP, Windows Server 2003 R2, Windows Server 2003,
// 이라면,
// http://www.microsoft.com/en-us/download/confirmation.aspx?id=23490
// 에서 sdk를 통해 빌드해야 한다.
//
// 설치후,
// C:\Program Files\Microsoft\VSSSDK72\lib
// 에 실행 platform의 lib가 있다.
//

// VSS Platform을 선택한다.
// "winxp", "win2003"을 지원한다.
// 만일 define이 되어 있지 않다면, Win7 이상 지원이다.

// #define VSS_PLATFORM_XP
// #define VSS_PLATFORM_WIN2003

#ifdef VSS_PLATFORM_XP
#define VSS_PLATFORM "winxp"
#elif VSS_PLATFORM_WIN2003
#define VSS_PLATFORM "win2003"
#endif

// 각 Platform에 맞는 lib를 link한다.
#ifdef VSS_PLATFORM
	#ifdef _WIN64
	#pragma comment(lib, "./lib/amd64." VSS_PLATFORM "vssapi.lib")
	#else
	#pragma comment(lib, "./lib/i386." VSS_PLATFORM ".vssapi.lib")
	#endif
#else
	#pragma comment(lib, "vssapi.lib")
#endif // VSS_PLATFORM

// http://msdn.microsoft.com/en-us/library/windows/desktop/aa384627(v=vs.85).aspx
// 에 따르면 각 상황별 include가 설명되어 있다.

#ifdef VSS_PLATFORM_XP
#define INCLUDE_VSS	"./inc/winxp/vss.h"
#define INCLUDE_VSWRITER	"./inc/winxp/vswriter.h"
#define INCLUDE_VSBACKUP	"./inc/winxp/vsbackup.h"
#elif VSS_PLATFORM_WIN2003
#define INCLUDE_VSS	"./inc/win2003/vss.h"
#define INCLUDE_VSWRITER	"./inc/win2003/vswriter.h"
#define INCLUDE_VSBACKUP	"./inc/win2003/vsbackup.h"
#else
#define INCLUDE_VSS	<vss.h>
#define INCLUDE_VSWRITER	<vswriter.h>
#define INCLUDE_VSBACKUP	<vsbackup.h>
#endif

// 각 Platform에 맞는 include를 수행한다.
#include INCLUDE_VSS
#include INCLUDE_VSWRITER
#include INCLUDE_VSBACKUP
//#include <vsserror.h>	// 오류 코드 포함됨. 주석 해제시 compile 오류 발생

#define ERR(hr) printf("ERR, Line=%d, HRESULT=0x%x\r\n", __LINE__, hr)

int _tmain(int argc, _TCHAR* argv[])
{
	HRESULT			hr			= S_OK;
	BOOL			bProperty			= FALSE;
	BOOL			bCoInit			= FALSE;
	IVssBackupComponents*	pVssBackup		= NULL;
	IVssAsync*		pAsyncMetadata		= NULL;
	IVssAsync*		pAsyncPrepare		= NULL;
	IVssAsync*		pAsyncSnapshotSet		= NULL;
	VSS_ID			guidSnapshotSet		= {0,};
	TCHAR			szVolumeName[MAX_PATH]	= {0,};
	VSS_SNAPSHOT_PROP		stSnapshotProp		= {0,};

	::MessageBoxA(NULL, "Create and modify C:\\Temp\\a.txt", "Info", MB_OK | MB_ICONINFORMATION);

	if (SUCCEEDED(::CoInitialize(NULL)))
	{
		bCoInit = TRUE;
	}

	// Create Interface
	hr = ::CreateVssBackupComponents(&pVssBackup);
	if ((FAILED(hr)) || (NULL == pVssBackup))
	{
		// hr에 대한 오류코드는,
		// vsserror.h를 참고한다.
		ERR(hr);
		assert(FALSE);
		goto FINAL;
	}

	// Creating a snapshot
	// volume shadow service가 시작된다.
	hr = pVssBackup->InitializeForBackup();
	if (FAILED(hr))
	{
		if (0x80042302 == hr)
		{
			// 1. CoInitialize가 안된 경우 였다.
			// 2. Volume shadow service가 시작못했다. (시작 유형이 disable인 경우)
		}
		ERR(hr);
		assert(FALSE);
		goto FINAL;
	}

	// context를 생성한다.
#if defined (VSS_PLATFORM_XP)
	// WinXP에서는 SetContext는 기본값만 사용되므로,
	// SetContext를 호출하면, E_NOTIMPL(=0x80004001)이 리턴된다.
	// (http://msdn.microsoft.com/ko-kr/library/windows/desktop/aa382842(v=vs.85).aspx 의 remark 참고)
	// 따라서 SetContext를 호출하지 않는다.
	hr = S_OK;
#else
	hr = pVssBackup->SetContext(VSS_CTX_BACKUP | VSS_CTX_CLIENT_ACCESSIBLE_WRITERS | VSS_CTX_APP_ROLLBACK);
#endif
	if (FAILED(hr))
	{
		ERR(hr);
		assert(FALSE);
		goto FINAL;
	}

	// Metadata 설정
	hr = pVssBackup->GatherWriterMetadata(&pAsyncMetadata);
	if ((FAILED(hr)) || (NULL == pAsyncMetadata))
	{
		ERR(hr);
		assert(FALSE);
		goto FINAL;
	}

	hr = pAsyncMetadata->Wait();
	if (FAILED(hr))
	{
		ERR(hr);
		assert(FALSE);
		goto FINAL;
	}

	// Snapshot 설정
	hr = pVssBackup->StartSnapshotSet(&guidSnapshotSet);
	if (FAILED(hr))
	{
		if (VSS_E_SNAPSHOT_SET_IN_PROGRESS == hr)
		{
			// 이미 한번이라도 다른 프로세스에서 StartSnapshotSet이 실행된 적 있다.
			//
			// 만약 해당 프로세스가 종료되거나 문제가 있었다면,
			// 계속 오류가 발생할 수 있을것 같다.
			// 이때는 scm을 통해 "Volume Shadow Copy" 서비스를 재시작 시키자.
			// 뭐.. 그렇다면, 혹시나 해당 프로세스가 문제가 없었다면,
			// 해당 프로세스가 오류가 발생할 수 있을 것 같다.
		}
		ERR(hr);
		assert(FALSE);
		goto FINAL;
	}

	// shadow copy에 volume을 추가한다.
	StringCchCopy(szVolumeName, MAX_PATH, TEXT("C:\\"));
	hr = pVssBackup->AddToSnapshotSet(szVolumeName, GUID_NULL, &guidSnapshotSet);
	if (FAILED(hr))
	{
		ERR(hr);
		assert(FALSE);
		goto FINAL;
	}

	// Backup 상태를 설정한다.
	hr = pVssBackup->SetBackupState(FALSE, FALSE, VSS_BT_COPY);
	if (FAILED(hr))
	{
		ERR(hr);
		assert(FALSE);
		goto FINAL;
	}

	// Prepare backup 실행
	hr = pVssBackup->PrepareForBackup(&pAsyncPrepare);
	if ((FAILED(hr)) || (NULL == pAsyncPrepare))
	{
		ERR(hr);
		assert(FALSE);
		goto FINAL;
	}

	hr = pAsyncPrepare->Wait();
	if (FAILED(hr))
	{
		ERR(hr);
		assert(FALSE);
		goto FINAL;
	}

	// 해당 설정에 모든 snapshot을 commit한다.
	hr = pVssBackup->DoSnapshotSet(&pAsyncSnapshotSet);
	if ((FAILED(hr)) || (NULL == pAsyncSnapshotSet))
	{
		ERR(hr);
		assert(FALSE);
		goto FINAL;
	}

	hr = pAsyncSnapshotSet->Wait();
	if (FAILED(hr))
	{
		ERR(hr);
		assert(FALSE);
		goto FINAL;
	}

	hr = pVssBackup->GetSnapshotProperties(guidSnapshotSet, &stSnapshotProp);
	if (FAILED(hr))
	{
		ERR(hr);
		assert(FALSE);
		goto FINAL;
	}
	else
	{
		// GetSnapshotProperties 성공
		bProperty = TRUE;
	}

	// Test 코드
	{
		HANDLE hFile		= INVALID_HANDLE_VALUE;
		TCHAR  szPath[MAX_PATH]	= {0,};
		DWORD  dwReaded		= 0;
		char   buf[1025]		= {0,};
		::MessageBoxA(NULL, "Now, you can change C:\\Temp\\a.txt while snapshot is running", "Info", MB_OK | MB_ICONINFORMATION);
		StringCchPrintf(szPath, MAX_PATH, L"%s\\Temp\\a.txt", stSnapshotProp.m_pwszSnapshotDeviceObject);
		hFile = ::CreateFile(szPath, GENERIC_READ, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
		if (INVALID_HANDLE_VALUE != hFile)
		{
			if (FALSE != ::ReadFile(hFile, buf, 1024, &dwReaded, NULL))
			{
				printf("-----------------------------------------------\r\n");
				printf(" Original C:\\Temp\\a.txt\r\n");
				_tprintf( L" Snapshot %s, %d bytes\r\n", szPath, dwReaded);
				printf("-----------------------------------------------\r\n");
				printf(buf);
				printf("\r\n(EOF)\r\n");
				printf("It is old file, isn't it?\r\n");
			}
			else
			{
				printf("Error, GetLastError=%d\r\n", ::GetLastError());
			}

			::CloseHandle(hFile);
			hFile = INVALID_HANDLE_VALUE;
		}
		else
		{
			printf("Error(2), GetLastError=%d\r\n", ::GetLastError());
		}
	}

	// stSnapshotProp::m_pwszSnapshotDeviceObject를 사용한다.
	// D:\somefile.txt
	// -->
	// \Device\HarddiskVolumeShadowCopy1\somefile.txt

FINAL:

	if (TRUE == bProperty)
	{
		VssFreeSnapshotProperties(&stSnapshotProp);
		bProperty = FALSE;
	}

	if (NULL != pAsyncSnapshotSet)
	{
		pAsyncSnapshotSet->Release();
		pAsyncSnapshotSet = NULL;
	}

	if (NULL != pAsyncPrepare)
	{
		pAsyncPrepare->Release();
		pAsyncPrepare = NULL;
	}

	if (NULL != pAsyncMetadata)
	{
		pAsyncMetadata->Release();
		pAsyncMetadata = NULL;
	}

	if (NULL != pVssBackup)
	{
		pVssBackup->FreeWriterMetadata();
		pVssBackup->Release();
		pVssBackup = NULL;
	}

	if (TRUE == bCoInit)
	{
		::CoUninitialize();
		bCoInit = FALSE;
	}

	_tprintf(L"Press a key to exit\r\n");
	getch();
	return 0;
}