Skip to content

feat(fs):支持 EROFS 只读文件系统 #2175

Description

@kaleidoscope416

1. Motivation

EROFS(Enhanced Read-Only File System)是 Linux 内核中的高性能只读文件系统,现已被 Android(system 分区默认格式)、containerd(erofs snapshotter,OCI 镜像 +erofs 层)、openEuler 等广泛采用。它以 4KiB 块为粒度压缩,与 SquashFS(128KiB 块)相比:

  • 压缩/解压粒度小,随机读取开销低,更适合容器与微虚拟机场景(如 Firecracker 只读 rootfs);
  • 支持 tail-packing 内联(小文件尾部数据直接内联在 inode 之后,零额外块占用);
  • 支持 chunk-based 去重寻址(容器镜像层共享数据块)。

DragonOS 需要一个可靠的只读文件系统作为 rootfs / 容器镜像层 / 最小镜像的载体。本项目直接复用开源社区已有的纯 Rust 实现 erofs-rs(erofs-sys crate,no_std),在 DragonOS 内核中实现 VFS 适配层,最小化新代码量。

2. On-Disk Format

EROFS 镜像(v1)由以下区域组成。所有整数为小端。Superblock 位于设备偏移 1024 字节处,共 128 字节。

参考:linux-6.6.21/fs/erofs/erofs_fs.h(磁盘格式权威定义)、erofs-rs/erofs-sys/src/superblock.rs(解析实现)。

┌───────────────────────────────────────────────────────────┐
│  Superblock   128 字节 @ offset 1024,magic 0xE0F5        │
│  magic / checksum / feature_compat / blkszbits /          │
│  root_nid / inos / build_time / blocks / meta_blkaddr /   │
│  xattr_blkaddr / uuid / feature_incompat / extra_devices  │
├───────────────────────────────────────────────────────────┤
│  Device Table   可选(多设备镜像,erofs_deviceslot 128B)  │
├───────────────────────────────────────────────────────────┤
│  Data Blocks   文件数据区(compressed / uncompressed)    │
├───────────────────────────────────────────────────────────┤
│  Inode Table   nid = meta_blkaddr*blksz + nid*32         │
│                Compact 32B | Extended 64B(槽位固定 32B)│
├───────────────────────────────────────────────────────────┤
│  Directory Blocks  目录块(见下)                          │
├───────────────────────────────────────────────────────────┤
│  Shared Xattr Area  可选(共享 xattr 前缀/条目)           │
└───────────────────────────────────────────────────────────┘

Inode 格式(erofs_fs.h 中 erofs_inode_compact / erofs_inode_extended,解析见 erofs-rs/erofs-sys/src/inode.rs):

i_format(u16)位域:bit 0 = 版本(0=Compact 32B,1=Extended 64B);bit 1-3 = 数据布局(datalayout):

值 布局 说明
0 FLAT_PLAIN 平坦数据块(i_u = 起始块地址)
1 COMPRESSED_FULL 全压缩(非紧凑索引)
2 FLAT_INLINE 平坦 + 尾部数据内联在 inode 后
3 COMPRESSED_COMPACT 紧凑压缩索引
4 CHUNK_BASED 分块索引寻址(支持多设备/去重)

Compact inode 无时间戳(继承 superblock 的 build_time),uid/gid 16 位,文件最大 4GiB;Extended 有 i_mtime、32 位 uid/gid、u64 文件大小。

目录格式(erofs_fs.h 中 erofs_dirent,解析见 erofs-rs/erofs-sys/src/dir.rs):

每个目录块头部为定长的 DirentDesc 数组(12 字节/条:nid: u64 + nameoff: u16 + file_type: u8 + reserved: u8),文件名从块尾部倒序存放,nameoff 指向名字起始位置。

┌──────────┬──────────┬─────────┬──────────┬─────────┐
│ Dirent 0 │ Dirent 1 │   ...   │  name.. │ name..  │
│ 12B      │ 12B      │   ...    │ (倒序)  │ (倒序)  │
└──────────┴──────────┴─────────┴──────────┴─────────┘

3. Core Structs

erofs-rs 侧(erofs-sys crate,直接复用,不修改)

ImageFileSystem<B>(erofs-sys/src/superblock/file.rs)— 镜像文件系统的核心实现:

pub struct ImageFileSystem<B> where B: FileBackend {
    backend: B,             // 数据后端(DragonOS 侧提供)
    infixes: Vec<XAttrInfix>,   // xattr 长前缀表
    sb: SuperBlock,             // 解析后的 superblock
    device_info: DeviceInfo,    // 设备表(多设备镜像)
}
impl ImageFileSystem<B> {
    pub fn try_new(backend: B) -> PosixResult<Self>;  // 从 offset 1024 读并解析 superblock
}

