如何访问文件系统?

解读

在国内 Rust 后端/基础架构岗位面试中,文件系统操作是“系统编程基本功”的必考点。面试官不仅想知道“能不能读文件”,更关注:

  1. 是否理解 std::fsstd::path 的语义差异;
  2. 能否正确处理 UTF-8 路径跨平台分隔符权限掩码
  3. 是否具备错误传播资源及时释放的意识;
  4. 大文件并发读写的零拷贝Direct I/O 有无工程级经验。

一句话:把“能跑”升级为“生产可用”。

知识点

  1. std::fs 核心 API:read_to_string、read_to_end、write、OpenOptions、metadata、symlink_metadata、DirEntry。
  2. std::path::{Path,PathBuf}:区别于 String,具备平台无关的语义;Path::new 不分配,PathBuf 可追加。
  3. 错误类型:std::io::Error,内部带 ErrorKind,需用 match 或 map_err 精准处理,而非 unwrap。
  4. 所有权与 RAII:File 一旦离开作用域即关闭,无需手动 close;但显式 drop 可在临界点前强制刷盘。
  5. 缓冲与零拷贝:BufReader/BufWriter 减少 syscall;mmap 需 libc 封装,面试提及即可。
  6. 并发安全:RwLock<File> 或 channel 单线程串行写,避免数据竞争;flock 需 libc,考点是“知道存在”。
  7. 国内踩坑实录
    • Windows 路径含中文且代码页非 UTF-8,需启用 windows::process::Command 的 UTF-8 特性
    • Linux 容器内umask 0022导致新建文件权限不足,CI 偶现 PermissionDenied;
    • 阿里云 NAS 延迟高,read_to_string 一次性加载 200 MB 日志直接 OOM,需流式读取

答案

use std::fs::{self, OpenOptions};
use std::io::{self, BufRead, BufReader, Write};
use std::path::Path;

/// 生产级文件读取:带缓冲、按行处理、错误细化
pub fn process_log(path: impl AsRef<Path>) -> io::Result<usize> {
    let file = OpenOptions::new()
        .read(true)
        // 明确禁止写,防止意外篡改
        .write(false)
        .open(path.as_ref())?;

    let mut reader = BufReader::new(file);
    let mut count = 0;
    let mut line = String::with_capacity(256);
    loop {
        line.clear();
        let n = reader.read_line(&mut line)?;
        if n == 0 {
            break;
        }
        if line.trim_end().contains("ERROR") {
            count += 1;
        }
    }
    Ok(count)
}

/// 生产级文件写入:原子写临时文件 + rename,防止**半写**可见
pub fn atomic_write(path: impl AsRef<Path>, content: &[u8]) -> io::Result<()> {
    let path = path.as_ref();
    let tmp = path.with_extension("tmp");
    {
        let mut f = OpenOptions::new()
            .write(true)
            .create(true)
            .truncate(true)
            // 国内云盘延迟高,sync 保证落盘
            .open(&tmp)?;
        f.write_all(content)?;
        f.sync_all()?;
    } // 此处 f 被 drop,fd 关闭
    fs::rename(tmp, path)?;
    Ok(())
}

要点讲解:

  1. AsRef<Path> 做参数,使调用者可传 &str、String、Path、PathBuf,零成本抽象
  2. OpenOptions 链式调用,比 fs::write 更细粒度,面试加分。
  3. sync_all + rename 是“原子写”标准套路,答出即可拉开与普通面试者差距。
  4. 全程 io::Result,? 运算符传播错误,符合 Rust 惯用法。

拓展思考

  1. 超大文件(>10 GB)单机处理:可结合 memmap2 crate 做只读 mmap,再按 64 MB chunk 并行扫描,crossbeam-channel 把行偏移发给线程池,兼顾零拷贝并发安全
  2. 国内政企内网常禁用 std 网络,可把日志通过命名管道(fifo)导出;Rust 打开 fifo 与常规文件 API 一致,只需设置 OpenOptions::custom_flags(libc::O_NONBLOCK)
  3. 嵌入式 Linux(如瑞芯微 ARM)没有 std,需用 #![no_std] + alloc,文件系统走 libc::open/close/read/write 封装,panic_handleralloc_error_handler 要自己实现,面试提及即体现“系统级”深度。
  4. 若面试官追问“如何防止日志被篡改”,可答:使用 append-only 属性(chattr +a),Rust 侧用 OpenOptions::append(true);进一步可引入 dm-integritymerkle tree可信日志,把区块链思路落地到单机文件系统。