第11.15章: NT 文件 API

English

概述

现代 Windows(NT、2000、XP、7、10、11)建立在 NT 内核之上。用户态文件 I/O 的最低层入口是原生系统调用:

它们位于 ntdll.dll,正是 Win32 子系统自己调用的那些函数。此外还有一套带 Zw 前缀的对应入口(ZwCreateFile 等);它们的区别只在于“前一模式”参数被设为 KernelMode 还是 UserMode。对用户态代码来说行为完全相同。

fast_io 通过下面这些类型暴露 NT 层:

编程模型与 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:

原始的 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
}

小结