第11.8章: 文件加载器(内存映射)

English

概述

fast_io 提供两种文件加载器类型,它们把整个文件装入内存,并通过类似容器的接口暴露出来。它们都是只可移动、由 RAII 管理的对象,离开作用域时会自动释放底层内存。

两种加载器提供相同的接口,因此你可以根据平台能力在它们之间切换,而不必改动代码的其余部分。大多数情况下,你应该使用 native_file_loader,让它为你的平台自动选择最佳实现。

公共接口

native_file_loader 与 allocation_file_loader 都提供类似连续容器的接口:

它们也可以直接打印:把加载器传给 print() 或 println(),文件内容就会被写到输出上。

native_file_loader

::fast_io::native_file_loader 使用操作系统的内存映射设施。在 POSIX 系统上它调用 mmap();在 Windows NT 上使用 NtCreateSection 与 NtMapViewOfSection。文件不会被读进另一块缓冲区 — 而是把文件内容直接映射进虚拟内存。对映射区域的访问由操作系统的缺页机制处理,从而获得零复制语义。

重要:内存映射默认使用写时复制(COW)页。这意味着:

如果你需要把修改写回磁盘,必须显式地用文件 I/O 操作写回。如果你需要修改文件并写回,请改用 allocation_file_loader(见下文)。


#include <fast_io.h>
#include <fast_io_device.h>

using namespace fast_io::io;

int main(int argc, char** argv)
{
    if (argc < 2)
    {
        perr("Usage: ", ::fast_io::mnp::os_c_str(*argv), " <file>\n");
        return 1;
    }

    // Memory-map the entire file
    ::fast_io::native_file_loader loader(::fast_io::mnp::os_c_str(argv[1]));

    // Print the entire file contents directly
    print(loader);
    println("File size: ", loader.size(), " bytes");

    // Iterate over the bytes
    ::std::size_t newline_count{};
    for (auto ch : loader)
    {
        if (ch == u8'\n')
        {
            ++newline_count;
        }
    }
    println("Newline count: ", newline_count);

    // Random access via operator[]
    if (loader.size() > 0zu)
    {
        println("First byte: ", loader[0]);
    }

    // Use with algorithms via begin()/end()
    auto it = ::std::find(loader.begin(), loader.end(), u8'X');
    if (it != loader.end())
    {
        println("Found 'X' at offset: ", static_cast<::std::size_t>(it - loader.begin()));
    }
}
      

加载器是只可移动的。你可以用 ::std::move 转移所有权,但不能复制它。加载器销毁时,内存映射被自动释放。

allocation_file_loader

::fast_io::allocation_file_loader 提供同样的接口,但用 malloc() 分配缓冲区、用 read() 填充它。这用于不支持内存映射的平台,例如 WASM(WebAssembly),或者使用 newlib 的环境。当你需要文件数据位于你自己掌控的堆内存中时,它也有用。


#include <fast_io.h>
#include <fast_io_device.h>

using namespace fast_io::io;

int main(int argc, char** argv)
{
    if (argc < 2)
    {
        perr("Usage: ", ::fast_io::mnp::os_c_str(*argv), " <file>\n");
        return 1;
    }

    // Load file into a heap-allocated buffer
    ::fast_io::allocation_file_loader loader(::fast_io::mnp::os_c_str(argv[1]));

    // Same container-like interface
    println("File size: ", loader.size(), " bytes");
    // Print file contents
    print(loader);

    // Access individual bytes
    for (::std::size_t i{}; i < loader.size(); ++i)
    {
        if (loader[i] == u8'\n')
        {
            println("Newline at offset: ", i);
        }
    }
}
      

在两者之间选择

特性 native_file_loader allocation_file_loader
底层机制 mmap() / MapViewOfFile(在不支持内存映射的平台上则是 malloc() + read()) malloc() + read()
是否复制到用户缓冲区 在支持内存映射的平台上不复制(零复制);其他平台上复制 复制
平台支持 所有平台(无内存映射时别名到 allocation_file_loader) 所有平台
在 WASM / newlib 上可用 可用(别名到 allocation_file_loader) 可用
接口 data(), size(), begin(), end(), operator[] data(), size(), begin(), end(), operator[]
只可移动、RAII 是 是

如果你的目标平台支持内存映射,优先用 native_file_loader 以获得最佳性能。如果你需要最大的可移植性(例如交叉编译到 WASM),就用 allocation_file_loader。

重要:如果你需要加载文件、在内存中大量修改其内容,甚至整个重建文件,请使用 allocation_file_loader 而不是 native_file_loader。内存映射式加载器会一直保持文件打开直到自己被销毁,这使你无法重新以写方式打开该文件或重建它。allocation_file_loader 把整个文件读进堆内存并立即关闭它,从而使你能安全地修改数据并写回(或者写入完全不同的内容)到同一个文件。


#include <fast_io.h>
#include <fast_io_device.h>

int main()
{
    using namespace ::fast_io::iomnp;

    // Load file into heap memory (file is closed after loading)
    ::fast_io::allocation_file_loader loader(u8"data.txt");

    // Modify the contents in memory
    for (auto& ch : loader)
    {
        if (ch == u8'a') ch = u8'A';
    }

    // Now we can safely write back to the same file
    ::fast_io::obuf_file obf(u8"data.txt");
    print(obf, loader);
}
      

如果在上面的例子里用了 native_file_loader,文件会保持内存映射并被锁定,导致 obuf_file 构造失败,或者写入破坏映射。

移动语义与 RAII

两种加载器都是只可移动类型。它们的析构函数释放底层资源(解除映射或释放缓冲区)。你可以通过 ::std::move 转移所有权:


#include <fast_io.h>
#include <fast_io_device.h>
#include <utility>

using namespace fast_io::io;

::fast_io::native_file_loader load_file(char const* path)
{
    ::fast_io::native_file_loader loader(path);
    println("Loaded file of size: ", loader.size());
    return loader; // NRVO or move
}

int main()
{
    auto loader = load_file("example.txt");

    // loader now owns the mapped file
    println("Size in main: ", loader.size());
    print(loader);

    // Move into another variable
    auto loader2 = ::std::move(loader);
    // loader is now in a moved-from state; loader2 owns the data

    // When loader2 goes out of scope, the mapping is released
}
      

相对于某个目录加载文件

你可以把 ::fast_io::dir_file 与文件加载器结合,相对于某个目录加载文件。对目录项调用 drt(ent),得到一个加载器可以直接打开的 (fd, name) 对。


#include <fast_io.h>
#include <fast_io_device.h>

int main()
{
    using namespace ::fast_io::iomnp;

    ::fast_io::dir_file df(u8"src");

    // Iterate over all files recursively
    for (auto const& ent : recursive(at(df)))
    {
        if (type(ent) != ::fast_io::file_type::regular) continue;

        // Load only .cpp files
        ::std::u8string_view ext{u8extension(ent)};
        if (ext != u8".cpp") continue;

        // drt(ent) gives the (fd, name) pair to open this entry
        ::fast_io::native_file_loader loader(drt(ent));
        ::std::size_t lines{};
        for (auto ch : loader)
        {
            if (ch == u8'\n') ++lines;
        }
        print(::fast_io::mnp::code_cvt(u8filename(ent)), ": ", lines, " lines\n");
    }
}
      

这种写法很高效,因为 drt(ent) 复用了目录的文件描述符 — 解析路径不需要额外的 open() 调用。

要点