第11章:文件和设备 I/O
I/O 系统
I/O 系统(I/O System)抽象了对物理设备和逻辑设备的访问。访问文件系统中的文件与访问串口、USB 摄像头或打印机是不同的操作,但它们共享同一套基础架构。I/O 系统的组件分布在用户态和内核态两个层面。
用户态进程通过各种 Windows API 调用 I/O 系统。在内核层面,I/O 管理器(I/O Manager)负责处理所有文件和设备操作。读取或写入等请求会被转换为一个内核结构,称为 I/O 请求包(IRP, I/O Request Packet)。IRP 中填充了请求的详细信息后,被传递给相应的设备驱动程序。对于实际文件,请求会到达文件系统驱动程序,如 NTFS。
可以把整个流程类比为邮寄系统:用户态程序写好信件(发出 I/O 请求),I/O 管理器作为邮政总局进行分拣(填充 IRP),设备驱动作为邮递员将信件投递到最终目的地(硬件/文件系统)。
从内核的角度来看,所有 I/O 本质上都是异步的——驱动程序应尽快启动操作并立即返回。然而,原始调用者可以选择进行同步调用,此时 I/O 管理器会代替调用者等待操作完成。
CreateFile 函数
CreateFile 是所有 I/O 操作的入口点。这里的 "File" 实际上指的是 文件对象(File Object),它是内核中与设备连接的一种抽象表示。换言之,CreateFile 不仅能打开文件,还能打开几乎任何设备。
CreateFile 原型
HANDLE CreateFile(
_In_ LPCTSTR lpFileName,
_In_ DWORD dwDesiredAccess,
_In_ DWORD dwShareMode,
_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
_In_ DWORD dwCreationDisposition,
_In_ DWORD dwFlagsAndAttributes,
_In_opt_ HANDLE hTemplateFile
);CreateFile2(Windows 8 / Server 2012)
typedef struct _CREATEFILE2_EXTENDED_PARAMETERS {
DWORD dwSize;
DWORD dwFileAttributes;
DWORD dwFileFlags;
DWORD dwSecurityQosFlags;
LPSECURITY_ATTRIBUTES lpSecurityAttributes;
HANDLE hTemplateFile;
} CREATEFILE2_EXTENDED_PARAMETERS, *PCREATEFILE2_EXTENDED_PARAMETERS;
HANDLE CreateFile2(
_In_ LPCWSTR lpFileName,
_In_ DWORD dwDesiredAccess,
_In_ DWORD dwShareMode,
_In_ DWORD dwCreationDisposition,
_In_opt_ PCREATEFILE2_EXTENDED_PARAMETERS pCreateExParams
);CreateFile2 在 UWP 和桌面应用中均可使用,而 CreateFile 不能从 UWP 调用。CreateFile2 仅支持 Unicode,并且额外支持 FILE_FLAG_OPEN_REQUIRING_OPLOCK 标志。
参数详解
lpFileName(文件名) 指定文件或设备名称,它指向对象管理器命名空间中的符号链接,并遵循特定的解析规则。常见的文件名格式如下表:
| 格式 | 示例 | 描述 |
|---|---|---|
| x:\dir1\dir2\file | c:\mydir\myfile.txt | 完整文件系统路径 |
| ..\dir1\file | ..\mydir\myfile.txt | 相对路径 |
| dir1\dir2\file | mydir1\mydir2\myfile.txt | 相对于当前目录 |
| file | myfile.txt | 当前目录中的文件 |
| \server\share\dir1\dir2\file | \myserver\myshare\mydir\myfile.txt | 网络共享文件 |
| \server\pipe\pipename | \myserver\pipe\mypipe | 命名管道客户端 |
| \server\mailslot\mailslotname | \myserver\mailslot\mymailslot | 邮槽客户端 |
| \\.*devicename* | \\.\kobjexp | 设备符号链接 |
| builtin | com1 | 旧 DOS 名称(作为符号链接处理) |
"符号链接"是最基本的名称解析机制。甚至可以像 "C:" 这样的盘符也是一个符号链接。可以用 Sysinternals 的 WinObj 工具或作者的 Object Explorer 来查看这些符号链接。
每个列出的名称都是可以通过 "\\.\" 前缀使用 CreateFile 打开的符号链接。有些不需要此前缀,例如 "C:" 实际指向类似 "\Device\HarddiskVolume3" 的目标。
dwDesiredAccess(期望的访问权限) 指定访问掩码。常用值包括 GENERIC_READ、GENERIC_WRITE 或二者组合。也可以指定为 0,此时仅能获取基本信息(如时间戳或文件大小)。还存在更细粒度的掩码,例如 FILE_READ_DATA。GENERIC_READ 映射到 FILE_READ_DATA 和 FILE_READ_ATTRIBUTES 等具体掩码。SYNCHRONIZE 和 FILE_READ_ATTRIBUTES 总是被隐式请求。
dwShareMode(共享模式) 控制文件/设备如何共享:
| 模式 | 描述 |
|---|---|
| 0 | 独占访问,不允许任何共享 |
| FILE_SHARE_READ | 允许后续以 GENERIC_READ 打开 |
| FILE_SHARE_WRITE | 允许后续以 GENERIC_WRITE 打开 |
| FILE_SHARE_DELETE | 允许后续以 DELETE 访问权限打开 |
| 组合值 | 各种组合的叠加效果 |
lpSecurityAttributes(安全属性) 是标准的 SECURITY_ATTRIBUTES 结构。
dwCreationDisposition(创建行为) 决定文件存在或不存在的处理方式:
| 值 | 文件存在时 | 文件不存在时 |
|---|---|---|
| CREATE_NEW (1) | 失败 | 创建新文件 |
| CREATE_ALWAYS (2) | 覆盖 | 创建新文件 |
| OPEN_EXISTING (3) | 打开 | 失败 |
| OPEN_ALWAYS (4) | 打开(不覆盖) | 创建文件 |
| TRUNCATE_EXISTING (5) | 打开并截断为零 | 失败 |
对于非文件系统设备,此参数应始终使用 OPEN_EXISTING。
dwFlagsAndAttributes(标志和属性) 包含三类可组合的值:
第一类:影响创建后操作的标志:
| 标志 | 描述 |
|---|---|
| WRITE_THROUGH | 强制写入刷新到磁盘 |
| NO_BUFFERING | 直接磁盘写入(无缓存),绕过文件系统缓存 |
| SEQUENTIAL_SCAN | 提示顺序访问以优化性能 |
| RANDOM_ACCESS | 提示随机访问,优化缓存策略 |
| DELETE_ON_CLOSE | 最后一个句柄关闭时删除文件 |
| OVERLAPPED | 以异步方式打开文件 |
| BACKUP_SEMANTICS | 打开目录句柄时必须指定 |
| POSIX_SEMANTICS | 大小写敏感的文件名查找 |
| OPEN_REPARSE_POINT | 忽略重解析点处理,直接打开重解析点本身 |
| OPEN_NO_RECALL | 提示远程文件不会被本地读取 |
| SESSION_AWARE(Win8+) | 会话感知的设备访问 |
第二类:新创建文件的文件属性:
| 属性 | 描述 |
|---|---|
| NORMAL | 普通文件,无特殊属性 |
| HIDDEN | 隐藏文件 |
| ARCHIVE | 归档标记 |
| ENCRYPTED | 加密内容 |
| READONLY | 只读文件 |
| SYSTEM | 系统文件 |
| OFFLINE | 数据存储在其他地方 |
| TEMPORARY | 临时存储提示,减少物理写入 |
| NOT_CONTENT_INDEXED | 不被内容索引服务索引 |
第三类:缓存标志 需要特别小心使用。对于 FILE_FLAG_NO_BUFFERING:
- 读写大小必须是卷扇区大小的倍数
- 缓冲区必须在物理扇区大小边界上对齐
- 应使用
VirtualAlloc或_aligned_malloc分配此类缓冲区
FlushFileBuffers 将缓冲区数据强制写入磁盘:
BOOL FlushFileBuffers(_In_ HANDLE hFile);hTemplateFile(模板文件句柄) 是一个可选句柄,新创建文件从此句柄复制属性,要求具有 GENERIC_READ 访问权限。对于已存在的文件此参数被忽略。
返回值 成功时返回文件/设备句柄,失败时返回 INVALID_HANDLE_VALUE。使用 GetLastError 获取详细错误信息。
使用符号链接(Symbolic Links)
符号链接(Symbolic Link)是 Windows 对象管理器命名空间中的基本名称解析机制。通俗地理解,符号链接就像文件系统中的快捷方式---它本身不是一个实际对象,而是指向另一个对象的"指针"。
查询符号链接:QueryDosDevice
DWORD QueryDosDevice(
_In_opt_ LPCTSTR lpDeviceName,
_Out_ LPTSTR lpTargetPath,
_In_ DWORD ucchMax
);该函数有两种使用模式:
- 如果
lpDeviceName非 NULL,查询指定符号链接并返回其目标 - 如果
lpDeviceName为 NULL,返回所有符号链接,以 '\0' 分隔,末尾额外加一个 '\0' 表示结束 - 返回值是写入目标缓冲区的字符数,失败时返回 0
以下 symlinks 应用程序演示了如何枚举系统中的符号链接。它首先分配足够大的缓冲区:
#include <memory>
#include <string>
#include <set>
using namespace std;
int wmain(int argc, wchar_t* argv[]) {
auto size = 1 << 14;
unique_ptr<WCHAR[]> buffer;
for ( ; ; ) {
buffer = make_unique<WCHAR[]>(size);
if (0 == ::QueryDosDevice(nullptr, buffer.get(), size)) {
if (::GetLastError() == ERROR_INSUFFICIENT_BUFFER) {
size *= 2;
continue;
}
else {
printf("Error: %d\n", ::GetLastError());
return 1;
}
}
else
break;
}循环中使用 make_unique 分配缓冲区,以 NULL 参数调用 QueryDosDevice,如果缓冲区不足则翻倍重试。成功后迭代结果,使用带大小写不敏感比较器的 std::set 进行过滤和排序:
if (argc > 1) {
::_wcslwr_s(argv[1], ::wcslen(argv[1]) + 1);
}
auto filter = argc > 1 ? argv[1] : nullptr;
using LinkPair = pair<wstring, wstring>;
struct LessNoCase {
bool operator()(const LinkPair& p1, const LinkPair& p2) const {
return ::_wcsicmp(p1.first.c_str(), p2.first.c_str()) < 0;
}
};
set<LinkPair, LessNoCase> links;
WCHAR target[512];
for (auto p = buffer.get(); *p; ) {
wstring name(p);
auto locase(name);
::_wcslwr_s((wchar_t*)locase.data(), locase.size() + 1);
if (filter == nullptr || locase.find(filter) != wstring::npos) {
::QueryDosDevice(name.c_str(), target, _countof(target));
links.insert({ name, target });
}
p += name.size() + 1;
}
for (auto& link : links) {
printf("%ws = %ws\n", link.first.c_str(), link.second.c_str());
}
}示例输出:
C:\>symlinks.exe c:
C: = \Device\HarddiskVolume3
c:\> symlinks.exe pipe
PIPE = \Device\NamedPipe
c:\>symlinks.exe nul
NUL = \Device\Null
c:\>symlinks con
CimfsControl = \Device\cimfs\control
CON = \Device\ConDrv\Console
CONIN$ = \Device\ConDrv\CurrentIn
CONOUT$ = \Device\ConDrv\CurrentOut
...
PartmgrControl = \Device\PartmgrControl
PciControl = \Device\PciControl
...
UVMLiteController = \Device\UVMLiteController0x1
VolMgrControl = \Device\VolMgrControl创建符号链接:DefineDosDevice
BOOL DefineDosDevice(
_In_ DWORD dwFlags,
_In_ LPCTSTR lpDeviceName,
_In_opt_ LPCTSTR lpTargetPath
);例如,将逻辑驱动器映射到现有目录(相当于 subst S: C:\Windows\System32):
::DefineDosDevice(0, L"s:", L"c:\\Windows\\System32");新创建的符号链接会出现在当前登录会话中,而非全局 ?? 目录,除非调用方以 LocalSystem 身份运行。
DefineDosDevice 的标志:
| 标志 | 描述 |
|---|---|
| DDD_NO_BROADCAST_SYSTEM | 不发送 WM_SETTINGSCHANGE 广播 |
| DDD_RAW_TARGET_PATH | 目标路径为原生 NT 路径,非 Win32 路径 |
| DDD_REMOVE_DEFINITION | 移除符号链接映射 |
| DDD_EXACT_MATCH_ON_REMOVE | 移除时精确匹配目标 |
路径长度
传统的 MAX_PATH 限制为 260 个字符。使用 CreateFileW 配合 "\\?\" 前缀(例如 "\\?\c:\MyDir\MyFile.txt")可将路径长度扩展到约 32,767 个字符,其中每个组件限制为 255 个字符。
对于 UNC 路径,使用 "\\?\UNC\" 前缀。在 C/C++ 中反斜杠需要转义。C++11 原始字符串字面量可以简化书写:R"(c:\temp\file.txt)" 或 LR"(c:\temp\file.txt)"。
Windows 10 版本 1607 和 Server 2016 添加了可选的长路径支持,需要同时满足两个条件:
- 注册表键
HKLM\System\CurrentControlSet\Control\FileSystem下的LongPathsEnabled(DWORD)设置为 1。该设置按进程缓存。 - 可执行文件必须在清单中包含
longPathAware设置为true:
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
<ws2:longPathAware>true</ws2:longPathAware>
</windowsSettings>
</application>目录
CreateFile 在指定 FILE_FLAG_BACKUP_SEMANTICS 时可以打开目录句柄。以下函数专门用于创建目录:
BOOL CreateDirectory(
_In_ LPCTSTR lpPathName,
_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes);
BOOL CreateDirectoryEx(
_In_ LPCTSTR lpTemplateDirectory,
_In_ LPCTSTR lpNewDirectory,
_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes);调用这两个函数之前,所有父目录必须存在。CreateDirectoryEx 可以从现有模板目录复制一些属性到新目录。
文件
获取文件大小
DWORD GetFileSize(
_In_ HANDLE hFile,
_Out_opt_ LPDWORD lpFileSizeHigh);
BOOL GetFileSizeEx(
_In_ HANDLE hFile,
_Out_ PLARGE_INTEGER lpFileSize);文件大小是 64 位的。GetFileSize 返回低 32 位作为返回值,高 32 位通过 lpFileSizeHigh 输出。出错时返回 INVALID_FILE_SIZE(0xffffffff)。GetFileSizeEx 更简洁,直接使用 LARGE_INTEGER。
逻辑文件大小可能与物理大小不同(压缩文件或稀疏文件)。使用 GetCompressedFileSize 获取压缩后的大小:
DWORD GetCompressedFileSize(
_In_ LPCTSTR lpFileName,
_Out_opt_ LPDWORD lpFileSizeHigh);文件时间
文件有三种时间戳:创建时间、修改时间和访问时间:
BOOL GetFileTime(
_In_ HANDLE hFile,
_Out_opt_ LPFILETIME lpCreationTime,
_Out_opt_ LPFILETIME lpLastAccessTime,
_Out_opt_ LPFILETIME lpLastWriteTime);时间以 FILETIME 格式返回,是自 1601 年 1 月 1 日以来的 100 纳秒间隔数。
文件属性
DWORD GetFileAttributes(_In_ LPCTSTR lpFileName);
BOOL GetFileAttributesEx(
_In_ LPCTSTR lpFileName,
_In_ GET_FILEEX_INFO_LEVELS fInfoLevelId,
_Out_ LPVOID lpFileInformation);GetFileAttributesEx 仅接受 GetFileExInfoStandard 信息级别,返回 WIN32_FILE_ATTRIBUTE_DATA 结构:
typedef struct _WIN32_FILE_ATTRIBUTE_DATA {
DWORD dwFileAttributes;
FILETIME ftCreationTime;
FILETIME ftLastAccessTime;
FILETIME ftLastWriteTime;
DWORD nFileSizeHigh;
DWORD nFileSizeLow;
} WIN32_FILE_ATTRIBUTE_DATA, *LPWIN32_FILE_ATTRIBUTE_DATA;额外可能出现的属性:
| 属性 | 描述 |
|---|---|
| DIRECTORY | 是目录 |
| REPARSE_POINT | 具有关联的重解析点 |
| COMPRESSED | 文件已压缩 |
| SPARSE_FILE | 文件是稀疏文件 |
| INTEGRITY_STREAM(Win8+) | 已配置完整性流(仅 ReFS) |
| NO_SCRUB_DATA(Win8+) | 不被完整性扫描器扫描 |
GetFileInformationByHandle 提供更多信息(它是 GetFileAttributes 的超集):
BOOL GetFileInformationByHandle(
_In_ HANDLE hFile,
_Out_ LPBY_HANDLE_FILE_INFORMATION lpFileInformation);
typedef struct _BY_HANDLE_FILE_INFORMATION {
DWORD dwFileAttributes;
FILETIME ftCreationTime;
FILETIME ftLastAccessTime;
FILETIME ftLastWriteTime;
DWORD dwVolumeSerialNumber;
DWORD nFileSizeHigh;
DWORD nFileSizeLow;
DWORD nNumberOfLinks;
DWORD nFileIndexHigh;
DWORD nFileIndexLow;
} BY_HANDLE_FILE_INFORMATION, *PBY_HANDLE_FILE_INFORMATION;该结构额外返回卷序列号、硬链接数量和文件索引(每个卷内唯一)。由于使用已打开的句柄,GetFileInformationByHandle 比 GetFileAttributes 或 GetFileAttributesEx 更快。
GetFileInformationByHandleEx 通过 FILE_INFO_BY_HANDLE_CLASS 枚举检索更多信息:
BOOL GetFileInformationByHandleEx(
_In_ HANDLE hFile,
_In_ FILE_INFO_BY_HANDLE_CLASS FileInformationClass,
_Out_ LPVOID lpFileInformation,
_In_ DWORD dwBufferSize
);
typedef enum _FILE_INFO_BY_HANDLE_CLASS {
FileBasicInfo,
FileStandardInfo,
FileNameInfo,
FileRenameInfo,
FileDispositionInfo,
FileAllocationInfo,
FileEndOfFileInfo,
FileStreamInfo,
FileCompressionInfo,
FileAttributeTagInfo,
FileIdBothDirectoryInfo,
FileIdBothDirectoryRestartInfo,
FileIoPriorityHintInfo,
FileRemoteProtocolInfo,
FileFullDirectoryInfo,
FileFullDirectoryRestartInfo,
#if (_WIN32_WINNT >= _WIN32_WINNT_WIN8)
FileStorageInfo,
FileAlignmentInfo,
FileIdInfo,
FileIdExtdDirectoryInfo,
FileIdExtdDirectoryRestartInfo,
#endif
#if (_WIN32_WINNT >= _WIN32_WINNT_WIN10_RS1)
FileDispositionInfoEx, FileRenameInfoEx,
#endif
#if (NTDDI_VERSION >= NTDDI_WIN10_19H1)
FileCaseSensitiveInfo, FileNormalizedNameInfo,
#endif
MaximumFileInfoByHandleClass
} FILE_INFO_BY_HANDLE_CLASS, *PFILE_INFO_BY_HANDLE_CLASS;设置文件信息
SetFileAttributes
BOOL SetFileAttributes(
_In_ LPCTSTR lpFileName,
_In_ DWORD dwFileAttributes);可以通过此函数设置的属性包括:ARCHIVE、HIDDEN、NORMAL、NOT_CONTENT_INDEXED、OFFLINE、READONLY、SYSTEM、TEMPORARY。
其他属性需要通过不同的 API 设置:
- COMPRESSED --- 通过
DeviceIoControl配合FSCTL_SET_COMPRESSION设置 - ENCRYPTED --- 通过
EncryptFile函数设置 - REPARSE_POINT --- 通过
DeviceIoControl配合FSCTL_SET_REPARSE_POINT设置 - SPARSE_FILE --- 通过
DeviceIoControl配合FSCTL_SET_SPARSE设置
SetFileTime
BOOL SetFileTime(
_In_ HANDLE hFile,
_In_opt_ CONST FILETIME* lpCreationTime,
_In_opt_ CONST FILETIME* lpLastAccessTime,
_In_opt_ CONST FILETIME* lpLastWriteTime);需要 FILE_WRITE_ATTRIBUTES 访问权限。对于任何不想修改的时间,传入 NULL 表示"不更改"。
SetFileInformationByHandle
BOOL SetFileInformationByHandle(
_In_ HANDLE hFile,
_In_ FILE_INFO_BY_HANDLE_CLASS FileInformationClass,
_In_reads_bytes_(dwBufferSize) LPVOID lpFileInformation,
_In_ DWORD dwBufferSize);可用于设置的有效信息类:FileBasicInfo、FileRenameInfo、FileDispositionInfo、FileAllocationInfo、FileEndOfFileInfo、FileIoPriorityHintInfo。
同步 I/O
当 CreateFile 不使用 FILE_FLAG_OVERLAPPED 标志时,创建的文件对象仅支持 同步 I/O(Synchronous I/O)。此时,每次 ReadFile 或 WriteFile 调用会阻塞直到操作完成。
ReadFile 和 WriteFile 适用于任何文件对象:
BOOL ReadFile(
_In_ HANDLE hFile,
_Out_ LPVOID lpBuffer,
_In_ DWORD nNumberOfBytesToRead,
_Out_opt_ LPDWORD lpNumberOfBytesRead,
_Inout_opt_ LPOVERLAPPED lpOverlapped);
BOOL WriteFile(
_In_ HANDLE hFile,
_In_ LPCVOID lpBuffer,
_In_ DWORD nNumberOfBytesToWrite,
_Out_opt_ LPDWORD lpNumberOfBytesWritten,
_Inout_opt_ LPOVERLAPPED lpOverlapped);实际读写的字节数可能少于请求的字节数。对于同步 I/O,lpOverlapped 应为 NULL,而 lpNumberOfBytesRead/lpNumberOfBytesWritten 必须为非 NULL。
向新文件写入数据:
HANDLE hFile = ::CreateFile(LR"(c:\temp\mydata.txt)",
GENERIC_WRITE, 0, nullptr, CREATE_NEW, 0, nullptr);
if (hFile != INVALID_HANDLE_VALUE) {
char text[] = "Hello from Windows!";
DWORD bytes;
::WriteFile(hFile, text, ::strlen(text), &bytes, nullptr);
::CloseHandle(hFile);
}从文件中读取所有字节:
HANDLE hFile = ::CreateFile(LR"(c:\temp\mydata.txt)",
GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
if (hFile != INVALID_HANDLE_VALUE) {
DWORD size = ::GetFileSize(hFile, nullptr);
auto buffer = std::make_unique<char[]>(size + 1);
DWORD bytes;
if (::ReadFile(hFile, buffer.get(), size, &bytes, nullptr)) {
buffer[bytes] = '\0';
printf("%s\n", buffer.get());
}
::CloseHandle(hFile);
}每个以同步方式打开的文件对象维护一个内部文件指针,每次 I/O 操作后该指针自动前进。
设置文件指针:SetFilePointer / SetFilePointerEx
用于实现随机访问(跳过文件开头或中间部分,直接读写指定位置):
DWORD SetFilePointer(
_In_ HANDLE hFile,
_In_ LONG lDistanceToMove,
_Inout_opt_ PLONG lpDistanceToMoveHigh,
_In_ DWORD dwMoveMethod);
BOOL SetFilePointerEx(
_In_ HANDLE hFile,
_In_ LARGE_INTEGER liDistanceToMove,
_Out_opt_ PLARGE_INTEGER lpNewFilePointer,
_In_ DWORD dwMoveMethod);移动方式(dwMoveMethod):
FILE_BEGIN(0)--- 从文件开头开始FILE_CURRENT(1)--- 从当前位置开始FILE_END(2)--- 从文件末尾开始
同一个文件的多个文件对象各自维护独立的文件指针。
设置文件结尾:SetEndOfFile
BOOL SetEndOfFile(_In_ HANDLE hFile);用于截断或扩展文件。当文件指针之后的数据将被丢弃(截断),或当文件指针超出当前文件大小时(扩展,新增部分填充为零)。
异步 I/O
Windows I/O 在内核层面本质上是异步的(Asynchronous I/O)。驱动程序将请求发给硬件后标记为"挂起"状态并立即返回调用者。要使用异步 I/O,CreateFile 的 dwFlagsAndAttributes 中必须指定 FILE_FLAG_OVERLAPPED。
与同步 I/O 的一个关键区别是:异步模式下不存在文件指针——每次操作都必须通过 OVERLAPPED 结构显式指定读写偏移量。
OVERLAPPED 结构
typedef struct _OVERLAPPED {
ULONG_PTR Internal;
ULONG_PTR InternalHigh;
union {
struct {
DWORD Offset;
DWORD OffsetHigh;
};
PVOID Pointer;
};
HANDLE hEvent;
} OVERLAPPED, *LPOVERLAPPED;该结构包含三个逻辑部分:
- Internal 和 InternalHigh:由 I/O 管理器使用。
Internal保存错误码(进行中时为STATUS_PENDING),InternalHigh在完成时保存实际传输的字节数 - Offset 和 OffsetHigh:文件起始偏移量
- hEvent:一个内核事件对象,操作完成时被设置为有信号
宏 HasOverlappedIoCompleted 可以检查 Internal 是否不等于 STATUS_PENDING 来判断操作是否完成。
对于异步操作,ReadFile/WriteFile 通常返回 FALSE,GetLastError 返回 ERROR_IO_PENDING。如果它们返回 TRUE,说明操作已同步完成(数据已就绪,无需等待)。
异步读取示例:
HANDLE hFile = ::CreateFile(LR"(c:\temp\mydata.txt)", GENERIC_READ,
FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr);
if (hFile != INVALID_HANDLE_VALUE) {
OVERLAPPED ov = { 0 };
ov.hEvent = ::CreateEvent(nullptr, TRUE, FALSE, nullptr);
BYTE buffer[1 << 12];
BOOL ok = ::ReadFile(hFile, buffer, sizeof(buffer), nullptr, &ov);
if (!ok) {
if (::GetLastError() != ERROR_IO_PENDING) {
return;
}
else {
::WaitForSingleObject(ov.hEvent, INFINITE);
::CloseHandle(ov.hEvent);
}
}
::CloseHandle(hFile);
}关键点:操作期间 OVERLAPPED 实例必须保持有效;任何线程都可以等待该事件。
GetOverlappedResult
用于检索完成后的传输字节数:
BOOL GetOverlappedResult(
_In_ HANDLE hFile,
_In_ LPOVERLAPPED lpOverlapped,
_Out_ LPDWORD lpNumberOfBytesTransferred,
_In_ BOOL bWait);bWait 参数:TRUE 表示等待完成;FALSE 表示操作未完成时立即返回 ERROR_IO_INCOMPLETE。
GetOverlappedResultEx(Windows 8+)增加了超时和可警告等待的支持:
BOOL GetOverlappedResultEx(
_In_ HANDLE hFile,
_In_ LPOVERLAPPED lpOverlapped,
_Out_ LPDWORD lpNumberOfBytesTransferred,
_In_ DWORD dwMilliseconds,
_In_ BOOL bAlertable);异步完成处理机制
| 机制 | 说明 |
|---|---|
| 等待文件句柄 | 简单但限于单个操作 |
| 等待 OVERLAPPED 事件 | 简单;任何线程都可等待 |
| ReadFileEx/WriteFileEx(带回调) | APC 仅排队到调用线程 |
| I/O 完成端口 | 灵活且功能强大 |
SetFileCompletionNotificationModes 可以跳过文件句柄的信号通知(优化性能):
BOOL SetFileCompletionNotificationModes(
_In_ HANDLE FileHandle,
_In_ UCHAR Flags);其中 FILE_SKIP_SET_EVENT_ON_HANDLE 标志使完成操作时不设置文件句柄信号。
ReadFileEx 和 WriteFileEx
这两个函数在执行异步 I/O 时,允许指定一个完成回调函数(Completion Routine),操作完成时该回调作为 APC(异步过程调用)被执行:
BOOL ReadFileEx(
_In_ HANDLE hFile,
_Out_ LPVOID lpBuffer,
_In_ DWORD nNumberOfBytesToRead,
_Inout_ LPOVERLAPPED lpOverlapped,
_In_ LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
BOOL WriteFileEx(
_In_ HANDLE hFile,
_In_ LPCVOID lpBuffer,
_In_ DWORD nNumberOfBytesToWrite,
_Inout_ LPOVERLAPPED lpOverlapped,
_In_ LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);完成回调函数原型
typedef VOID (WINAPI *LPOVERLAPPED_COMPLETION_ROUTINE)(
_In_ DWORD dwErrorCode,
_In_ DWORD dwNumberOfBytesTransfered,
_Inout_ LPOVERLAPPED lpOverlapped);回调作为 APC(Asynchronous Procedure Call,异步过程调用)在原始调用线程上运行。这意味着调用线程必须偶尔进入可警告状态(Alertable State),否则回调永远得不到执行。
手动排队的异步过程调用(APC)
APC(Asynchronous Procedure Call,异步过程调用)是 Windows 提供的一种线程间通知机制。可以将 APC 理解为"给线程下达的任务便条"——你可以在目标线程的待办队列中插入一个函数调用,该线程在下次进入可警告等待时便会执行它。
QueueUserAPC 用于向目标线程手动排队一个 APC:
DWORD QueueUserAPC(
_In_ PAPCFUNC pfnAPC,
_In_ HANDLE hThread,
_In_ ULONG_PTR dwData);回调原型:
typedef VOID (WINAPI *PAPCFUNC)(_In_ ULONG_PTR Parameter);目标线程必须具有 THREAD_SET_CONTEXT 访问权限,并且必须进入可警告状态(调用 SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectsEx 等以 "Ex" 结尾且 bAlertable 设为 TRUE 的函数)。
使用可警告等待的工作线程示例:
DWORD WorkThread(PVOID param) {
HANDLE hEvent = (HANDLE)param;
for ( ; ; ) {
if(::WaitForSingleObjectEx(hEvent, INFINITE, TRUE) == WAIT_OBJECT_0)
break;
}
return 0;
}线程创建和工作排队:
HANDLE hEvent = ::CreateEvent(nullptr, TRUE, FALSE, nullptr);
HANDLE hThread = ::CreateThread(nullptr, 0, WorkThread, (PVOID)hEvent, 0, nullptr);
// 排队工作
::QueueUserAPC(hThread, SomeFunction, SomeData);关闭工作线程:
::SetEvent(hEvent);
::WaitForSingleObject(hThread, INFINITE);I/O 完成端口(I/O Completion Ports)
I/O 完成端口(I/O Completion Port,IOCP)是 Windows 中最强大也最灵活的异步 I/O 完成机制。它可以将多个文件对象(不仅仅是文件,还包括管道、套接字、设备等)与一个完成端口关联,并维护一个完成请求队列和一个线程池来处理这些完成操作。
可以将 I/O 完成端口想象成一个"任务分发中心":多个快递员(工作线程)在分发中心等待,任务(I/O 完成通知)到达后被分发给空闲的快递员处理。
CreateIoCompletionPort
HANDLE CreateIoCompletionPort(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE ExistingCompletionPort,
_In_ ULONG_PTR CompletionKey,
_In_ DWORD NumberOfConcurrentThreads);该函数可以创建新端口、将文件与现有端口关联、或同时执行两者。它是唯一不接受 SECURITY_ATTRIBUTES 的内核对象创建函数。
创建独立的完成端口:
HANDLE hNewCP = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0,
NumberOfConcurrentThreads);将 NumberOfConcurrentThreads 设为 0 会将其设置为逻辑处理器数量。
将文件与端口关联:
const int Key = 1;
HANDLE hFile = ::CreateFile(..., FILE_FLAG_OVERLAPPED, ...);
HANDLE hOldCP = ::CreateIoCompletionPort(hFile, hNewCP, Key, 0);
assert(hOldCP == hNewCP);这里的 "文件" 也可以是管道、套接字或设备。
GetQueuedCompletionStatus
将线程绑定到完成端口并等待完成通知:
BOOL GetQueuedCompletionStatus(
_In_ HANDLE CompletionPort,
_Out_ LPDWORD lpNumberOfBytesTransferred,
_Out_ PULONG_PTR lpCompletionKey,
_Out_ LPOVERLAPPED* lpOverlapped,
_In_ DWORD dwMilliseconds);成功时返回 TRUE,并输出传输字节数、完成键和 OVERLAPPED 指针。超时时返回 FALSE,lpOverlapped 为 NULL,错误码为 WAIT_TIMEOUT。
一次仅允许指定数量的线程同时获取到成功完成。如果处理线程进入等待状态,另一个线程可能获得处理机会。线程在首次调用时绑定到端口,直到线程退出、端口关闭或绑定到另一个端口。
完成包的获取是 LIFO(后进先出)队列——最后执行的线程最先获得下一个包。这减少了上下文切换并改善了 CPU 缓存利用率。
GetQueuedCompletionStatusEx
批量获取多个完成通知:
BOOL GetQueuedCompletionStatusEx(
_In_ HANDLE CompletionPort,
_Out_ LPOVERLAPPED_ENTRY lpCompletionPortEntries,
_In_ ULONG ulCount,
_Out_ PULONG ulNumEntriesRemoved,
_In_ DWORD dwMilliseconds,
_In_ BOOL fAlertable
);OVERLAPPED_ENTRY 结构:
typedef struct _OVERLAPPED_ENTRY {
ULONG_PTR lpCompletionKey;
LPOVERLAPPED lpOverlapped;
ULONG_PTR Internal;
DWORD dwNumberOfBytesTransferred;
} OVERLAPPED_ENTRY, *LPOVERLAPPED_ENTRY;PostQueuedCompletionStatus
手动向完成端口发送一个完成通知(常用于发送退出信号或自定义通知):
BOOL PostQueuedCompletionStatus(
_In_ HANDLE CompletionPort,
_In_ DWORD dwNumberOfBytesTransferred,
_In_ ULONG_PTR dwCompletionKey,
_In_opt_ LPOVERLAPPED lpOverlapped
);批量复制应用程序
BulkCopy 应用程序展示了使用 I/O 完成端口进行异步文件复制。该应用允许用户添加多个文件,设置目标目录,然后批量复制。
复制文件不仅仅是简单地从源文件读取并写入目标文件——还需要复制安全描述符和 NTFS 流等额外信息。以下是核心实现。
添加文件(OnAddFiles)
LRESULT CMainDlg::OnAddFiles(WORD, WORD wID, HWND, BOOL&) {
CMultiFileDialog dlg(nullptr, nullptr,
OFN_FILEMUSTEXIST | OFN_ALLOWMULTISELECT,
L"All files (*.*)\0*.*\0", *this);
dlg.ResizeFilenameBuffer(1 << 16);
if (dlg.DoModal() == IDOK) {
CString path;
int errors = 0;
dlg.GetFirstPathName(path);
do {
wil::unique_handle hFile(::CreateFile(path, 0, FILE_SHARE_READ, nullptr,
OPEN_EXISTING, 0, nullptr));
if (!hFile) { errors++; continue; }
LARGE_INTEGER size;
::GetFileSizeEx(hFile.get(), &size);
int n = m_List.AddItem(m_List.GetItemCount(), 0, path, 0);
m_List.SetItemText(n, 1, FormatSize(size.QuadPart));
m_List.SetItemData(n, (DWORD_PTR)Type::File);
} while (dlg.GetNextPathName(path));
m_List.EnsureVisible(m_List.GetItemCount() - 1, FALSE);
UpdateButtons();
if (errors > 0)
AtlMessageBox(*this, L"Some files could not be opened",
IDR_MAINFRAME, MB_ICONEXCLAMATION);
}
return 0;
}访问掩码为 0,因为 SYNCHRONIZE 和 FILE_READ_ATTRIBUTES 总是被隐式请求。
FileData 结构
struct FileData {
CString Src;
CString Dst;
wil::unique_handle hDst, hSrc;
};该结构存储在对话框类中:std::vector<FileData> m_Data;
OnGo 处理器
构建 FileData 列表但不在此阶段打开文件:
LRESULT CMainDlg::OnGo(WORD, WORD wID, HWND, BOOL&) {
m_Data.clear();
int count = m_List.GetItemCount();
m_Data.reserve(count);
for (int i = 0; i < count; i++) {
if (m_List.GetItemData(i) != (DWORD_PTR)Type::File) {
continue;
}
FileData data;
m_List.GetItemText(i, 0, data.Src);
m_List.GetItemText(i, 2, data.Dst);
m_Data.push_back(std::move(data));
}
// 创建 Worker 线程
auto hThread = ::CreateThread(nullptr, 0, [](auto param) {
return ((CMainDlg*)param)->WorkerThread();
}, this, 0, nullptr);
::CloseHandle(hThread);
m_Progress.SetPos(0);
m_Running = true;
UpdateButtons();
return 0;
}WorkerThread 核心逻辑
DWORD CMainDlg::WorkerThread() {
wil::unique_handle hCP(::CreateIoCompletionPort(
INVALID_HANDLE_VALUE, nullptr, 0, 0));
ATLASSERT(hCP);
if (!hCP) {
PostMessage(WM_ERROR, ::GetLastError());
return 0;
}
const int chunkSize = 1 << 16; // 64 KB遍历所有文件对,打开源文件和目标文件,获取源文件大小,预扩展目标文件到最终大小:
LONGLONG count = 0;
for (auto& data : m_Data) {
wil::unique_handle hSrc(::CreateFile(data.Src, GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr));
if (!hSrc) {
PostMessage(WM_ERROR, ::GetLastError());
continue;
}
LARGE_INTEGER size;
::GetFileSizeEx(hSrc.get(), &size);
CString filename = data.Src.Mid(data.Src.ReverseFind(L'\\'));
wil::unique_handle hDst(::CreateFile(data.Dst + filename, GENERIC_WRITE, 0,
nullptr, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, nullptr));
if (!hDst) {
PostMessage(WM_ERROR, ::GetLastError());
continue;
}
::SetFilePointerEx(hDst.get(), size, nullptr, FILE_BEGIN);
::SetEndOfFile(hDst.get());将文件与完成端口关联(读和写使用不同的完成键值来区分操作类型):
ATLVERIFY(hCP.get() == ::CreateIoCompletionPort(hSrc.get(), hCP.get(),
(ULONG_PTR)Key::Read, 0));
ATLVERIFY(hCP.get() == ::CreateIoCompletionPort(hDst.get(), hCP.get(),
(ULONG_PTR)Key::Write, 0));
data.hSrc = std::move(hSrc);
data.hDst = std::move(hDst);IOData -- 扩展 OVERLAPPED
struct IOData : OVERLAPPED {
HANDLE hSrc, hDst;
std::unique_ptr<BYTE[]> Buffer;
ULONGLONG Size;
};通过继承 OVERLAPPED,可以在 I/O 完成时携带额外的上下文信息(源句柄、目标句柄、缓冲区、文件总大小)。
首次读取操作
auto io = new IOData;
io->Size = size.QuadPart;
io->Buffer = std::make_unique<BYTE[]>(chunkSize);
io->hSrc = data.hSrc.get();
io->hDst = data.hDst.get();
::ZeroMemory(io, sizeof(OVERLAPPED));
auto ok = ::ReadFile(io->hSrc, io->Buffer.get(), chunkSize, nullptr, io);
ATLASSERT(!ok && ::GetLastError() == ERROR_IO_PENDING);
count += (size.QuadPart + chunkSize - 1) / chunkSize;
}
PostMessage(WM_PROGRESS_START, count);完成处理循环
while (count > 0) {
DWORD transferred;
ULONG_PTR key;
OVERLAPPED* ov;
BOOL ok = ::GetQueuedCompletionStatus(hCP.get(), &transferred, &key, &ov, INFINITE);
if (!ok) {
PostMessage(WM_ERROR, ::GetLastError());
count--;
delete ov;
continue;
}
auto io = static_cast<IOData*>(ov);
if (key == (DWORD_PTR)Key::Read) {
ULARGE_INTEGER offset = { io->Offset, io->OffsetHigh };
offset.QuadPart += chunkSize;
if (offset.QuadPart < io->Size) {
// 还有更多数据要读,发起下一次读取
auto newio = new IOData;
newio->Size = io->Size;
newio->Buffer = std::make_unique<BYTE[]>(chunkSize);
newio->hSrc = io->hSrc;
newio->hDst = io->hDst;
::ZeroMemory(newio, sizeof(OVERLAPPED));
newio->Offset = offset.LowPart;
newio->OffsetHigh = offset.HighPart;
auto ok = ::ReadFile(newio->hSrc, newio->Buffer.get(), chunkSize, nullptr, newio);
auto error = ::GetLastError();
ATLASSERT(!ok && error == ERROR_IO_PENDING);
}
// 将已读数据写入目标文件
io->Internal = io->InternalHigh = 0;
ok = ::WriteFile(io->hDst, io->Buffer.get(), transferred, nullptr, ov);
auto error = ::GetLastError();
ATLASSERT(!ok && error == ERROR_IO_PENDING);
}
else {
// 写入完成
count--;
delete io;
PostMessage(WM_PROGRESS);
}
}当读取完成时(Key::Read):如果还有剩余数据,发起下一次读取;然后使用同一个 IOData 将已读数据写入目标文件。当写入完成时(Key::Write):递减计数器,释放 IOData,通知进度。
可通过 PostQueuedCompletionStatus 发送自定义通知,也可通过限制并发 I/O 操作数来避免内存过度消耗。
使用线程池进行 I/O 完成
Windows 线程池(Thread Pool)提供了对 I/O 完成端口的高层封装,无需手动管理线程和完成循环。
CreateThreadpoolIo
在幕后创建一个 I/O 完成端口并将其与文件关联:
PTP_IO CreateThreadpoolIo(
_In_ HANDLE hFile,
_In_ PTP_WIN32_IO_CALLBACK pfnio,
_Inout_opt_ PVOID pv,
_In_opt_ PTP_CALLBACK_ENVIRON pcbe);回调原型:
typedef VOID (WINAPI *PTP_WIN32_IO_CALLBACK)(
_Inout_ PTP_CALLBACK_INSTANCE Instance,
_Inout_opt_ PVOID Context,
_Inout_opt_ PVOID Overlapped,
_In_ ULONG IoResult,
_In_ ULONG_PTR NumberOfBytesTransferred,
_Inout_ PTP_IO Io);线程池 I/O 生命周期管理
- StartThreadpoolIo -- 必须在每次异步操作前调用,通知线程池一个 I/O 操作已开始
- CancelThreadpoolIo -- 如果 ReadFile/WriteFile 立即失败,取消之前 StartThreadpoolIo 的计数
- WaitForThreadpoolIoCallbacks -- 等待或取消待处理的回调
- CloseThreadpoolIo -- 释放线程池 I/O 对象
VOID StartThreadpoolIo(_Inout_ PTP_IO pio);
VOID CancelThreadpoolIo(_Inout_ PTP_IO pio);
VOID WaitForThreadpoolIoCallbacks(_Inout_ PTP_IO pio, _In_ BOOL fCancelPendingCallbacks);
VOID CloseThreadpoolIo(_Inout_ PTP_IO pio);批量复制 2 应用程序
BulkCopy2 使用线程池进行 I/O 完成,代替了手动创建的专用线程。StartCopy 函数直接遍历文件对并打开文件,无需创建专用线程。
扩展的 FileData
struct FileData {
CString Src;
CString Dst;
wil::unique_handle hDst, hSrc;
wil::unique_threadpool_io tpSrc, tpDst;
};创建线程池 I/O 对象和执行首次读取
data.tpDst.reset(::CreateThreadpoolIo(hDst.get(), WriteCallback, this, nullptr));
data.tpSrc.reset(::CreateThreadpoolIo(hSrc.get(), ReadCallback, data.tpDst.get(), nullptr));
// ...
data.hSrc = std::move(hSrc);
data.hDst = std::move(hDst);
auto io = new IOData;
io->Size = size.QuadPart;
io->Buffer = std::make_unique<BYTE[]>(chunkSize);
io->hSrc = data.hSrc.get();
io->hDst = data.hDst.get();
::ZeroMemory(io, sizeof(OVERLAPPED));
::StartThreadpoolIo(data.tpSrc.get());
auto ok = ::ReadFile(io->hSrc, io->Buffer.get(), chunkSize, nullptr, io);
ATLASSERT(!ok && ::GetLastError() == ERROR_IO_PENDING);
::InterlockedAdd64(&m_OperationCount, (size.QuadPart + chunkSize - 1) / chunkSize);关键点:读取 I/O 对象的上下文参数接收的是写入 I/O 对象(data.tpDst.get()),这样在 ReadCallback 中就能通过 Context 获取写入 I/O 对象来发起 WriteFile。
ReadCallback
void CMainDlg::ReadCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context,
PVOID Overlapped, ULONG IoResult, ULONG_PTR Transferred, PTP_IO Io) {
if (IoResult == ERROR_SUCCESS) {
auto io = static_cast<IOData*>(Overlapped);
ULARGE_INTEGER offset = { io->Offset, io->OffsetHigh };
offset.QuadPart += chunkSize;
if (offset.QuadPart < io->Size) {
auto newio = new IOData;
newio->Size = io->Size;
newio->Buffer = std::make_unique<BYTE[]>(chunkSize);
newio->hSrc = io->hSrc;
newio->hDst = io->hDst;
::ZeroMemory(newio, sizeof(OVERLAPPED));
newio->Offset = offset.LowPart;
newio->OffsetHigh = offset.HighPart;
::StartThreadpoolIo(Io);
auto ok = ::ReadFile(newio->hSrc, newio->Buffer.get(), chunkSize,
nullptr, newio);
auto error = ::GetLastError();
ATLASSERT(!ok && error == ERROR_IO_PENDING);
}
io->Internal = io->InternalHigh = 0;
auto writeIo = (PTP_IO)Context;
::StartThreadpoolIo(writeIo);
auto ok = ::WriteFile(io->hDst, io->Buffer.get(),
(ULONG)Transferred, nullptr, io);
auto error = ::GetLastError();
ATLASSERT(!ok && error == ERROR_IO_PENDING);
}
}WriteCallback
void CMainDlg::WriteCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context,
PVOID Overlapped, ULONG IoResult, ULONG_PTR Transferred, PTP_IO Io) {
if (IoResult == ERROR_SUCCESS) {
auto pThis = static_cast<CMainDlg*>(Context);
pThis->PostMessage(WM_PROGRESS);
auto io = static_cast<IOData*>(Overlapped);
delete io;
if (0 == InterlockedDecrement64(&pThis->m_OperationCount)) {
pThis->PostMessage(WM_DONE);
}
}
}当操作计数归零时,表示所有复制任务完成,发送 WM_DONE 消息。
I/O 取消
Windows 提供了取消正在进行的 I/O 操作的机制:
CancelIo 和 CancelIoEx
BOOL CancelIo(_In_ HANDLE hFile);
BOOL CancelIoEx(
_In_ HANDLE hFile,
_In_opt_ LPOVERLAPPED lpOverlapped);- CancelIo:取消调用线程在指定句柄上的所有异步操作
- CancelIoEx:允许针对指定的 OVERLAPPED 进行精确取消
取消并不保证成功——驱动程序可能不支持取消,或者操作已经进行到无法取消的阶段。因此取消应被视为"请求",而非"保证"。成功取消的操作返回 ERROR_OPERATION_ABORTED。
I/O 也会在以下情况下自动被取消:
- 文件句柄关闭时(除非已关联到完成端口)
- 线程退出时(完成端口关联的句柄上的请求除外)
CancelSynchronousIo
用于取消同步 I/O 操作:
BOOL CancelSynchronousIo(_In_ HANDLE hThread);需要在目标线程句柄上具有 PROCESS_TERMINATE 访问权限。
设备
ReadFile 和 WriteFile 适用于任何设备,但并非所有设备都同时支持读写。DeviceIoControl 提供了额外的设备控制能力:
BOOL DeviceIoControl(
_In_ HANDLE hDevice,
_In_ DWORD dwIoControlCode,
_In_ LPVOID lpInBuffer,
_In_ DWORD nInBufferSize,
_Out_ LPVOID lpOutBuffer,
_In_ DWORD nOutBufferSize,
_Out_opt_ LPDWORD lpBytesReturned,
_Inout_opt_ LPOVERLAPPED lpOverlapped);DeviceIoControl 可以执行设备/文件系统特有的操作,而这些操作无法通过 ReadFile/WriteFile 完成。
设置文件为稀疏文件
typedef struct _FILE_SET_SPARSE_BUFFER {
BOOLEAN SetSparse;
} FILE_SET_SPARSE_BUFFER;
FILE_SET_SPARSE_BUFFER buffer;
buffer.SetSparse = TRUE;
DWORD bytes;
::DeviceIoControl(hFile, FSCTL_SET_SPARSE, &buffer, sizeof(buffer),
nullptr, 0, &bytes, nullptr);在稀疏文件中写入零值范围
FILE_ZERO_DATA_INFORMATION buffer;
buffer.FileOffset.QuadPart = 100;
buffer.BeyondFinalZero.QuadPart = 1 << 20;
::DeviceIoControl(hFile, FSCTL_SET_ZERO_DATA, &buffer, sizeof(buffer),
nullptr, 0, &bytes, nullptr);DumpDrive 应用程序
DumpDrive 演示了如何直接读取物理磁盘的原始字节。它从命令行解析驱动器索引、扇区偏移和扇区数量:
int main(int argc, const char* argv[]) {
if (argc < 4) {
printf("Usage: DumpDrive <index> <offset in sectors> <size in sectors>\n");
return 0;
}
WCHAR path[] = L"\\\\.\\PhysicalDriveX";
path[::wcslen(path) - 1] = argv[1][0];
auto offset = atoll(argv[2]) * 512;
auto size = atol(argv[3]) * 512;打开设备、定位并读取:
HANDLE hDevice = ::CreateFile(path, GENERIC_READ, FILE_SHARE_READ
| FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr);
if (hDevice == INVALID_HANDLE_VALUE)
return Error("Failed to open Physical drive");
LARGE_INTEGER fp;
fp.QuadPart = offset;
if (!::SetFilePointerEx(hDevice, fp, nullptr, FILE_BEGIN))
return Error("Failed in SetFilePointerEx");
auto buffer = std::make_unique<BYTE[]>(size);
DWORD bytes;
if (!::ReadFile(hDevice, buffer.get(), size, &bytes, nullptr))
return Error("Failed to read data");
DisplayData(offset, buffer.get(), bytes);
::CloseHandle(hDevice);
return 0;
}DisplayData 函数 以十六进制格式显示数据:
void DisplayData(long long offset, const BYTE* buffer, DWORD bytes) {
const int bytesPerLine = 16;
for (DWORD i = 0; i < bytes; i += bytesPerLine) {
printf("%16X: ", offset + i);
for (int b = 0; b < bytesPerLine; b++) {
printf("%02X ", buffer[i + b]);
}
printf("\n");
}
}示例输出(截断):
c:\>DumpDrive 1 0 2
0: 33 C0 8E D0 BC 00 7C FB 50 07 50 1F FC BE 1B 7C
10: BF 1B 06 50 57 B9 E5 01 F3 A4 CB BD BE 07 B1 04
20: 38 6E 00 7C 09 75 13 83 C5 10 E2 F4 CD 18 8B F5
...建议的练习:使用 IOCTL_DISK_GET_DRIVE_GEOMETRY 动态获取扇区大小而非硬编码为 512 字节。
符号链接也用于不属于硬件驱动但需要内核模式功能的"软件驱动程序"。例如 Process Explorer 使用类似 "ProcExp152" 的符号链接,作者的 ObjectExplorer 使用 "KObjExp"。打开方式:
HANDLE hDevice = ::CreateFile(L"\\\\.\\ProcExp152", GENERIC_READ | GENERIC_WRITE, 0, nullptr,
OPEN_EXISTING, 0, nullptr);硬件设备符号链接包含 GUID。设备接口按功能类型(打印、扫描等)由 GUID 标识。
EnumDevices 应用程序
通过接口 GUID 枚举设备:
struct DeviceInfo {
std::wstring SymbolicLink;
std::wstring FriendlyName;
};std::vector<DeviceInfo> EnumDevices(const GUID& guid) {
std::vector<DeviceInfo> devices;
auto hInfoSet = ::SetupDiGetClassDevs(&guid, nullptr, nullptr,
DIGCF_PRESENT | DIGCF_INTERFACEDEVICE);
if (hInfoSet == INVALID_HANDLE_VALUE)
return devices;
devices.reserve(4);
SP_INTERFACE_DEVICE_DATA data = { sizeof(data) };
SP_DEVINFO_DATA ddata = { sizeof(ddata) };
BYTE buffer[1 << 12];
for (DWORD i = 0; ; i++) {
if (!::SetupDiEnumDeviceInterfaces(hInfoSet, nullptr, &guid, i, &data))
break;
if (::SetupDiGetDeviceInterfaceDetail(hInfoSet, &data, details,
sizeof(buffer), nullptr, &ddata)) {
DeviceInfo info;
info.SymbolicLink = details->DevicePath;
if(::SetupDiGetDeviceRegistryProperty(hInfoSet, &ddata,
SPDRP_DEVICEDESC, nullptr, buffer, sizeof(buffer), nullptr))
info.FriendlyName = (WCHAR*)buffer;
devices.push_back(std::move(info));
}
}
::SetupDiDestroyDeviceInfoList(hInfoSet);
return devices;
}DisplayDevices 尝试打开每个设备:
void DisplayDevices(const std::vector<DeviceInfo>& devices, const char* name) {
printf("%s\n%s\n", name, std::string(::strlen(name), '-').c_str());
for (auto& di : devices) {
printf("Symbolic link: %ws\n", di.SymbolicLink.c_str());
printf(" Name: %ws\n", di.FriendlyName.c_str());
auto hDevice = ::CreateFile(di.SymbolicLink.c_str(), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING, 0, nullptr);
if (hDevice == INVALID_HANDLE_VALUE)
printf(" Failed to open device (%d)\n", ::GetLastError());
else {
printf(" Device opened successfully!\n");
::CloseHandle(hDevice);
}
}
printf("\n");
}程序入口:
#define INITGUID
#include <Wiaintfc.h>
#include <Ntddvdeo.h>
#include <devpkey.h>
#include <Ntddkbd.h>
int main() {
auto devices = EnumDevices(GUID_DEVINTERFACE_IMAGE);
DisplayDevices(devices, "Image");
DisplayDevices(EnumDevices(GUID_DEVINTERFACE_MONITOR), "Monitor");
DisplayDevices(EnumDevices(GUID_DEVINTERFACE_DISPLAY_ADAPTER),
"Display Adapter");
DisplayDevices(EnumDevices(GUID_DEVINTERFACE_DISK), "Disk");
DisplayDevices(EnumDevices(GUID_DEVINTERFACE_KEYBOARD), "keyboard");
return 0;
}示例输出会显示类似 \\?\display#deld06e#... 和 \\?\pci#ven_8086&dev_3e9b#... 的符号链接。某些设备可能打开失败(错误 5 = 访问拒绝)。注意 "\\?\" 前缀等价于 "\\.\"。
管道(Pipes)和邮槽(Mailslots)
管道(Pipes)是跨进程和跨网络计算机的通信机制,可以是单向或双向的。邮槽(Mailslots)是单向的,可在本地或网络范围内工作。
匿名管道(Anonymous Pipes)
匿名管道是最简单的管道形式,单向、仅限本地使用。通过 CreatePipe 创建:
BOOL CreatePipe(
_Out_ PHANDLE hReadPipe,
_Out_ PHANDLE hWritePipe,
_In_opt_ LPSECURITY_ATTRIBUTES lpPipeAttributes,
_In_ DWORD nSize);SimpleRedirect 应用程序
该应用演示了如何将子进程的输出通过匿名管道重定向到对话框的编辑控件中。
OnRedirect 处理器:
LRESULT CMainDlg::OnRedirect(WORD, WORD wID, HWND, BOOL&) {
wil::unique_handle hRead, hWrite;
if (!::CreatePipe(hRead.addressof(), hWrite.addressof(), nullptr, 0))
return Error(L"Failed to create pipe");
::SetHandleInformation(hWrite.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
if (!CreateOtherProcess(hWrite.get()))
return Error(L"Failed to create process");
hWrite.reset();
char buffer[1 << 12] = { 0 };
DWORD bytes;
CEdit edit(GetDlgItem(IDC_TEXT));
ATLASSERT(edit);
while (::ReadFile(hRead.get(), buffer, sizeof(buffer), &bytes, nullptr) && bytes > 0) {
CString text;
edit.GetWindowText(text);
text += CString(buffer);
edit.SetWindowText(text);
::memset(buffer, 0, sizeof(buffer));
}
// ...
}CreateOtherProcess 设置标准输出的继承:
bool CMainDlg::CreateOtherProcess(HANDLE hOutput) {
PROCESS_INFORMATION pi;
STARTUPINFO si = { sizeof(si) };
si.hStdOutput = hOutput;
si.dwFlags = STARTF_USESTDHANDLES;
WCHAR path[MAX_PATH];
::GetModuleFileName(nullptr, path, _countof(path));
*::wcsrchr(path, L'\\') = L'\0';
::wcscat_s(path, L"\\EnumDevices.exe");
BOOL created = ::CreateProcess(nullptr, path, nullptr, nullptr, TRUE,
CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi);
if (created) {
::CloseHandle(pi.hProcess);
::CloseHandle(pi.hThread);
}
return created;
}该流程将管道写入端标记为可继承,创建子进程时将其作为 stdout,然后关闭本地写入端,最后从读取端持续读取子进程输出并显示在编辑框中。
事务性 NTFS
内核事务管理器(KTM, Kernel Transaction Manager)支持对文件和注册表操作进行事务处理,这一特性有时被称为事务性 NTFS(Transactional NTFS,TxF)。事务遵循 ACID 属性:
- 原子性(Atomicity)—— 所有操作要么全部完成,要么全部回滚
- 一致性(Consistency)—— 事务前后数据处于一致状态
- 隔离性(Isolation)—— 并发事务互不干扰
- 持久性(Durability)—— 已提交事务的结果永久保存
警告:微软的文档多年来一直警告开发者不要依赖 KTM,建议寻找其他替代机制。虽然事务性支持目前仍存在于 Windows 中,但可能在未来的 Windows 版本中被移除。
CreateTransaction
HANDLE CreateTransaction (
_In_opt_ LPSECURITY_ATTRIBUTES lpTransactionAttributes,
_In_opt_ LPGUID UOW,
_In_opt_ DWORD CreateOptions,
_In_opt_ DWORD IsolationLevel,
_In_opt_ DWORD IsolationFlags,
_In_opt_ DWORD Timeout,
_In_opt_ LPTSTR Description);需要包含 <ktmw32.h> 并链接 ktmw32.lib。
UOW必须为 NULLIsolationLevel和IsolationFlags必须为 0CreateOptions可以是 0 或TRANSACTION_DO_NOT_PROMOTETimeout:0 表示无超时,INFINITE表示无限等待,或指定毫秒数Description:可选的人类可读描述文本- 返回事务句柄或
INVALID_HANDLE_VALUE
CreateFileTransacted
HANDLE CreateFileTransacted(
_In_ LPCTSTR lpFileName,
_In_ DWORD dwDesiredAccess,
_In_ DWORD dwShareMode,
_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
_In_ DWORD dwCreationDisposition,
_In_ DWORD dwFlagsAndAttributes,
_In_opt_ HANDLE hTemplateFile,
_In_ HANDLE hTransaction,
_In_opt_ PUSHORT pusMiniVersion,
_Reserved_ PVOID lpExtendedParameter);文件名必须引用本地文件。pusMiniVersion 的取值(来自 txfw32.h):
TXFS_MINIVERSION_COMMITTED_VIEW—— 基于最后提交的版本TXFS_MINIVERSION_DIRTY_VIEW—— 正在被事务修改的"脏"视图TXFS_MINIVERSION_DEFAULT_VIEW—— 对非修改操作为已提交视图,否则为脏视图- 自定义 miniversions 通过
FSCTL_TXFS_CREATE_MINIVERSION创建
返回的句柄可用于 ReadFile、WriteFile 等函数。其他事务化函数包括:CopyFileTransacted、CreateHardLinkTransacted、DeleteFileTransacted、CreateDirectoryTransacted 等。
提交和回滚事务
BOOL CommitTransaction(_In_ HANDLE TransactionHandle);
BOOL RollbackTransaction(_In_ HANDLE TransactionHandle);使用 CloseHandle 关闭事务句柄。事务的内核对象类型为 TmTx。
其他事务 API
BOOL GetTransactionId (
_In_ HANDLE TransactionHandle,
_Out_ LPGUID TransactionId);
HANDLE OpenTransaction (
_In_ DWORD dwDesiredAccess,
_In_ LPGUID TransactionId);事务通过公共日志文件系统(CLFS, Common Log File System)日志实现。
文件搜索和枚举
FindFirstFile / FindFirstFileEx
HANDLE FindFirstFileW(
_In_ LPCTSTR lpFileName,
_Out_ LPWIN32_FIND_DATA lpFindFileData);
HANDLE FindFirstFileEx(
_In_ LPCTSTR lpFileName,
_In_ FINDEX_INFO_LEVELS fInfoLevelId,
_Out_ LPVOID lpFindFileData,
_In_ FINDEX_SEARCH_OPS fSearchOp,
_Reserved_ LPVOID lpSearchFilter,
_In_ DWORD dwAdditionalFlags);两个函数都接受可包含通配符的文件名(例如 c:\temp\*.png)。
WIN32_FIND_DATA 结构:
typedef struct _WIN32_FIND_DATA {
DWORD dwFileAttributes;
FILETIME ftCreationTime;
FILETIME ftLastAccessTime;
FILETIME ftLastWriteTime;
DWORD nFileSizeHigh;
DWORD nFileSizeLow;
DWORD dwReserved0;
DWORD dwReserved1;
_Field_z TCHAR cFileName[MAX_PATH];
_Field_z TCHAR cAlternateFileName[14];
} WIN32_FIND_DATA, *PWIN32_FIND_DATA, *LPWIN32_FIND_DATA;FindFirstFileEx 的额外参数:
- fInfoLevelId:
FindExInfoStandard(等价于 FindFirstFile)或FindExInfoBasic(跳过短文件名,更快) - fSearchOp:
FindExSearchLimitToDirectories(提示仅搜索目录) - dwAdditionalFlags:
FIND_FIRST_EX_CASE_SENSITIVE、FIND_FIRST_EX_LARGE_FETCH、FIND_FIRST_EX_ON_DISK_ENTRIES_ONLY
返回搜索句柄或 INVALID_HANDLE_VALUE。
FindNextFile 和 FindClose
BOOL FindNextFile(
_In_ HANDLE hFindFile,
_Out_ LPWIN32_FIND_DATA lpFindFileData);
BOOL FindClose(_Inout_ HANDLE hFindFile);当 FindNextFile 返回 FALSE 时表示没有更多匹配。搜索完毕后必须调用 FindClose 释放搜索句柄。
NTFS 流
NTFS 支持文件流(File Streams)——本质上是"文件中的文件"。默认的数据流($DATA)就是通常意义上的文件内容,但 NTFS 允许在一个文件中创建额外的命名流。这些附加流通常被资源管理器等标准工具隐藏。
可以将 NTFS 流理解为"文件的附属口袋"——一个文件在表面上只有一个主口袋(默认流),但实际上可以在其内部隐藏任意多的附属口袋,每个口袋里都能存放独立的数据。
常见例子:下载的文件会附带一个 "Zone.Identifier" 流,用于标记文件来源。Sysinternals 的 streams 工具可以识别这些流。示例输出:
C:\>streams -nobanner file.chm
C:\file.chm:
:Zone.Identifier:$DATA 26用 CreateFile 创建隐藏流
HANDLE hFile = ::CreateFile(L"c:\\temp\\myfile.txt:mystream", GENERIC_WRITE, 0,
nullptr, CREATE_NEW, 0, nullptr);
char text[] = "Hello from a hidden stream!";
DWORD bytes;
::WriteFile(hFile, text, ::strlen(text), &bytes, nullptr);
::CloseHandle(hFile);创建这个流后,dir myfile.txt 显示文件大小为 0:
C:\temp>dir myfile.txt
... 1 File(s) 0 bytes
C:\temp>streams -nobanner myfile.txt
:mystream:$DATA 27$DATA 后缀是默认流重解析点。
枚举流
HANDLE WINAPI FindFirstStream(
_In_ LPCTSTR lpFileName,
_In_ STREAM_INFO_LEVELS InfoLevel,
_Out_ LPVOID lpFindStreamData,
_Reserved_ DWORD dwFlags);
BOOL FindNextStreamW(
_In_ HANDLE hFindStream,
_Out_ LPVOID lpFindStreamData);WIN32_FIND_STREAM_DATA 结构:
typedef struct _WIN32_FIND_STREAM_DATA {
LARGE_INTEGER StreamSize;
WCHAR cStreamName[MAX_PATH + 36];
} WIN32_FIND_STREAM_DATA, *PWIN32_FIND_STREAM_DATA;总结
本章涵盖了文件和设备的输入/输出操作,包括同步和异步两种模式。核心内容回顾:
- I/O 系统架构:用户态 API 通过 I/O 管理器将请求转换为 IRP,传递给设备驱动程序
- CreateFile:所有 I/O 操作的统一入口点,不仅用于文件,还用于设备、管道、邮槽等
- 符号链接:Windows 对象管理器中的核心名称解析机制,是访问设备的基础
- 路径长度:从传统的 260 字符 MAX_PATH 限制到现代长路径支持
- 同步 I/O:通过文件指针自动推进的简单模型
- 异步 I/O:通过 OVERLAPPED 结构实现,有多种完成通知机制
- I/O 完成端口:最高效的异步通知机制,LIFO 调度和并发控制
- 线程池 I/O:对 IOCP 的高层封装,简化异步 I/O 编程
- I/O 取消:CancelIo/CancelIoEx 提供可请求但不保证的取消
- DeviceIoControl:设备控制的通用接口,支持文件系统特有的操作
- 管道和邮槽:进程间通信机制
- 事务性 NTFS:ACID 事务支持(不被推荐用于新代码)
- NTFS 流:文件中的隐藏数据流
未涵盖的主题包括:文件操作(复制、移动)、文件链接(软链接和硬链接)、文件锁定以及文件加密/解密。
下一章将探讨内存管理,这是任何应用程序和操作系统的核心领域。