SuperBlock(superblock.rs)— blksz() / blocks() / inos() / root_nid(pub)/ build_time(pub),内部提供 iloc(nid)(inode 槽位定位)、blk_access()(跨块边界访问)。

InodeInfo(inode.rs)— 磁盘 inode 的统一内存表示,enum { Compact(..), Extended(..) },提供 file_size() / inode_type() / inode_perm() / spec() / nlink() / uid() / gid() / mtime()。spec() 把 i_u 联合体解析为 Spec::{RawBlk(u32), Chunk(ChunkFormat), Device(u32), CompressedBlocks(u32)}。通过 InodeInfo::try_from((&dyn FileSystem<I>, Nid)) 从磁盘读取。

DirentDesc / Dirent(dir.rs)— 目录条目:nid: u64、file_type: u8、dirname() -> &[u8]。

DragonOS 侧(新增,kernel/src/filesystem/erofs/)

/// 块设备适配器:把 DragonOS BlockDevice 变成 erofs-sys 的 Source
pub struct BlockDevSource { dev: Arc<dyn BlockDevice> }

/// 新类型包装,绕过孤儿规则为 UncompressedBackend 实现 FileBackend
pub(crate) struct BlockDevBackend(UncompressedBackend<BlockDevSource>);

/// 文件系统实例
pub struct ErofsFileSystem {
    inner: ImageFileSystem<BlockDevBackend>,   // erofs-sys 核心
    root_inode: Arc<ErofsInode>,
    inode_cache: RwLock<BTreeMap<Nid, (InodeId, Weak<ErofsInode>)>>,  // nid → inode 缓存
    blk_dev: Arc<dyn BlockDevice>,             // 持有设备,防止卸载
    next_ino: AtomicUsize,                     // DragonOS InodeId 分配器
}

/// VFS inode
pub struct ErofsInode {
    info: InodeInfo,          // erofs-sys 解析结果
    ino: InodeId,             // DragonOS inode id
    nid: Nid,
    fs: Weak<ErofsFileSystem>, // 回指(Weak 防循环引用)
}

/// 内部桥接:临时构造,实现 erofs-sys 的 Inode trait,用于调用 mapped_iter 等
pub(crate) struct ErofsSysInode { info: InodeInfo, nid: Nid, xattrs: XAttrSharedEntries }

4. Core Traits That Must Be Implemented

只需实现 erofs-sys 的 3 个 trait(约 400 行),其余复用 ImageFileSystem;再实现 DragonOS VFS 的 3 个 trait 完成接入。

4.1 erofs-sys trait(kernel/src/filesystem/erofs/)

impl Source for BlockDevSource(source.rs)— 任意字节偏移读取:

impl Source for BlockDevSource {
    fn fill(&self, data: &mut [u8], _device_id: i32, offset: Off) -> PosixResult<u64> {
        // 1. 字节偏移 → LBA:blk = offset / LBA_SIZE, off_in_blk = offset % LBA_SIZE
        // 2. 整扇区读取:dev.read_at_sync(blk, blk_count, &mut raw_buf)
        // 3. 从扇区缓冲区拷贝 data.len() 字节到目标,返回实际拷贝数
    }
}

impl Backend/FileBackend for BlockDevBackend(fs.rs)— 透传 fill 到 UncompressedBackend<BlockDevSource>(新类型是必须的:FileBackend 的 blanket impl 是 impl<T: Source> FileBackend for UncompressedBackend<T>,孤儿规则要求在自己的 crate 里包一层)。

impl Inode for ErofsSysInode(inode.rs)— 提供 info() / nid() / xattrs_shared_entries(),调用 erofs-sys 方法时从 ErofsInode 栈上临时构造,零堆分配。

4.2 DragonOS VFS trait

impl MountableFileSystem for ErofsFileSystem(挂载入口,参照 ext4/filesystem.rs:1438 的 Ext4MountData 模式):

register_mountable_fs!(ErofsFileSystem, EROFSFSMAKER, "erofs");

pub struct ErofsMountData { gendisk: Arc<GenDisk> }
impl FileSystemMakerData for ErofsMountData { fn as_any(&self) -> &dyn Any { self } }

