使用 Win32 事务





5.00/5 (20投票s)
本文演示了如何将 Win32 事务用于文件和注册表操作。
引言
Win32 子系统是位于用户应用程序和 Windows 系统之间的接口。它是一组统一的 API,所有更高级别的框架都构建在其之上。几乎所有在 Windows 上运行的应用程序都会使用 Win32 子系统,因为 .NET Framework、Java 运行时库、MFC……都会将其更高级别的功能转换为 Win32 调用。
Win32 子系统本身并没有引起程序员太多关注,因为很少需要直接与它交互。尽管如此,它还是有一些非常有趣的、可能非常有用的功能。而且由于任何语言都可以进行 Win32 调用,因此将这些功能添加到您的工具箱中非常容易。
在本文中,我想解释一个我认为被严重低估的功能:事务。我将描述一个该功能具有巨大附加值的场景。我将简要介绍 API 本身,然后解释我如何在我的应用程序中实现它。
场景
考虑这样一个场景:我们的应用程序正在更新其配置。在我们的示例中,应用程序有一个可能位于磁盘上的配置文件,以及一个指示该文件路径的注册表值。如果应用程序想保存其最新的配置,它会使用新文件的名称更新注册表文件,并将文件保存到磁盘。
这是两个需要同时成功或失败的更改的非常基本的示例。如果文件无法创建,则不应更新注册表值。现在对于这个例子,答案很简单:我们先写入文件,然后仅在之后更新注册表。这是万无一失的。
或者不是呢?当然,我们可以先写入文件。但如果我们覆盖它呢?那么我们必须先创建一个临时副本,执行写入操作,在发生错误时恢复文件,并验证恢复是否成功。对于一个简单的操作来说,这已经是一大堆额外的代码了。如果比一个文件操作和一个注册表项更新稍微复杂一点,代码就会迅速变得复杂得多。假设文件由两个不同的操作更新,而第二个操作失败了怎么办?或者假设有一个文件需要添加内容,而另一个文件需要被覆盖?
我与 John Robbins(著有《调试应用程序》)持有相同的观点:优秀的程序员需要时刻审视他们的设计,问自己“如果……”,然后给出答案。如果你对代码进行过度的“如果……”,那么很可能一半的实际代码都是用来处理“如果……”。在我提到的例子中:如果有多个独立的文件或注册表项,并且您需要为每种可能的错误场景手动编程回滚,以确保系统始终处于已知状态,那么这可能是一项非常复杂的任务。
事务
考虑在前面概述的场景中,以类似数据库事务的方式执行文件和注册表更新的可能性。编程更改、制作临时备份、在出现问题时恢复这些备份、删除之前过时的备份、验证系统状态未被修改……这些都可能很复杂且容易出错。
然而,有了事务,我们实际上无需关心这一切。
我们打开一个事务句柄,并通过该事务执行所有 IO 操作。在所有更改完成后,我们查看我们执行的操作的错误状态,然后提交事务或回滚。如果我们提交,所有更改将同时生效。如果我们检测到错误并指示事务回滚,那么无论发生什么,都好像什么都没发生过一样。
即使系统在进行到一半时断电或崩溃,也不会出现配置信息处于未知状态的情况。
Win32 事务
自 Windows Vista 以来,Windows 就能够以事务方式执行操作,从而可以将文件、注册表项、命名管道……作为事务的一部分进行更新,该事务可以作为单个操作进行提交或回滚,要么所有操作都生效,要么都不生效。微软实现事务的方式基本上是添加了两组功能:用于管理事务对象本身的功能,以及将您的操作连接到这些事务的功能。
前者是最简单的。其中,我们将更详细地讨论 CreateTransaction
、CommitTransaction
和 RollbackTransaction
。请注意,还有更大的一套管理例程。这些超出了本文的范围。您可以在此处阅读更多相关信息。
后者并不难使用,但它们有些丑陋,而且可能有点微妙。我将详细介绍其中两个:CreateFileTransacted
和 RegCreateKeyTransacted
。微软已经为大量的操作实现了事务支持,他们基本上只是将他们想要添加支持的每个函数,在其名称后加上 Transacted
,并更新参数列表来创建一个新函数。
为求完整性,我必须指出,可以实现自定义事务管理器和资源管理器。如果您的系统设计中实现了某种对象管理,那么就可以使其具有事务意识,从而与 Win32 事务生态系统的其余部分协同工作。这也远远超出了本文的范围。您可以在此链接阅读更多相关信息。
可悲的是,有一件令人遗憾的事情我必须提及。由于该技术并未得到广泛采用,微软正在文档中发布通知,鼓励用户寻找 NTFS 事务的其他解决方案,并警告 NTFS 事务将来可能会被移除。
我希望不会走到那一步,因为在我看来,NTFS 事务是一项非凡的技术,本应得到更好的推广。注册表和其他事务目前没有收到此警告。仅仅这一点就值得花时间去了解,因为能够以事务方式处理复杂的注册表更新是一件很棒的事情。
创建事务
这可能是过程中最简单的一部分。您可以简单地创建一个事务句柄,仅此而已。微软的文档如下:
HANDLE CreateTransaction(
[in, optional] LPSECURITY_ATTRIBUTES lpTransactionAttributes,
[in, optional] LPGUID UOW,
[in, optional] DWORD CreateOptions,
[in, optional] DWORD IsolationLevel,
[in, optional] DWORD IsolationFlags,
[in, optional] DWORD Timeout,
[in, optional] LPWSTR Description);
UOW
、IsolationLevel
和 IsolationFlags
是保留参数,所以我们忽略它们。lpTransactionAttributes
是为事务分配特定安全描述符的方法,如果您是像我们这样的第三方开发者,在一个进程中创建和使用事务,并且 ACL 来自主用户或模拟用户,那么您就不需要这个。唯一可能有用的是 Description 参数。
由于在大多数情况下我们不需要这些参数,所以我创建了一个包装器。可以重用相同的函数名并创建重载函数,但我更喜欢这样做,以使其显而易见,这不是一个官方函数,并向其他开发者清楚地表明,这是一个作用域更有限的简化函数。
//
// This function creates a transaction with default settings, which is what
// is appropriate in most cases.
//
HANDLE CreateTransactionSimple(LPWSTR Description){
return CreateTransaction(
NULL, //Using default security.
NULL, //Reserved
0, //Create options, only relevant for inheriting handles
0, //Reserved
0, //Reserved
0, //Timeout
Description); //User readable description
}
CommitTransaction
和 RollbackTransaction
不需要额外的解释,因为它们只需要事务句柄作为输入,然后分别提交或回滚事务。
CreateFileTransacted
CreateFileTransacted
函数名会扩展为 ASCII 或 Unicode 版本。我展示的是 Unicode 版本。这些参数中的大多数与非事务版本相同。我在这里不做描述。只有最后三个参数值得关注。
HANDLE CreateFileTransactedW(
[in] LPCWSTR lpFileName,
[in] DWORD dwDesiredAccess,
[in] DWORD dwShareMode,
[in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
[in] DWORD dwCreationDisposition,
[in] DWORD dwFlagsAndAttributes,
[in, optional] HANDLE hTemplateFile,
[in] HANDLE hTransaction,
[in, optional] PUSHORT pusMiniVersion,
PVOID lpExtendedParameter);
lpExtendedParameter
是保留的,所以我们只传入 NULL。hTransaction
是我们正在使用的事务。如果文件句柄被创建,它就会链接到该事务。后续的文件操作都可以使用正常的 API 进行,这些 API 在区分事务性文件或其他类型的文件时没有区别。
pusMiniversion
除非在非常特殊的情况下,否则不会被使用。该参数的作用是,如果您从多个位置打开文件,那么在事务进行期间,谁能看到哪个版本的文件。默认情况下,修改文件的事务可以看到文件的脏视图,而其他客户端则看到文件上次提交时的视图。
我也为此函数创建了一个简化包装器。
//
// This function acts as a simplified wrapper for creating a
// transacted file handle to hide an obnoxiously long argument list
// that has several reserved parameters, and a couple that are
// fine with default values for our use.
//
HANDLE CreateFileTransactedSimple(
const LPCTSTR& filepath, //the file we want to create or open
DWORD desiredAccess, //the type of requested access
DWORD createDisposition, //optional specifier to determine
//if we want to open, or create, or always create, ...
const HANDLE& transaction) //the transaction under which this filehandle is covered.
{
return CreateFileTransacted(
filepath,
desiredAccess,
FILE_SHARE_READ, //We allow others to open the file for read access.
//For newly created files, this is pointless
//because no one will see the file
//until we commit. But for previously created files,
//other clients see the last committed file
//while we are still updating it.
NULL, //File security will be default
createDisposition,
FILE_ATTRIBUTE_NORMAL, //the file is a regular file
NULL, //no template file is used
transaction,
NULL, //No need for a special miniversion of the file
NULL); //reserved
}
除了保留或未使用的参数外,包装器还指定文件是常规文件,没有特殊属性(压缩、加密……)。它还指定当我们在使用它时,其他人可以打开该文件进行读取。对于新创建的文件,这一点无关紧要,因为它们甚至看不到该文件。但如果我们打开一个现有文件,其他人仍然可以看到以前的视图,直到我们提交。
RegCreateKeyTransacted
此函数创建一个注册表项句柄,可用于事务下的注册表操作。其文档如下:
LSTATUS RegCreateKeyTransactedW(
[in] HKEY hKey,
[in] LPCWSTR lpSubKey,
DWORD Reserved,
[in, optional] LPWSTR lpClass,
[in] DWORD dwOptions,
[in] REGSAM samDesired,
[in, optional] const LPSECURITY_ATTRIBUTES lpSecurityAttributes,
[out] PHKEY phkResult,
[out, optional] LPDWORD lpdwDisposition,
[in] HANDLE hTransaction,
PVOID pExtendedParemeter
);
与前一个函数一样,我不会介绍在常规函数调用中也存在的参数。只有最后两个参数值得关注,它们在概念上与前一个函数相同。一旦注册表项句柄被创建,该句柄就会链接到事务,您可以使用正常的 API 来使用它。
对于此函数,我也编写了一个包装器。
//
// Simplified wrapper for creating a registry key under a transaction to hide an
// obnoxiously long argument list that has several reserved parameters,
// and a couple that are
// fine with default values for our use.
//
LSTATUS RegCreateKeyTransactedSimple(
HKEY parentKey, //location where we want to open a new key
const LPCTSTR& regkey, //keyname
REGSAM samDesired, //requested rights
HKEY& regkeyhandle, //resulting registry key of the child.
const HANDLE& transaction) //transaction under which the key is opened.
{
return RegCreateKeyTransacted(
parentKey,
regkey,
0, //reserved
NULL, //user class. can be ignored
REG_OPTION_NON_VOLATILE,//the change is to be permanent
samDesired,
NULL, //security attributes. NULL -> default security inherited
®keyhandle,
NULL, //disposition feedback ->> was it created or opened?
//don't care.
transaction,
NULL); //reserved
}
为我们的场景创建应用程序
在排除所有这些之后,我们可以将所有内容整合起来。
整体结构
客户端的结构非常简单易懂。创建事务后,我们进行所有相关的配置。最后,我们提交或回滚。再简单不过了,而且代码量肯定比大量手动创建的数据备份和恢复代码少得多,而且也可靠得多。
//Create the transaction covering the actions in this example
HANDLE transaction = CreateTransactionSimple();
if (transaction == INVALID_HANDLE_VALUE)
{
cout << "Failed to create Win32 Transaction\n";
return GetLastError();
}
// … Do stuff here
//Commit or rollback the transaction depending on whether the changes were
//successful and not cancelled by the user
if (error == NO_ERROR){
cout << "Committing transaction\n";
CommitTransaction(transaction);
}
else{
cout << "Rolling back the transaction\n";
RollbackTransaction(transaction);
}
CloseHandle(transaction);
}
获取用户输入
我们程序的逻辑是,从用户那里获取一个新的文件名,该文件名充当我们的模拟设置文件。
请注意,我们特意在此处不做任何输入验证。用户可以提供带有非法字符的文件名。这将触发一个错误,并可用于演示事务在处理错误方面的有效性。
由于应用程序的其余部分支持 TCHAR
,而标准库不支持此概念,因此我显式实现了这两个选项。
#ifdef _UNICODE
cout << "Enter the name of the file to be created:\n";
wstring filename;
getline(wcin, filename);
cout << "Enter the root folder or press enter for c:\\temp\\:\n";
wstring rootfolder;
getline(wcin, rootfolder);
if (rootfolder.empty())
rootfolder = wstring(TEXT("C:\\TEMP\\"));
wstring filepath = rootfolder + filename;
#else
cout << "Enter the name of the file to be created:\n";
string filename;
getline(cin, filename);
cout << "Enter the root folder or press enter for c:\\temp\\:\n";
string rootfolder;
getline(cin, rootfolder);
if (rootfolder.empty())
rootfolder = string(TEXT("C:\\TEMP\\"));
string filepath = rootfolder + filename;
#endif
写入注册表
在这里,我们在使用我们之前创建的事务打开注册表项后更新注册表。您会注意到,更新注册表的函数与您习惯使用的函数相同。尽管在此处我包装了 SetRegValueEx
函数,以隐藏将字符串写入注册表所需的丑陋类型转换。
//Location in the registry where the path to the newly create file is to be stored
HKEY registryRoot = HKEY_CURRENT_USER;
LPCTSTR regkey = TEXT("Win32Transaction");
LPCTSTR valueName = TEXT("ConfigFile");
DWORD error = NO_ERROR;
//Open or create a registry key which is connected to the transaction
HKEY regkeyhandle = NULL;
if (ERROR_SUCCESS != RegCreateKeyTransactedSimple(
registryRoot, regkey, KEY_READ | KEY_WRITE, regkeyhandle, transaction))
error = GetLastError();
//write the path of the file to the registry.
//At this point no error checks have been
//performed so there is still the possibility
//that the 2nd half of this example will
//trigger an error.
if (error == NO_ERROR) {
if (ERROR_SUCCESS != SetRegValueExTString
(regkeyhandle, valueName, filepath.c_str()))
error = GetLastError();
else
if (ERROR_SUCCESS != RegCloseKey(regkeyhandle))
error = GetLastError();
}
写入文件
与注册表访问一样,在创建文件句柄后,我们可以使用正常的文件 IO 函数来执行文件 IO。
//Create a file of which the path is based on the user input.
//No input validation was done so this part can trigger an error
if (error == NO_ERROR) {
HANDLE fileHandle = CreateFileTransactedSimple(
filepath.c_str(), GENERIC_READ | GENERIC_WRITE, CREATE_ALWAYS, transaction);
if (fileHandle == INVALID_HANDLE_VALUE)
error = GetLastError();
else
{
//so far so good, put something in the file and close it.
if (WriteFileTString(fileHandle, TEXT("Hello transacted world!")))
{
if (!CloseHandle(fileHandle))
error = GetLastError();
}
else
error = GetLastError();
}
}
可选地选择回滚
为了我们的目的,我们让用户选择提交或回滚更改。我们这样做是为了给他们时间手动检查注册表或磁盘上的文件夹,并验证更改尚未生效。
在现实世界中,您可能不会这样做,尽管您可以提供一个更改的概述,让用户在激活所有更改之前进行审阅。
if (error == NO_ERROR)
{
char choice;
do {
cout << "Changes have been made.\n";
cout << "Enter C to commit or R to rollback.\n";
cin >> choice;
if(__isascii(choice) && islower(choice))
choice = _toupper(choice);
} while (choice != 'C' && choice != 'R');
if(choice == 'R')
error = ERROR_CANCELLED;
}
else {
cout << "An error was detected during the changes.\n";
}
运行应用程序
本文随附了此应用程序的源代码,以及一个可在您的系统上运行的已构建版本。我将其与静态运行时库链接,因此您无需安装特定版本的运行时库即可运行它。
注册表设置存储在 HKEY_CURRENT_USER
下,这永远不会成为安全问题。文件存储在 *C:\temp* 中,除非您提供替代选项。
结论
如您所见,Win32 事务是一项极其强大的功能,值得比目前获得的更多关注。我鼓励大家更详细地研究它们,并在适当的时候使用它们。事务不仅可以为您节省大量手动编码的工作,还可以提高您程序的可靠性。
历史
- 2022 年 7 月 21 日:初版