为什么 C 拥有最好的文件 API

发布: (2026年3月2日 GMT+8 03:25)
5 分钟阅读

Source: Hacker News

2026-02-28 (Programming) (Rants)

内存映射(Memory Mapping)在 C 中的使用

有很多优秀的编程语言,但文件操作总是像事后才想到的。通常只能得到 read()write() 以及某种序列化库。

在 C 中,你可以像访问内存中的数据一样访问文件:

#include 
#include 
#include 
#include 
#include 

int main(void) {
    // Create/open a file containing 1000 unsigned integers,
    // initialized to all zeros.
    int len = 1000 * sizeof(uint32_t);
    int file = open("numbers.u32", O_RDWR | O_CREAT, 0600);
    ftruncate(file, len);

    // Map it into memory.
    uint32_t *numbers = mmap(NULL, len,
        PROT_READ | PROT_WRITE, MAP_SHARED,
        file, 0);

    // Do something:
    printf("%d\n", numbers[42]);
    numbers[42] = numbers[42] + 1;

    // Clean up
    munmap(numbers, len);
    close(file);
    return 0;
}

内存映射与加载的区别

内存映射并不等同于把文件加载到内存中:即使文件不适合放入 RAM,也能工作。数据会按需加载,所以打开一个 TB 级别的文件不会花费整天时间。它适用于所有数据类型,并且会自动缓存;如果系统需要内存,缓存会被清除。

其他语言的局限性

然而,在大多数其他语言中,你必须用 read() 读取小块数据,进行解析、处理、序列化,最后再 write() 回磁盘。这既繁琐,又不必要地限制为顺序访问——计算机已经几十年没有使用磁带了。

如果你足够幸运拥有内存映射,它通常也只能用于字节数组,仍然需要显式的解析/序列化。结果往往只是 read()write() 的更好包装。

性能与开销

C 的实现并不完美:内存映射会产生开销(页面错误、TLB 刷新),而且 C 并不处理字节序问题——但它仍然比完全没有直接访问要好。

当然,你可能想做一些解析和校验,但这不应该在每次数据离开磁盘时都必须进行。内存不足的情况非常常见,这会导致无法把所有内容解析到 RAM 中。能够在不增加代码复杂度的情况下卸载数据非常有用。

安全性与序列化

只要看看 Python 的 pickle:这是一种完全不安全的序列化格式。加载一个文件甚至可能导致代码执行,即使你只想要一些数字,但它仍被广泛使用,因为它符合 Python 的“代码‑数据混合”模型。很多文件并不是不可信的。

文件系统即数据库

文件操作 同样被忽视。文件系统是最早的 NoSQL 数据库,但你通常只能得到一个包装了 C 的 readdir() 的接口。这通常导致人们在文件系统之上再跑另一个数据库,例如 SQLite,但关系型数据库从来都不完全适合你的程序。

…而且 SQL 的整合比文件更糟:除了必须序列化所有数据外,你还得用一种完全不同的语言来编写访问代码!大多数程序员最终把它当作键‑值存储来使用,并自行实现索引,结果形成了奇怪的三层嵌套数据库。

结论

所以回答标题的问题,我认为这源于一个错误的假设:认为从文件读取的数据来自别处,需要被解析;而写入磁盘的数据是要发送到别处,需要序列化成标准格式。这在内存受限的系统上根本不成立——而且在 100 GB 级别的文件面前,所有系统都是内存受限的。

0 浏览
Back to Blog

相关文章

阅读更多 »

Rust 只是一个工具

我喜欢 Rust。它足够通用,可以用于应用程序和系统编程。它拥有我见过的任何语言中最好的工具链。它还有相当…

Rust 只是一种工具

2026年2月4日 我喜欢 Rust。它足够通用,可以用于应用程序和系统编程。它拥有我见过的任何语言中最好的 tooling。它...