impl MountableFileSystem for ErofsFileSystem {
    fn make_fs_with_flags(data: Option<&dyn FileSystemMakerData>, mount_flags: MountFlags)
        -> Result<Arc<dyn FileSystem + 'static>, SystemError> {
        // 1. downcast 得到 ErofsMountData → gendisk → BlockDevice
        // 2. let backend = BlockDevBackend(UncompressedBackend::new(BlockDevSource::new(dev)));
        // 3. let inner = ImageFileSystem::try_new(backend)?;      // 解析 superblock
        // 4. 读取根 inode:let root_info = InodeInfo::try_from((&inner, sb.root_nid))?;
        // 5. Arc::new_cyclic 构造 ErofsFileSystem(root_inode 持 Weak<fs>)
        // 6. 挂载完成后 forget(mount_guard)(设备生命周期贯穿挂载期)
    }
    fn make_mount_data(raw_data, source) -> ... {
        // 参照 Ext4MountData::from_source:
        //   user_path_at → 校验 BlockDevice 类型 → vcore::try_find_gendisk(dname)
    }
}

impl FileSystem for ErofsFileSystem(fs.rs):

方法 实现
root_inode() 返回缓存好的 root_inode
name() "erofs"
super_block() 从 inner.superblock() 映射:Magic::from_bits_truncate(0xE0F5)、bsize/frsize = blksz()、blocks、files = inos();bfree/bavail/ffree = 0(只读)
sync_fs() Ok(())(只读无需同步)
support_readahead() false(无 page cache 后端,压缩数据也无法线性预读)

impl IndexNode for ErofsInode(inode.rs):

方法 实现
read_at(offset, len, buf) fs.inner.mapped_iter(&sys_inode, offset) → 遍历 BufferMapIter 逐块 copy_from_slice,EOF 截断,越界返回 0
write_at(..) Err(SystemError::EROFS) — 只读
metadata() InodeInfo → Metadata(mode、size、uid/gid、mtime、nlink、file_type;Compact inode 的 mtime 用 sb.build_time 推导)
find(name) / find_bytes(name) fill_dentries 回调中匹配名字 → 命中后 get_or_create_inode(nid) 并停止遍历
list_entries() fill_dentries 回调中收集全部 DirectoryEntry
open() / close() / flush_file() 空操作(只读,无状态)

读路径数据流(read() 系统调用):

ErofsInode::read_at(offset, len, buf)
  └─ fs.inner.mapped_iter(&sys_inode, offset)        // erofs-sys:逻辑→物理映射
       └─ TempBufferMapIter::try_yield(map)          // 按 Map 逐段读取
            └─ backend.fill(&mut block, 0, map.physical_start)
                 └─ BlockDevSource::fill(...)        // 字节偏移 → 扇区对齐
                      └─ BlockDevice::read_at_sync(lba, count, buf)  // virtio-blk 等

4.3 文件布局(新增代码)

kernel/src/filesystem/
└── erofs/                   ★ 新建
    ├── mod.rs               # pub mod fs / inode / source / errno
    ├── errno.rs             # erofs_sys::Errno → SystemError 映射(EPERM/ENOENT/EIO/...,fallback EIO)
    ├── fs.rs                # ErofsFileSystem + BlockDevBackend + MountableFileSystem
    ├── inode.rs             # ErofsInode + ErofsSysInode + impl IndexNode
    └── source.rs            # BlockDevSource + impl erofs_sys::data::Source

kernel/Cargo.toml:
  erofs-sys = { path = "../../erofs-rs/erofs-sys" }   # 相对路径引用;不走 CONFIG_EROFS_FS 内核模式

kernel/src/filesystem/mod.rs:
  pub mod erofs;

kernel/src/filesystem/vfs/mod.rs:
  Magic 枚举新增 EROFS_MAGIC = 0xE0F5   # 参照现有 FAT_MAGIC/EXT4 等变体

5. Dependency: erofs-sys

  1. 纯 Rust no_std:#![no_std] + extern crate alloc,无 FFI、无 C 依赖,可直接编译进 DragonOS 内核
  2. 可运行:FUSE 版已可运行,Linux 内核 Rust-for-Linux 亦计划采用;接入成本远低于自行维护磁盘格式解析。
  3. 接入面小:磁盘格式解析(superblock/inode/dir/xattr)、地址映射(flat/chunk/内联)、迭代器全部由 erofs-sys 提供,DragonOS 只写适配层。

编译模式:不设 CONFIG_EROFS_FS=y,走 extern crate alloc 路径,erofs-sys 作为普通 path 依赖。

6. Limitations(阶段一范围)

