第11.15章: NT 文件 API
概述
现代 Windows(NT、2000、XP、7、10、11)建立在 NT 内核之上。用户态文件 I/O 的最低层入口是原生系统调用:
NtCreateFileNtReadFileNtWriteFileNtClose
它们位于 ntdll.dll,正是 Win32 子系统自己调用的那些函数。此外还有一套带 Zw 前缀的对应入口(ZwCreateFile 等);它们的区别只在于“前一模式”参数被设为 KernelMode 还是 UserMode。对用户态代码来说行为完全相同。
fast_io 通过下面这些类型暴露 NT 层:
-
::fast_io::nt_file— RAII 拥有式包装。构造函数调用NtCreateFile,析构函数调用NtClose。 -
::fast_io::nt_io_observer— 对已有 NTHANDLE的非拥有视图。可平凡复制,没有析构函数。
编程模型与 posix_file、win32_file 完全是同一个模式。
用 nt_file 打开文件
#include <fast_io.h>
#include <fast_io_device.h>
int main()
{
using namespace ::fast_io::iomnp;
// The constructor translates open_mode into the corresponding
// NtCreateFile parameters (DesiredAccess, CreateDisposition, ...).
::fast_io::nt_file nf(u8"hello_nt.txt", ::fast_io::open_mode::out);
print(nf, "Hello from nt_file!\n");
::std::size_t const n{42zu};
println(nf, u8"The answer is: ", n);
// NtClose is invoked automatically when `nf` goes out of scope.
}
包裹已有的 HANDLE
#include <fast_io.h>
#include <fast_io_device.h>
void inspect(void* nt_handle)
{
using namespace ::fast_io::iomnp;
// Non-owning view. No NtClose on destruction.
::fast_io::nt_io_observer observer{
static_cast<::fast_io::nt_io_observer::native_handle_type>(nt_handle)};
print(observer, "Written through an nt_io_observer\n"
"\n", ::fast_io::mnp::handlevw(observer.native_handle()));
}
int main()
{
::fast_io::nt_file nf(u8"inspect.txt", ::fast_io::open_mode::out);
inspect(nf.native_handle());
}
相对打开
由于 NtCreateFile 原生接受一个 RootDirectory 句柄和一个相对的 ObjectName,NT 层是 Windows 上 at(dir) 最自然的归宿:
#include <fast_io.h>
#include <fast_io_device.h>
int main(int argc, char** argv)
{
using namespace ::fast_io::iomnp;
::fast_io::nt_file dir{::fast_io::mnp::os_c_str(argv[1]),
::fast_io::open_mode::directory};
::fast_io::nt_file nf(at(dir), u8"relative.txt",
::fast_io::open_mode::out);
println(nf, u8"Opened through NtCreateFile with a root directory\n");
}
为什么直接使用 nt_file?
大多数程序直接用 ::fast_io::native_file(在 NT 上别名到 nt_file)或 ::fast_io::iobuf_file 即可。当你需要下列能力时,才直接使用 nt_file:
- 不经
kernel32.dll的直接内核访问。 - 对
NtCreateFile参数的细粒度控制。 - 相对打开,它能干净地映射到原生 API 上。
原始的 NT I/O API
在使用 fast_io 的包装之前,先了解底层 NT 原生 API 会有帮助。这些是由 ntdll.dll 导出的函数,也就是 kernel32.dll 自己调用的最低层用户态入口:
| API 函数 | 用途 |
|---|---|
NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, AllocationSize, FileAttributes, ShareAccess, CreateDisposition, CreateOptions, EaBuffer, EaLength) |
打开或创建文件。通过 FileHandle 返回 HANDLE。这是 NT 上最基础的文件打开 API。 |
NtReadFile(FileHandle, Event, ApcRoutine, ApcContext, IoStatusBlock, Buffer, Length, ByteOffset, Key) |
从文件句柄读取字节。可以是同步或异步的。 |
NtWriteFile(FileHandle, Event, ApcRoutine, ApcContext, IoStatusBlock, Buffer, Length, ByteOffset, Key) |
向文件句柄写入字节。可以是同步或异步的。 |
NtClose(Handle) |
关闭句柄。永远需要 — 句柄是有限资源。 |
NtSetInformationFile(FileHandle, IoStatusBlock, FileInformation, Length, FileInformationClass) |
设置文件信息,包括文件位置(定位)。配合 FilePositionInformation 使用。 |
NtQueryInformationFile(FileHandle, IoStatusBlock, FileInformation, Length, FileInformationClass) |
查询文件信息,包括文件位置与大小。 |
另外还有 Zw* 变体(ZwCreateFile、ZwReadFile 等)。在用户空间,Nt* 与 Zw* 是相同的。它们只在内核态才有区别:Nt* 校验来自用户态的指针参数,而 Zw* 把前一模式设为 KernelMode 并跳过校验。对用户态代码来说行为一致。
这些 NT API 不带缓冲 — 每次调用都经 ntdll.dll 进入内核(ntoskrnl.exe)。这使得它们在操作很小或很频繁时很慢。fast_io 的带缓冲类型(如 ibuf_file 与 obuf_file)用用户态缓冲包装这些调用,性能好得多。
即使不用 fast_io 的 I/O,也可以用 nt_file 做 RAII
即使你更愿意直接调用原始 NT API(例如 NtReadFile 与 NtWriteFile)而不使用 fast_io 的 print 与 scan,你仍然应该用 ::fast_io::nt_file 做 RAII。它的析构函数保证调用 NtClose,所以你永远不会忘记关闭句柄。
当涉及异常或提前返回时,这一点尤其重要 — 没有 RAII,你必须在每一条退出路径上手动调用 NtClose,这很容易出错。
#include <fast_io.h>
#include <fast_io_device.h>
#include <ntstatus.h>
// Note: NtCreateFile, NtReadFile, etc. are declared in the Windows SDK headers.
// fast_io provides wrappers, but you can also call them directly.
int main()
{
// Open with nt_file for RAII, but use raw NT NtReadFile/NtWriteFile
::fast_io::nt_file nf(u8"data.bin", ::fast_io::open_mode::out);
// Get the raw HANDLE
void* h{nf.native_handle()};
// In practice, calling NtWriteFile directly is complex due to IO_STATUS_BLOCK
// and OBJECT_ATTRIBUTES setup. Most programs should use fast_io's print/println.
// This example shows the concept, but real code typically uses fast_io's I/O.
// nf's destructor calls NtClose(h) automatically when it goes out of scope.
// No need to manually call NtClose() here, even if we return early or throw.
}
注意:直接调用 NT API 很复杂。NtCreateFile 需要设置 OBJECT_ATTRIBUTES、IO_STATUS_BLOCK 以及许多其他参数。NtReadFile 与 NtWriteFile 也同样繁琐。大多数程序应该改用 fast_io 的 print、println 与 scan。这里记录原始 API 是为了完整,并说明 fast_io 是用 RAII 把它们包起来的。
如果你确实直接调用 NT API,同样的 RAII 原则仍然适用:
#include <fast_io.h>
#include <fast_io_device.h>
int main()
{
::fast_io::nt_file nf(u8"data.bin", ::fast_io::open_mode::in);
void* h{nf.native_handle()};
// If you call NtReadFile directly here...
// (complex setup omitted for brevity)
// NtClose(h) is called automatically by nf's destructor
}
小结
-
::fast_io::nt_file用 RAII 包装 NT 内核的NtCreateFile/NtClose。 -
::fast_io::nt_io_observer是对已有 NTHANDLE的非拥有视图。 -
API 表面与
posix_file、win32_file一致:学会一层之后,其他层都变得平平无奇。 -
在现代 Windows 上,
::fast_io::native_file是nt_file的别名。 -
原始 NT API 有
NtCreateFile、NtReadFile、NtWriteFile、NtClose、NtSetInformationFile与NtQueryInformationFile。它们不带缓冲,都经ntdll.dll进入内核。 -
Nt*与Zw*变体在用户空间完全相同,只在内核态才有区别。 -
即使你直接调用原始 NT API(例如
NtReadFile与NtWriteFile),也仍然要用::fast_io::nt_file做 RAII,以保证NtClose被自动调用。这能防止资源泄漏,在有异常或提前返回时尤其如此。 -
直接调用 NT API 很复杂。大多数程序应该改用
fast_io的print、println与scan。 - 关于 NT API 的更多细节,见 Microsoft WDM 文档。