f2fs 工具详解
2631字约9分钟
2025-03-16
f2fs-tools
是 f2fs 文件系统配套的工具集合,包含了 mkf2fs
resize.f2fs
dump.f2fs
等模块。 本文记载了当前的 f2fs-tools
相关解析。
背景知识
Brief Introduction
F2FS 是一款针对 flash 设备的日志型文件系统,能够极好的利用flash设备的性能优势,实验表明相比于传统的日志型文件系统有着更好的性能表现。
On-disk Layout
F2FS将整个卷分成六个区域,如下所述:
SuperBlock (SB)
- 位于F2FS文件系统的开头,具有双副本保证数据崩溃一致性。它包含 F2FS 的基本分区信息(磁盘大小,每个区域的起始位置)和一些默认参数。
- F2FS执行挂载操作的时候,会直接读取头部的superblock信息到内存中,转换成
struct f2fs_sb_info
,该结构包括了锁结构以及其他区域的内存管理结构。
CheckPoint (CP)
struct f2fs_checkpoint { __le64 checkpoint_ver; __le64 user_block_count; /* # of user blocks */ __le64 valid_block_count; /* # of valid blocks in main area */ __le32 rsvd_segment_count; /* # of reserved segments for gc */ __le32 overprov_segment_count; /* # of overprovision segments */ __le32 free_segment_count; /* # of free segments in main area */ /* information of current node segments */ __le32 cur_node_segno[MAX_ACTIVE_NODE_LOGS]; __le16 cur_node_blkoff[MAX_ACTIVE_NODE_LOGS]; /* information of current data segments */ __le32 cur_data_segno[MAX_ACTIVE_DATA_LOGS]; __le16 cur_data_blkoff[MAX_ACTIVE_DATA_LOGS]; __le32 ckpt_flags; /* Flags : umount and journal_present */ __le32 cp_pack_total_block_count; /* total # of one cp pack */ __le32 cp_pack_start_sum; /* start block number of data summary */ __le32 valid_node_count; /* Total number of valid nodes */ __le32 valid_inode_count; /* Total number of valid inodes */ __le32 next_free_nid; /* Next free node number */ __le32 sit_ver_bitmap_bytesize; /* Default value 64 */ __le32 nat_ver_bitmap_bytesize; /* Default value 256 */ __le32 checksum_offset; /* checksum offset inside cp block */ __le64 elapsed_time; /* mounted time */ /* allocation type of current segment */ unsigned char alloc_type[MAX_ACTIVE_LOGS]; /* SIT and NAT version bitmap */ unsigned char sit_nat_version_bitmap[1]; } __packed;
f2fs_checkpoint
: 包含检查点信息,记录了分配到了哪一个数据块,NAT/SIT 有效位图等orphan node
: 记录孤立的节点列表active segment
: 正常模式下就是六块区域。记录正在进行数据分配的 segment,又称 current segment, CURSEG) 的 summary entries。下面展示了当前正在进行分配的segment的数据/* for active log information */ struct curseg_info { struct mutex curseg_mutex; /* lock for consistency */ struct f2fs_summary_block *sum_blk; /* cached summary block */ struct rw_semaphore journal_rwsem; /* protect journal area */ struct f2fs_journal *journal; /* cached journal info */ unsigned char alloc_type; /* current allocation type */ unsigned short seg_type; /* segment type like CURSEG_XXX_TYPE */ unsigned int segno; /* current segment number */ unsigned short next_blkoff; /* next block offset to write */ unsigned int zone; /* current zone number */ unsigned int next_segno; /* preallocated segment */ bool inited; /* indicate inmem log is inited */ };
Segment Information Table (SIT)
为 f2fs_sit_entry
的集合,每个 entry 指示了该 segment 的有效 blocks。一个block里面包含了55个entry是因为4KB对齐。vblock
代表了有多少个block被使用,valid_map
是一个大小为64字节的位图,标记了一个segment包含的512个block的使用情况,mtime
表示该segment最后修改的时间
struct f2fs_sit_entry {
__le16 vblocks; /* reference above */
__u8 valid_map[SIT_VBLOCK_MAP_SIZE]; /* bitmap for valid blocks */
__le64 mtime; /* segment age for cleaning */
} __attribute__((packed));
Node Address Table (NAT)
f2fs_nat_entry
的集合,每个 entry 指示了 ino 对应的 block_addr。struct f2fs_nat_entry { __u8 version; /* latest version of cached nat entry */ __le32 ino; /* inode number */ __le32 block_addr; /* block address */ } __attribute__((packed));
Segment Summary Area (SSA)
每个 segment 对应一个f2fs_summary_block
struct f2fs_summary_block {
struct f2fs_summary entries[ENTRIES_IN_SUM];
struct f2fs_journal journal;
struct summary_footer footer;
};
f2fs_summary 为物理地址(通过第 # 块 block[物理地址] 可以定位到一一对应的 f2fs_summay)到逻辑地址(nid + ofs_in_node)的映射。
struct f2fs_summary { __le32 nid; /* parent node id */ union { __u8 reserved[3]; struct { __u8 version; /* node version number */ __le16 ofs_in_node; /* block index in parent node */ } __attribute__((packed)); }; } __attribute__((packed));/
journal,为了减少对 NAT/SIT 的频繁更新,有一部分操作暂存在此处,在到达检查点后写入。所以在 f2fs.resize 工具中,对文件系统的更改需要先调用
flush_journal_entries
函数进行刷新。
struct f2fs_journal {
union {
__le16 n_nats;
__le16 n_sits;
};
/* spare area is used by NAT or SIT journals or extra info */
union {
struct nat_journal nat_j;
struct sit_journal sit_j;
struct f2fs_extra_info info;
};
} __packed;
footer 具有 .check_sum 与 .type,其中 .type 是 segment 的类型枚举:
struct summary_footer { unsigned char entry_type; /* SUM_TYPE_XXX */ __le32 check_sum __attribute__((packed)); /* summary checksum */ }; enum { CURSEG_HOT_DATA = 0, /* directory entry blocks */ CURSEG_WARM_DATA, /* data blocks */ CURSEG_COLD_DATA, /* multimedia or GCed data blocks */ CURSEG_HOT_NODE, /* direct node blocks of directory files */ CURSEG_WARM_NODE, /* direct node blocks of normal files */ CURSEG_COLD_NODE, /* indirect node blocks */ NO_CHECK_TYPE };
Main Area
F2FS 将整个卷划分为多个 segments,每个 segment 的大小固定为2MB。一个 section 由连续的 segments 组成,zone 由多个 sections 组成。默认情况下,zone = section = segment,但用户可以通过 mkfs
修改这些大小。
shadow copy mechanism
F2FS 通过检查点机制来维护文件系统一致性。F2FS仅使用两份 CP,其中一个总是指示最后一个有效数据。对于文件系统一致性,每个 CP 指向 NAT 和 SIT 副本有效的位置,如下图所示:
在 F2FS 小工具中, NM_I(sbi)->nat_bit_map
的第 i 位指示了第 i 个 NAT block 使用的是对应的 segment #0/#1。
f2fs.resize 主流程
resize.f2fs [ -t target sectors ] [ -d debugging-level ] device
f2fs.resize
在 fsck/main.c#533 将参数解析到名为 c 的全局f2fs_configuration
中挂载 F2FS,将 super block 以及其他部分元数据 cache 至 sbi 中
根据 c 中的 taget_sectors 与 super block 中的 block count 决定是缩容还是扩容
扩容:
- 刷新 journal entries
- 通过 get_new_sb 函数构建新 super block
- 检验 nat 大小,确定是否缩容
- 优先通过整理碎片空间来迁移
- 若碎片空间不够,则迁移 main 区域内容
- 根据
get_seg_entry
获得所有 main segments 信息 - 对 segment 中有效的 block 进行迁移
- 获取 block 对应的 summary 信息
- 如果是数据 block,调用
update_data_blkaddr
进行更新 - 否则是 node block,调用
update_nat_blkaddr
进行更新 - 注意这里的
update_data_blkaddr
更新的 SIT 区域是更新在 sbi-cache 中的,而update_nat_blkaddr
是直接更新 f2fs-nat 区域的。
- 根据
- 迁移 ssa 区域内容
- 这里主要点在于根据前后块的位置关系从前往后迁移或者从后往前迁移
- 迁移 nat 区域内容
- 将全部的 NAT 块迁移至新 NAT#0 中,同时清空 NAT 指示位图
- 将扩充的 NAT 区域用零块填充
- 迁移 sit 区域内容
- 清空原 sit 区域
- 直接通过 cache 在 sbi 中的信息重建 sit 区域(在此之前,对 SIT 的更新均是在 cache 中),偏移量为 offset
- 重建检查点
- 更新 super block
缩容:
刷新 journal entries
整理碎片:
这里的 main 区域缩容实则是将 main 区域的碎片进行了整理,代码需要扫描整个 main 区域。
我们的缩容同样需要考虑缩容的 1GB 都是碎片的情况。
更新 super block
重建检查点
f2fs-tools 中可能能利用的函数
sbi 相关
sbi 是工具挂载文件系统时 cache 下的数据结构。
对文件系统进行修改时,如果是 sbi cache 下的内容,尽可能优先修改/读取 sbi cache,最后再落盘至 f2fs。如果不是 sbi cache 的内容,则直接修改盘。:>
文件系统修改操作最繁杂的部分就是要利用旧的分区信息来完成一个新分区文件系统的修改。
F2FS_RAW_SUPER(struct f2fs_sb_info *sbi) -> raw_super_block *
通过 @sbi 获取 raw_super_block 结构的指针。
NM_I(struct f2fs_sb_info *sbi) -> nm_i *
通过 sbi 获取 node manager 的信息,其中管理了 max_nid
,有效位图信息等。
GET_SEGNO(f2fs_sb_info *sbi, u64 blk_addr) -> u64
地址为 blk_addr 块是 Main 区域的第 # 个 segment。
OFFSET_IN_SEG(f2fs_sb_info *sbi, u64 blk_addr) -> u64
地址为 blk_addr 块在是该 segment 的第 # 块。
get_sb(#member)
宏
取 sb 字段用,入参为字段名,即取代 sb->member 的写法,宏使用需要 super block 结构指针的名字为 sb。
get_newsb, get_cp, set_sb, set_cp 同理。
至于为什么要使用 get_sb 而不直接 sb->member,其实是类型转换的语法糖 (例如 __le32 -> uint32_t)。
get_seg_entry(f2fs_sb_info *sbi, uint i) -> seg_entry *
获取 main 区域第 i 个 segment 的信息 entry(cached),包含有效 blocks 数量、有效位图、类型、检查点等信息。
封装的比较完备的函数
get_sum_block(f2fs_sb_info *sbi, uint segno
,
int *ret_type) -> f2fs_summary_block *
获取第 segno 个 segment(从 main 记) 的 summary_block。根据 ret_type 为SEG_TYPE_DATA | SEG_TYPE_NODE | SEG_TYPE_MAX 时,需要对返回值进行 free。
该函数需要读取 F2FS 的 SSA 块信息。
get_sum_entry(f2fs_sb_info *sbi, u32 blk_addr,
f2fs_summary *sum_entry) -> int
获取 blk_addr 的 summary 信息放入 sum_entry 的地址中,具有 nid, ofs_in_node 等信息。
该函数需要读取 F2FS 的 SSA 块信息。
find_next_free_block(struct f2fs_sb_info *sbi, u64 *to, int left, int want_type, bool new_sec) -> int
查找下一个空闲块,返回值为 0 查找成功:
left 在这里其实作为 bool 值使用比较合适:
- true: [main_blkaddr, min(*to, end_blkaddr)) 区域向左查找 free block
- false: [*to, end_blkaddr) 向右查找 free block,要求 *to >= main_blkaddr
want_type: 若 segment type 一致则可查找,若是空 segment,会设置 sbi 中的 seg_entry 的 type 为 want_type
new_sec: 要求查找的 block 为空 segment 里的
f2fs_defragment(f2fs_sb_info *sbi, u64 from, u64 len,
u64 to, int left) -> int
碎片整理,将 [from, from + len) 的 blocks 整理到区域的空闲块中:
- true: [main_blkaddr, min(*to, end_blkaddr)) 区域向左整理
- false: [*to, end_blkaddr) 向右整理,要求 *to >= main_blkaddr
⚠️ 这里的 main_blkaddr 均是 sbi 中的老 sb 信息,所以说并不太适用我们的情况,但稍做修改应该问题不大。
NAT 相关
NAT_BLOCK_OFF
与 get_nat_block_addr
是没被封装,但是重复出现的代码段。
NAT_BLOCK_OFF(nid_t nid) -> pgoff_t
根据 nid 获得 NAT 中逻辑块偏移,逻辑即 0(#0#1) 1(#0#1) 2(#0#1) ...
return nid / NAT_ENTRY_PER_BLOCK;
GET_NAT_BLOCK_ADDR(f2fs_sb_info *sbi, pgoff_t nat_block_off)
-> pgoff_t
根据 NAT 逻辑块偏移获得 block_addr(#0)。如若需要 #1 的 block_addr,另外 + sbi->blocks_per_seg.
pgoff_t get_nat_block_addr(pgoff_t nat_block_off) {
return (pgoff_t)(old_nat_blkaddr +
(seg_off << sbi->log_blocks_per_seg << 1) +
(block_off & ((1 << sbi->log_blocks_per_seg) - 1)));
}
F2FS 读写操作
关于文件系统的实际操作,最基本的接口为 dev_read_block
与 dev_write_block
。
migrate_block(f2fs_sb_info *sbi, u64 from, u64 to)
在 fsck/defrag.c#12 中有一个给碎片整理使用的迁移 MAIN 区域 block 的本地函数,不能直接调用,但可以从中了解一下迁移块时所需要遵循的一下约束。
dev_read_block(void *buf, u64 blk_addr)
将文件系统第 @blk_addr 块读取至 @buf 地址中,其中 @blk_addr 为块地址(文件系统的第 # 块)。
dev_write_block(void *buf, u64 blk_addr)
将 @buf 地址中的内容写至文件系统第 @blk_addr 块中,其中 @blk_addr 为块地址(文件系统的第 # 块)。
参考
代码
- https://git.kernel.org/pub/scm/linux/kernel/git/jaegeuk/f2fs-tools.git (f2fs 工具)
文档参考
- https://docs.kernel.org/filesystems/f2fs.html
- https://www.usenix.org/conference/fast15/technical-sessions/presentation/lee
- https://github.com/RiweiPan/F2FS-NOTES
- https://manpath.be/f30/8/resize.f2fs