限制 说明
仅未压缩镜像 使用 UncompressedBackend;lz4 等压缩镜像(mkfs.erofs -z lz4)阶段一不支持。压缩支持见下「阶段二:压缩支持」(需在 erofs-rs 补齐)
无 xattr as_sys_inode() 的 XAttrSharedEntries 为空;getxattr/listxattr VFS 回调不实现(Linux 参考 xattr.c)
单设备 device_id 参数忽略,不支持多设备镜像(extra_devices > 0 时挂载应报错拒绝)
无 mmap / page cache page_cache() 返回 None,map_pages/fault 走默认;只读 FS 影响有限
无 readahead support_readahead() = false;数据按需解压/读取
inode 缓存无上限 BTreeMap<Nid, Weak<..>> 不主动淘汰;阶段二可加 LRU(参照 Linux inode.c 的 inode cache 语义)

阶段二:压缩支持(需在 erofs-rs 补齐)

erofs-rs 目前仅有 UncompressedBackend(compression.rs 只有占位枚举、map.rs 的 MAP_ENCODED 常量已定义但无实现),压缩读取需要在 erofs-rs 中补齐:

  1. 压缩索引解析:COMPRESSED_FULL / COMPRESSED_COMPACT 两种 layout 的物理簇索引解析(逻辑块 → 压缩簇),对应 Linux zmap.c(z_erofs_map_blocks_iter);
  2. CompressedBackend:读物理簇 → 解压 → 输出解压后数据,MapIter 增加 MAP_ENCODED 路径(对应 Linux zdata.c 的 z_erofs_decompress);
  3. 解压器抽象:按 superblock available_compr_algs 位图分派(对应 Linux decompressor.c),挂载时对不支持的算法明确拒绝(EUCLEAN/ENOTSUP)。

解压器选用 ruzstd:

(a) Widely adopted (57,012,877 all-time downloads);
(b) Pure-Rust implementation (no FFI / no C library);
(c) Full no_std support;
(d) Focused on decompression (compression available but limited) —— EROFS 只需要解压,恰好匹配。

注:EROFS 原生算法为 LZ4(默认)/ LZMA(5.16+)/ DEFLATE(6.6+),Zstandard 是 Linux 6.10+ 新增(6.6.21 中尚不存在)。采用 ruzstd 即走 zstd 压缩簇路线:测试镜像用 mkfs.erofs -z zstd 生成,DragonOS 由此直接获得 zstd 压缩能力;阶段二后续可再按需补 lz4 解压器。

阶段二其他候选:chunk-based 去重文件、mmap、xattr、多设备。

7. Testing

7.1 集成测试

# 镜像准备(erofs-utils)
mkdir -p /tmp/erofs_root/subdir
echo "hello from erofs!" > /tmp/erofs_root/hello.txt
mkfs.erofs erofs.img /tmp/erofs_root/             # 阶段一:未压缩镜像
mkfs.erofs -z zstd erofs-zstd.img /tmp/erofs_root/ # 阶段二:zstd 压缩镜像(ruzstd)

7.2 dunitest

参照 user/apps/tests/dunitest/ ext4 测例模式,新增 erofs_basic.cc(gtest),覆盖 mount/stat/statfs/open+read/readdir/readlink/write→EROFS。内核侧 #[cfg(test)] 单测覆盖 superblock 解析。

7.3 erofs-sys 自身

cd erofs-rs && cargo test

8. 验收标准

  • mount -t erofs /dev/vdX /mnt/erofs 成功,umount 成功;
  • 未压缩镜像下:文件读写路径正确(read_at 跨块、EOF 截断、尾部内联 FLAT_INLINE);
  • 目录操作正确(list_entries、find、符号链接);
  • metadata/statfs 返回正确的 mode/size/uid/gid/mtime 与 magic 0xE0F5;
  • 写操作返回 EROFS;多设备镜像挂载被明确拒绝(EUCLEAN/ENOTSUP),不 panic;(阶段一)压缩镜像同样被明确拒绝;
  • dunitest erofs_basic 全绿;
  • (阶段二)mkfs.erofs -z zstd 压缩镜像可正常挂载与读取,未压缩镜像不受影响;含不支持算法的镜像被明确拒绝。

参考

  • EROFS 官方文档:https://erofs.docs.kernel.org/en/latest/
  • 磁盘格式(Linux 6.6.21):linux-6.6.21/fs/erofs/erofs_fs.h;挂载/解析参考 super.c、inode.c、data.c、dir.c、namei.c;压缩(阶段二)参考 zmap.c、zdata.c、decompressor.c
  • erofs-rs:erofs-sys/src/{superblock, inode, dir, map, data, operations}.rs
  • DragonOS 接入参考:kernel/src/filesystem/ext4/(挂载模式)、tools/run-qemu.sh(挂盘)、user/apps/tests/dunitest/(测例模式)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentation

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions