第11.8章: 文件加载器(内存映射)
概述
fast_io 提供两种文件加载器类型,它们把整个文件装入内存,并通过类似容器的接口暴露出来。它们都是只可移动、由 RAII 管理的对象,离开作用域时会自动释放底层内存。
-
::fast_io::native_file_loader— 在可用时使用内存映射(POSIX 上的mmap(),Windows 上的CreateFileMapping/MapViewOfFile)。这是零复制的方式:操作系统把文件直接映射进进程的虚拟地址空间。重要:在不支持内存映射的平台(例如 WASM、newlib)上,native_file_loader会自动别名到allocation_file_loader。 -
::fast_io::allocation_file_loader— 用malloc()加read()把整个文件读进堆内存。这在所有平台上都可用,包括不支持内存映射的平台。
两种加载器提供相同的接口,因此你可以根据平台能力在它们之间切换,而不必改动代码的其余部分。大多数情况下,你应该使用 native_file_loader,让它为你的平台自动选择最佳实现。
公共接口
native_file_loader 与 allocation_file_loader 都提供类似连续容器的接口:
data()— 返回指向已加载文件数据的指针。size()— 以::std::size_t返回文件的字节大小。begin()/end()— 遍历文件字节的随机访问迭代器。operator[](i)— 访问下标i处的字节。empty()— 文件大小为零时返回true。
它们也可以直接打印:把加载器传给 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() 调用。
要点
-
优先用文件加载器而不是随机访问 I/O。当你需要读取整个文件或进行大量随机访问时,使用
::fast_io::native_file_loader(或allocation_file_loader)而不是定位操作。文件加载器更简单、更快、更不易出错。 - 定位操作难用而且慢。第11.7章里的定位操作要求你手动跟踪文件位置、处理不完整的读写、并发起多次系统调用。对大多数用例来说,把整个文件装入内存更高效。
-
按位置 I/O(pwrite/pread)同样复杂。虽然按位置 I/O 避免了改变文件位置,但它仍然需要手动管理偏移和缓冲区指针。文件加载器给你一个干净的容器接口,带
operator[]、begin()/end(),以及标准库的算法。 -
文件加载器并非总是可行。对没有固定大小的流(网络 socket、管道、终端),你无法使用文件加载器。那些情况下,就按需退回带缓冲 I/O(
ibuf_file、obuf_file)或不带缓冲 I/O(native_file)。 -
在可用时使用
native_file_loader。它通过内存映射提供零复制访问。在不支持内存映射的平台上,它会自动别名到allocation_file_loader。注意映射使用写时复制页:你可以修改内存中的字节,但这些改动不影响磁盘上的文件,并且加载器销毁时就丢失了。 -
读-改-写循环或重建文件时使用
allocation_file_loader。如果你需要加载文件、大量修改其内容,或完全重建文件,请使用allocation_file_loader。native_file_loader会让文件保持内存映射并被锁定,使你无法写入或重建它。allocation_file_loader把文件读进堆内存并立即关闭,使你能够安全地修改数据并写回,或者创建一个全新的文件。 -
配合
dir_file做相对于目录的加载。对目录项使用drt(ent),就能在没有额外路径解析的情况下高效地加载文件。