为什么需要 F2FS?传统的文件系统(如 FAT32, EXT4)是为“旋转的磁碟(HDD)”设计的,而 F2FS 是为“闪存(NAND Flash)”设计的。
在存储领域,传统的文件系统如 FAT32 或 EXT4 最初都是为机械硬盘这种“旋转磁碟”设计的。这类设备拥有物理磁头,在磁道上移动时可以直接覆盖旧数据。然而,F2FS 则是专门为 NAND 闪存设计的,它深刻理解闪存与磁盘在物理特性上的巨大差异。闪存最核心的限制在于它无法像磁碟那样进行原地“覆盖写”。一旦一个存储位置写入了数据,必须先执行擦除操作才能再次写入。更棘手的是,闪存写入的最小单位通常很小,只有几 KB 左右的“页”,但擦除的最小单位却非常大,往往达到数 MB 级别的“块”。如果文件系统像处理磁盘那样频繁地在同一个逻辑地址修改数据,底层的闪存就不得不不断执行繁重的“读-改-写”循环,这会导致整体写入性能随时间推移而急剧下降。
为了掩盖闪存这些古怪的物理特性,所有的闪存设备,无论是你手中的 SD 卡还是高性能的 SSD,内部都内置了一个微控制器来运行闪存转换层,也就是 FTL。FTL 的本质是一个欺骗机制,它让操作系统觉得这块闪存依然是一个可以随意覆盖写的普通磁盘。然而,当传统文件系统进行大量的随机写入时,FTL 必须在后台疯狂地进行“垃圾回收”和“磨损均衡”,试图在不断变化的物理空间中寻找空白区域来安置这些新数据。由于 FTL 并不了解文件系统的上层布局,它的这种盲目搬运效率极低。这就是为什么很多嵌入式设备或智能手机在使用一段时间后会变得卡顿,其深层原因往往是文件系统的随机写入行为让底层的 FTL 陷入了几乎崩溃的处理链条中。
F2FS 解决这一难题的核心武器是采用了日志结构文件系统 ,即 LFS 架构。我们可以做一个形象的比喻:传统文件系统就像一块黑板,哪里写错了就擦掉哪里,然后再原地重写;而 F2FS 则像是一个永远不使用橡皮擦的笔记本,无论你是新增文件还是修改旧文件,它永远不会去动之前写过的内容,而是不断在笔记本最后一页的空白处按顺序往后写。这种“追加式写入”的做法将所有的逻辑修改转换成了物理上的顺序写。对于底层闪存来说,顺序写入是最完美的运行模式,它极大地减轻了 FTL 的管理压力,让后台垃圾回收变得非常简单高效,同时也因为数据分布更加均匀而天然地实现了磨损均衡,从而显著延长了昂贵闪存硬件的寿命。
虽然 LFS 架构的概念很早就有,但早期的实现一直被“漫游树(wandering tree)”这一难题所困扰,导致无法大规模商用。在传统的 LFS 中,如果你修改了一个数据块,由于它必须写到新位置,物理地址就变了。这意味着指向这个块的上一级索引节点也必须修改地址来指向它,而索引节点的地址改变又会触发上上级节点的修改,最终这种递归更新会一直蔓延到文件系统的根节点,造成巨大的额外开销。**F2FS 的绝活在于它引入了节点地址表,也就是 NAT。**NAT 像一个稳固的中转站,它给每个索引节点分配一个固定的 ID,并记录这个 ID 目前对应的物理地址。当一个数据块移动时,F2FS 只需要更新 NAT 表里对应的一个条目即可,彻底切断了向上递归的“漫游”效应。正是这个核心发明,让 F2FS 能够保持 LFS 的高性能,又避开了复杂的连锁更新,成为了现代闪存存储的基石。
总体结构相对于传统的日志结构型文件系统,F2FS 在 wandering tree 和 gc 的高时间开销等问题,有一定的改进和优化。
wandering tree 问题: 在传统的 LFS 中,由于采用异地更新(Out-of-place Update),当一个数据块被修改并写到新地址时,其父节点的指针也必须随之更新以指向新地址。这种更新会沿着文件系统的索引树一路向上递归,直到根节点。这种现象会导致极高的元数据(Metadata)写入开销,原本只改一个字节,最后可能导致整个树路径上的所有块都要重写。F2FS 通过引入 **NAT(Node Address Table)**层,成功切断了这种递归关联,实现了“不动声色”的地址转换。 高 gc 开销问题: LFS 随着时间推移会产生大量无效数据(旧版本的块)。当磁盘空间不足时,必须启动垃圾回收。传统的 LFS 往往在 GC 时面临不可预知的长延迟,严重影响实时性能。F2FS 通过 **Multi-head Logging(多头日志写入)**和多种 GC 策略(贪婪算法与成本收益算法),根据数据的冷热程度进行分类存储,极大地降低了 GC 时的有效数据搬迁量,减少了系统的波动性。 系统特性 基本数据单位分为四类:
**Block(块):**F2FS 读写操作的最小原子单位,大小固定为 4KB 。这一设计与 Linux 的 Page Cache 以及大多数闪存设备的 Page 大小高度契合。所有的元数据结构和用户数据均以 Block 为单位组织。 **Segment(段):**管理 Block 的基本容器,大小为 2MB (包含 512 个 Block)。Segment 是 F2FS 进行空间管理和分配的基石。之所以选择 2MB,是为了适配主流闪存芯片的物理擦除块(Erase Block)大小,从而减少擦除时的干扰。 **Section(节):**由连续的若干个 Segment 组成,默认情况下 1 Section = 1 Segment 。Section 是垃圾回收(GC)的物理操作单位。F2FS 会根据数据的冷热属性,将数据分配到不同的 Section 中,以便在 GC 时能以更高的效率释放整块空间。 **Zone(区):**由多个 Section 组成。在某些高级配置或特定硬件(如具有隔离特性的闪存阵列)上,Zone 用于进一步的物理隔离,以防止不同类型的数据在物理层面上产生写干扰。 LFS 异地更新特性F2FS 遵循“永远不要覆盖旧数据”的原则。这种**异地更新(Out-of-place Update)**策略是 F2FS 性能和可靠性的根源:
**传统就地更新(In-place):**如 FAT/EXT4。当修改文件 A 的第 1 块时,直接在原物理位置重写。对于闪存,这会触发底层昂贵的“读取-擦除-修改-写回”过程。 **F2FS 异地更新:**当修改发生时,F2FS 直接在当前的 Log 尾部找一个干净的物理块写入新数据。旧块被标记为“Invalid(无效)”,留待未来的 GC 处理。 优点:
**写聚合:**将碎片化的随机写转换为大吞吐量的顺序写,极大提升 IOPS。 **磨损均衡:**数据自动在全盘范围内散布,避免了因频繁更新特定元数据(如文件表)导致的局部闪存颗粒提前损坏。 这样做的问题是:这种设计依赖 **Checkpoint(检查点)**机制来保证一致性。F2FS 不会像 JFS(日志文件系统)那样在每次操作时写日志,而是定期创建快照。这意味着如果突发断电且此时还没到 Checkpoint 时刻,可能会丢失极短时间内的数据。
Multi-head Logging(多头日志写入)特性F2FS 的核心精髓在于它能感知数据的“温度”。Log 区域指的是文件系统中用于分配 free block(空闲的且没有写入数据的 block)的区域,例如 F2FS 的一个文件需要写入新数据,它就要去 Log 区域请求 free block,然后再将数据写入这个 free block 中。传统的 LFS 往往会维护一个大的日志区域,一切数据的分配都从这个大的日志区域中进行处理。它同时维护了 6 个活动的 Log 区域(活跃段),将数据精准分类:
节点类(Node):存储文件的索引结构 HOT NODE :目录文件的直接索引块(Direct Node)。由于目录操作极其频繁且对用户感知影响大,将其单独存放以加快访问速度。WARM NODE :普通文件的直接索引块。COLD NODE :间接索引块(Indirect Node)。这些块通常属于超大文件,修改频率相对较低。数据类(Data):存储用户实际内容 HOT DATA :目录项数据(Dentry)。目录下的文件名、inode 号等信息经常变动。WARM DATA :普通文件的内容。这是系统中最主要的数据流。COLD DATA :多媒体文件、只读文件或 GC 搬迁产生的数据。这类数据一旦写入,很难再次被修改。 闪存设备物理布局
通过 mkfs.f2fs 格式化后,整个存储空间被划分为六大功能区。除了主数据区外,前五个区域统称为元数据区。
Superblock(超级块,SB) 位于分区的最开始,存储了文件系统的关键几何参数(块大小、段数量、区域边界)。为了保证安全性,F2FS 会保存两个 SB 备份。在挂载(Mount)时,驱动首先读取 SB 并初始化内存中的 f2fs_sb_info 结构。
Checkpoint(检查点,CP) 这是系统的“大脑”。CP 记录了当前文件系统的最新一致状态。它包含了两份备份(A/B 切换写入),以防在写 CP 过程中突然断电。CP 存储了 NAT 和 SIT 的位图快照、当前活跃段的列表以及 Orphan Inode(孤儿节点)等信息。在 RTEMS 重启后,挂载过程会根据 CP 来快速恢复整个文件系统的逻辑拓扑。
Segment Information Table(段信息表,SIT) SIT 负责监控 Main Area 中每个 Segment 的使用情况。它包含一个位图,指示该段内 512 个块中哪些是有效的(Valid)。主要作用是 SIT 是 GC 决策的关键依据。系统通过查询 SIT 找到那些无效块最多的段进行回收。
Node Address Table(节点地址表,NAT) 建立了一张表保存了每一个 node 的物理地址信息。这是 F2FS 解决漫游树问题的秘密武器。
**机制:**每个 Node(索引节点)都有一个唯一的逻辑 ID(NID)。NAT 就像一个巨大的映射表,记录了 NID -> 物理地址 的转换。 **优势:**当文件数据更新导致 Node 本身位置发生移动时,只有 NAT 表中的物理地址需要更新,而指向该 NID 的父节点无需改动。这大大减少了元数据连锁更新的负担。 Segment Summary Area(段摘要区,SSA) 这个区域主要保存了 jounal(SIT/NAT 临时的修改信息)以及 summary(记录了逻辑地址和物理地址关系的结构,主要用于 GC)。SSA 区域在内存中没有专门的数据结构。SSA 存储了块的“身份证明”。
**反向查找:**SSA 记录了主数据区每个物理块对应的父 Node ID 及其在文件内部的偏移。 **主要用途:**在 GC 搬迁数据时,系统必须确定这个物理块到底是哪个文件的,SSA 提供了这种反向检索能力,确保搬迁后能正确更新对应的索引指针。 Main Area(主数据区) 这是文件系统最广阔的腹地,所有的用户数据、目录项、以及各种 Node 块(Inode 块、索引块)都存储在这里。所有的块分配都遵循前面提到的 6 个 Log 区域的冷热分类策略。
SuperBlock 区域SuperBlock 区域的结构如下:
Superblock区域是由两个 struct f2fs_super_block 结构组成,互为备份。
f2fs_super_block 结构struct f2fs_super_block 定义了文件系统的几何参数和各功能区的物理偏移。我们可以观察到,F2FS 的许多设计都采用了 log2 的指数形式,例如 log_blocksize 和 log_blocks_per_seg。这种设计允许内核在计算物理地址时使用位移操作(Shift)代替昂贵的除法运算,这对于 CPU 资源有限的嵌入式设备(如运行 RTEMS 的环境)非常友好。此外,该结构体包含了一系列以 _blkaddr 结尾的字段,它们精确指向了 Checkpoint、SIT、NAT、SSA 以及 Main Area 的起始块地址。magic 字段(魔数)则用于标识这确实是一个 F2FS 卷,是驱动程序进行合法性检查的第一道关卡。定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 struct f2fs_super_block { __le32 magic; __le16 major_ver; __le16 minor_ver; __le32 log_sectorsize; __le32 log_sectors_per_block; __le32 log_blocksize; __le32 log_blocks_per_seg; __le32 segs_per_sec; __le32 secs_per_zone; __le32 checksum_offset; __le64 block_count; __le32 section_count; __le32 segment_count; __le32 segment_count_ckpt; __le32 segment_count_sit; __le32 segment_count_nat; __le32 segment_count_ssa; __le32 segment_count_main; __le32 segment0_blkaddr; __le32 cp_blkaddr; __le32 sit_blkaddr; __le32 nat_blkaddr; __le32 ssa_blkaddr; __le32 main_blkaddr; __le32 root_ino; __le32 node_ino; __le32 meta_ino; __u8 uuid[16 ]; __le16 volume_name[MAX_VOLUME_NAME]; __le32 extension_count; __u8 extension_list[F2FS_MAX_EXTENSION][F2FS_EXTENSION_LEN]; __le32 cp_payload; __u8 version[VERSION_LEN]; __u8 init_version[VERSION_LEN]; __le32 feature; __u8 encryption_level; __u8 encrypt_pw_salt[16 ]; struct f2fs_device devs [MAX_DEVICES ]; __le32 qf_ino[F2FS_MAX_QUOTAS]; __u8 hot_ext_count; __le16 s_encoding; __le16 s_encoding_flags; __u8 s_stop_reason[MAX_STOP_REASON]; __u8 s_errors[MAX_F2FS_ERRORS]; __u8 reserved[258 ]; __le32 crc; } __packed;
f2fs_sb_info 结构当 RTEMS 成功从磁盘读取了原始的超级块数据后,它并不会直接频繁操作磁盘上的那段字节流,而是会初始化一个内存管理结构体 struct f2fs_sb_info(简称 SBI)。SBI 是 F2FS 在运行时的“指挥部”,它不仅包含了原始超级块的指针 raw_super,还整合了节点管理器(NM)、段管理器(SM)以及用于保证线程安全的各类锁机制(如 sb_lock、gc_mutex)。对于移植工作而言,SBI 是最频繁被打交道的数据结构,它将静态的磁盘布局转换成了动态的内存对象,负责维护文件系统的实时挂载状态。struct f2fs_super_block 在内存中对应的结构是 struct f2fs_sb_info,它除了包含了struct f2fs_super_block的信息以外,还包含了一些额外的功能,如锁、SIT、NAT 对应的内存管理结构等。定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 struct f2fs_sb_info { struct super_block *sb ; struct proc_dir_entry *s_proc ; struct f2fs_super_block *raw_super ; struct f2fs_rwsem sb_lock ; int valid_super_block; unsigned long s_flag; struct mutex writepages ; #ifdef CONFIG_BLK_DEV_ZONED unsigned int blocks_per_blkz; unsigned int max_open_zones; unsigned int blkzone_alloc_policy; #endif struct f2fs_nm_info *nm_info ; struct inode *node_inode ; struct f2fs_sm_info *sm_info ; struct f2fs_bio_info *write_io [NR_PAGE_TYPE ]; struct f2fs_rwsem io_order_lock ; pgoff_t page_eio_ofs[NR_PAGE_TYPE]; int page_eio_cnt[NR_PAGE_TYPE]; struct f2fs_checkpoint *ckpt ; int cur_cp_pack; spinlock_t cp_lock; struct inode *meta_inode ; struct f2fs_rwsem cp_global_sem ; struct f2fs_rwsem cp_rwsem ; struct f2fs_rwsem node_write ; struct f2fs_rwsem node_change ; wait_queue_head_t cp_wait; unsigned long last_time[MAX_TIME]; long interval_time[MAX_TIME]; struct ckpt_req_control cprc_info ; struct cp_stats cp_stats ; struct inode_management im [MAX_INO_ENTRY ]; spinlock_t fsync_node_lock; struct list_head fsync_node_list ; unsigned int fsync_seg_id; unsigned int fsync_node_num; unsigned int max_orphans; struct list_head inode_list [NR_INODE_TYPE ]; spinlock_t inode_lock[NR_INODE_TYPE]; struct mutex flush_lock ; struct extent_tree_info extent_tree [NR_EXTENT_CACHES ]; atomic64_t allocated_data_blocks; unsigned int max_read_extent_count; unsigned int hot_data_age_threshold; unsigned int warm_data_age_threshold; unsigned int last_age_weight; unsigned int donate_files; unsigned int log_sectors_per_block; unsigned int log_blocksize; unsigned int blocksize; unsigned int root_ino_num; unsigned int node_ino_num; unsigned int meta_ino_num; unsigned int log_blocks_per_seg; unsigned int blocks_per_seg; unsigned int unusable_blocks_per_sec; unsigned int segs_per_sec; unsigned int secs_per_zone; unsigned int total_sections; unsigned int total_node_count; unsigned int total_valid_node_count; int dir_level; bool readdir_ra; u64 max_io_bytes; block_t user_block_count; block_t total_valid_block_count; block_t discard_blks; block_t last_valid_block_count; block_t reserved_blocks; block_t current_reserved_blocks; block_t unusable_block_count; unsigned int nquota_files; struct f2fs_rwsem quota_sem ; struct task_struct *umount_lock_holder ; atomic_t nr_pages[NR_COUNT_TYPE]; struct percpu_counter alloc_valid_block_count ; struct percpu_counter rf_node_block_count ; atomic_t wb_sync_req[META]; struct percpu_counter total_valid_inode_count ; struct f2fs_mount_info mount_opt ; struct f2fs_rwsem gc_lock ; struct f2fs_gc_kthread *gc_thread ; struct atgc_management am ; unsigned int cur_victim_sec; unsigned int gc_mode; unsigned int next_victim_seg[2 ]; spinlock_t gc_remaining_trials_lock; unsigned int gc_remaining_trials; unsigned long long skipped_gc_rwsem; unsigned int reserved_pin_section; unsigned short gc_pin_file_threshold; struct f2fs_rwsem pin_sem ; unsigned int max_victim_search; unsigned int migration_granularity; unsigned int migration_window_granularity; #ifdef CONFIG_F2FS_STAT_FS struct f2fs_stat_info *stat_info ; atomic_t meta_count[META_MAX]; unsigned int segment_count[2 ]; unsigned int block_count[2 ]; atomic_t inplace_count; atomic64_t total_hit_ext[NR_EXTENT_CACHES]; atomic64_t read_hit_rbtree[NR_EXTENT_CACHES]; atomic64_t read_hit_cached[NR_EXTENT_CACHES]; atomic64_t read_hit_largest; atomic_t inline_xattr; atomic_t inline_inode; atomic_t inline_dir; atomic_t compr_inode; atomic64_t compr_blocks; atomic_t swapfile_inode; atomic_t atomic_files ; atomic_t max_aw_cnt; unsigned int io_skip_bggc; unsigned int other_skip_bggc; unsigned int ndirty_inode[NR_INODE_TYPE]; atomic_t cp_call_count[MAX_CALL_TYPE]; #endif spinlock_t stat_lock; unsigned int data_io_flag; unsigned int node_io_flag; struct kobject s_kobj ; struct completion s_kobj_unregister ; struct kobject s_stat_kobj ; struct completion s_stat_kobj_unregister ; struct kobject s_feature_list_kobj ; struct completion s_feature_list_kobj_unregister ; struct list_head s_list ; struct mutex umount_mutex ; unsigned int shrinker_run_no; int s_ndevs; struct f2fs_dev_info *devs ; unsigned int dirty_device; spinlock_t dev_lock; bool aligned_blksize; unsigned int first_seq_zone_segno; unsigned int bggc_io_aware; unsigned int allocate_section_hint; unsigned int allocate_section_policy; u64 sectors_written_start; u64 kbytes_written; __u32 s_chksum_seed; struct workqueue_struct *post_read_wq ; struct work_struct s_error_work ; unsigned char errors[MAX_F2FS_ERRORS]; unsigned char stop_reason[MAX_STOP_REASON]; spinlock_t error_lock; bool error_dirty; struct kmem_cache *inline_xattr_slab ; unsigned int inline_xattr_slab_size; unsigned int gc_segment_mode; unsigned int gc_reclaimed_segs[MAX_GC_MODE]; unsigned long seq_file_ra_mul; int max_fragment_chunk; int max_fragment_hole; atomic64_t current_atomic_write; s64 peak_atomic_write; u64 committed_atomic_block; u64 revoked_atomic_block; bool carve_out; #ifdef CONFIG_F2FS_FS_COMPRESSION struct kmem_cache *page_array_slab ; unsigned int page_array_slab_size; u64 compr_written_block; u64 compr_saved_block; u32 compr_new_inode; struct inode *compress_inode ; unsigned int compress_percent; unsigned int compress_watermark; atomic_t compress_page_hit; #endif #ifdef CONFIG_F2FS_IOSTAT spinlock_t iostat_lock; unsigned long long iostat_count[NR_IO_TYPE]; unsigned long long iostat_bytes[NR_IO_TYPE]; unsigned long long prev_iostat_bytes[NR_IO_TYPE]; bool iostat_enable; unsigned long iostat_next_period; unsigned int iostat_period_ms; spinlock_t iostat_lat_lock; struct iostat_lat_info *iostat_io_lat ; #endif };
init_sb_info()init_sb_info() 函数承担了从“原始字节”到“逻辑对象”的转换任务。首先,它会执行大量的字节序转换(如 le32_to_cpu),确保无论磁盘是在大端还是小端机器上格式化的,在当前 CPU 内存中都能正确解析。接着,它会计算出一些关键的导出参数,如总节点数量、根目录索引号(root_ino)等。除此之外,该函数还会初始化文件系统的同步原语,包括各种读写信号量、互斥锁和自旋锁。在 RTEMS 移植过程中,你需要将这些 Linux 特有的锁接口(如 mutex_init 或 init_rwsem)映射到 RTEMS 的信号量或互斥量 API 上。只有正确完成了这一步,F2FS 才能在多线程任务环境下安全地操作元数据。定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 static void init_sb_info (struct f2fs_sb_info *sbi) { struct f2fs_super_block *raw_super = sbi->raw_super; int i; sbi->log_sectors_per_block = le32_to_cpu(raw_super->log_sectors_per_block); sbi->log_blocksize = le32_to_cpu(raw_super->log_blocksize); sbi->blocksize = BIT(sbi->log_blocksize); sbi->log_blocks_per_seg = le32_to_cpu(raw_super->log_blocks_per_seg); sbi->blocks_per_seg = BIT(sbi->log_blocks_per_seg); sbi->segs_per_sec = le32_to_cpu(raw_super->segs_per_sec); sbi->secs_per_zone = le32_to_cpu(raw_super->secs_per_zone); sbi->total_sections = le32_to_cpu(raw_super->section_count); sbi->total_node_count = SEGS_TO_BLKS(sbi, ((le32_to_cpu(raw_super->segment_count_nat) / 2 ) * NAT_ENTRY_PER_BLOCK)); sbi->allocate_section_hint = le32_to_cpu(raw_super->section_count); sbi->allocate_section_policy = ALLOCATE_FORWARD_NOHINT; F2FS_ROOT_INO(sbi) = le32_to_cpu(raw_super->root_ino); F2FS_NODE_INO(sbi) = le32_to_cpu(raw_super->node_ino); F2FS_META_INO(sbi) = le32_to_cpu(raw_super->meta_ino); sbi->cur_victim_sec = NULL_SECNO; sbi->gc_mode = GC_NORMAL; sbi->next_victim_seg[BG_GC] = NULL_SEGNO; sbi->next_victim_seg[FG_GC] = NULL_SEGNO; sbi->max_victim_search = DEF_MAX_VICTIM_SEARCH; sbi->migration_granularity = SEGS_PER_SEC(sbi); sbi->migration_window_granularity = f2fs_sb_has_blkzoned(sbi) ? DEF_MIGRATION_WINDOW_GRANULARITY_ZONED : SEGS_PER_SEC(sbi); sbi->seq_file_ra_mul = MIN_RA_MUL; sbi->max_fragment_chunk = DEF_FRAGMENT_SIZE; sbi->max_fragment_hole = DEF_FRAGMENT_SIZE; spin_lock_init(&sbi->gc_remaining_trials_lock); atomic64_set(&sbi->current_atomic_write, 0 ); sbi->dir_level = DEF_DIR_LEVEL; sbi->interval_time[CP_TIME] = DEF_CP_INTERVAL; sbi->interval_time[REQ_TIME] = DEF_IDLE_INTERVAL; sbi->interval_time[DISCARD_TIME] = DEF_IDLE_INTERVAL; sbi->interval_time[GC_TIME] = DEF_IDLE_INTERVAL; sbi->interval_time[DISABLE_TIME] = DEF_DISABLE_INTERVAL; sbi->interval_time[ENABLE_TIME] = DEF_ENABLE_INTERVAL; sbi->interval_time[UMOUNT_DISCARD_TIMEOUT] = DEF_UMOUNT_DISCARD_TIMEOUT; clear_sbi_flag(sbi, SBI_NEED_FSCK); for (i = 0 ; i < NR_COUNT_TYPE; i++) atomic_set (&sbi->nr_pages[i], 0 ); for (i = 0 ; i < META; i++) atomic_set (&sbi->wb_sync_req[i], 0 ); INIT_LIST_HEAD(&sbi->s_list); mutex_init(&sbi->umount_mutex); init_f2fs_rwsem(&sbi->io_order_lock); spin_lock_init(&sbi->cp_lock); sbi->dirty_device = 0 ; spin_lock_init(&sbi->dev_lock); init_f2fs_rwsem(&sbi->sb_lock); init_f2fs_rwsem(&sbi->pin_sem); }
CheckPoint 区域Checkpoint(检查点,简称 CP)是 F2FS 维护数据一致性的灵魂结构,它记录了文件系统在某一时刻的完整“快照”状态。由于 F2FS 采用日志结构(LFS)不断进行异地更新,系统的元数据(如 NAT、SIT)和数据位置总是在动态变化。为了保证在系统突然断电或崩溃后能够恢复,F2FS 会定期或在特定条件下(如 Umount、Sync)将当前活跃的段信息、节点分配情况以及位图快照写入 Checkpoint 区域。为了极致的可靠性,F2FS 维护了两份互为备份的 CP 结构(通常称为 CP #0 和 CP #1)。**系统始终保持一个为“稳定版本”,另一个为“正在写入版本”。**如果在更新 CP 时发生故障,系统可以安全地回退到上一个稳定的 CP,确保文件系统不会因为元数据损坏而无法挂载。
CheckPoint 区域的结构如下。从图中看出分别是 checkpoint 元数据区域(f2fs_checkpoint)、orphan node 区域、active segments 区域。同时 active segments 区域在不同的情况下,会有不同的形式,目的是减少 IO 的写入。
f2fs_checkpoint 结构struct f2fs_checkpoint 定义了恢复系统所需的最少信息量。其中最重要的字段是 checkpoint_ver(版本号),系统通过比较两个 CP 备份的版本号来确定哪一个是最新的。此外,cur_node_segno 和 cur_node_blkoff(以及对应的 Data 字段)记录了上次 CP 时系统 6 个写入头的精确坐标。如果没有这些坐标,挂载后文件系统将不知道该从哪个物理块开始继续追加数据。同时,结构体末尾的 sit_nat_version_bitmap 记录了 SIT 和 NAT 区域的版本位图,帮助系统快速定位元数据表的最前沿更新。F2FS 必须定时执行 Checkpoint 去记录当前系统的 log 分配到哪个位置,否则在系统宕机的时候,会出现数据丢失等一致性问题,因此 cur_xxx_segno 以及 cur_xxx_blkoff 记录了上次 Checkpoint 时,系统正在使用的 log 的 segment number,以及分配到这个 segment 的哪个位置。定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 struct f2fs_checkpoint { __le64 checkpoint_ver; __le64 user_block_count; __le64 valid_block_count; __le32 rsvd_segment_count; __le32 overprov_segment_count; __le32 free_segment_count; __le32 cur_node_segno[MAX_ACTIVE_NODE_LOGS]; __le16 cur_node_blkoff[MAX_ACTIVE_NODE_LOGS]; __le32 cur_data_segno[MAX_ACTIVE_DATA_LOGS]; __le16 cur_data_blkoff[MAX_ACTIVE_DATA_LOGS]; __le32 ckpt_flags; __le32 cp_pack_total_block_count; __le32 cp_pack_start_sum; __le32 valid_node_count; __le32 valid_inode_count; __le32 next_free_nid; __le32 sit_ver_bitmap_bytesize; __le32 nat_ver_bitmap_bytesize; __le32 checksum_offset; __le64 elapsed_time; unsigned char alloc_type[MAX_ACTIVE_LOGS]; unsigned char sit_nat_version_bitmap[]; } __packed;
Orphan Node这是一个动态的区域,如果没有 orphan node list 则不会占用空间。
Active SegmentsActive Segments 又被称为 CURSEG ,是 F2FS 实施 Multi-head Logging 策略的直接体现。为了提高效率,F2FS 并非每次分配一个块就去更新一次磁盘上的 SIT(段信息表)或 NAT(节点地址表),因为这样会带来巨大的写压力。相反,它利用 CP 区域中的 **Journal(日志)**和 **Summary(摘要)**结构作为临时缓存。Journal 记录了活跃段中元数据的频繁修改,而 Summary 记录了逻辑地址与物理地址的映射。只有当触发 Checkpoint 操作时,这些暂存在内存和 CP 区域的信息才会被批量持久化。这种设计极大地平衡了闪存的寿命与系统的实时性能。
CP 的主要任务是维护数据一致性,因此 CP 的 active segment 区域的主要任务是维护 Active Segment 的分配状态,使系统宕机时候可以恢复正常。维护 active segment 需要维护三种信息,分别是 f2fs_checkpoint 的信息,以及该 segment 对应的 journal 和 summary 的信息。
f2fs_checkpoint 中 Active Segment 信息 :从上面给出的 f2fs_checkpoint 定义,cur_node_segno[MAX_ACTIVE_NODE_LOGS] 和 cur_data_segno[MAX_ACTIVE_DATA_LOGS] 表示 node 和 data 当前的 Active Segment 的编号(segment number, segno),系统可以通过这个编号找到对应的 segment。MAX_ACTIVE_NODE_LOGS 以及 MAX_ACTIVE_NODE_LOGS 分别表示 data 和 node 有多少种类型,F2FS 默认情况下都等于 3,表示 HOT、WARM、COLD 类型数据。cur_node_blkoff[MAX_ACTIVE_NODE_LOGS] 以及 cur_data_blkoff[MAX_ACTIVE_DATA_LOGS] 则分别表示当前 active segment 分配到哪一个 block(一个 segment 包含了 512 个 block)。Segment 对应的 Journal 信息 :Journal 在两处地方都有出现,分别是 CP 区域以及 SSA 区域。CP 区域的 journal 主要用来保存 active segment 的修改信息,而 SSA 区域的则是持久化保存的所有的 segment 的 journal 信息。如系统分配出一个 block 给用户,那么就要将这个 block 所在的 segment 的 bitmap 中标记为已分配,防止其他写请求使用。分两个区域存放 journal 是为了减轻频繁更新导致的系统性能下降。例如,当系统写压力很大的时候,bitmap 就会频繁被更新,如果这个时候频繁将 bitmap 写入 SSA,就会加重写压力。因此 CP 区域的 Journal 的作用就是维护这些经常修改的数据,等待 CP 被触发的时候才回写到闪存设备,从而减少写压力,提高闪存寿命。Segment 对应的 Summary 信息 :summary 同样在 CP 区域和 SSA 区域有出现,它表示的是逻辑地址和物理地址的映射关系,这个映射关系会使用到 GC 流程中。summary 与 segment 是一对一的关系,一个 summary 保存了一个 segment 所有的 block 的物理地址和逻辑地址的映射关系。summary 保存在 CP 区域中同样是出于减少 IO 的写入。 内存管理结构Checkpoint 的内存管理结构是 struct f2fs_checkpoint 本身,因为 Checkpoint 一般只在 F2FS 启动的时候被读取数据,用于数据恢复,而在运行过程中大部分情况都是被写,用于记录恢复信息。因此,Checkpoint 不需要过于复杂的内存管理结构,因此使用 struct f2fs_checkpoint 本身即可以满足需求。
F2FS 的 log,即 active segments,主要用于系统 free block 的分配,因此需要特定的管理结构 struct curseg_info 进行管理。定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct curseg_info { struct mutex curseg_mutex ; struct f2fs_summary_block *sum_blk ; struct rw_semaphore journal_rwsem ; struct f2fs_journal *journal ; unsigned char alloc_type; unsigned short seg_type; unsigned int segno; unsigned short next_blkoff; unsigned int zone; unsigned int next_segno; int fragment_remained_chunk; bool inited; };
从结构分析可以直到,curseg_info 记录当前的 segment 的分配信息,当系统出现宕机的时候,可以从 CP 记录的 curseg_info 恢复当上一次 CP 点的状态。每一种类型的 active segment 就对应一个 struct curseg_info 结构。在 F2FS 中,使用一个数组来表示:
1 2 3 4 5 struct f2fs_sm_info { ... struct curseg_info *curseg_array ; ... }
struct f2fs_sm_info是 SIT 的管理结构,它也管理了 CP 最终的 active segment 的信息,是一个跨区域的管理结构。
struct f2fs_checkpoint 通过 get_checkpoint_version() 函数从磁盘读取出来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 static int get_checkpoint_version (struct f2fs_sb_info *sbi, block_t cp_addr, struct f2fs_checkpoint **cp_block, struct page **cp_page, unsigned long long *version) { unsigned long blk_size = sbi->blocksize; size_t crc_offset = 0 ; __u32 crc = 0 ; *cp_page = f2fs_get_meta_page(sbi, cp_addr); *cp_block = (struct f2fs_checkpoint *)page_address(*cp_page); crc_offset = le32_to_cpu((*cp_block)->checksum_offset); if (crc_offset > (blk_size - sizeof (__le32))) { f2fs_msg(sbi->sb, KERN_WARNING, "invalid crc_offset: %zu" , crc_offset); return -EINVAL; } crc = cur_cp_crc(*cp_block); if (!f2fs_crc_valid(sbi, crc, *cp_block, crc_offset)) { f2fs_msg(sbi->sb, KERN_WARNING, "invalid crc value" ); return -EINVAL; } *version = cur_cp_version(*cp_block); return 0 ; }
struct curseg_info 则是通过 build_curseg() 函数进行初始化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 static int build_curseg (struct f2fs_sb_info *sbi) { struct curseg_info *array ; int i; array = f2fs_kzalloc(sbi, array_size(NR_CURSEG_TYPE, sizeof (*array )), GFP_KERNEL); if (!array ) return -ENOMEM; SM_I(sbi)->curseg_array = array ; for (i = 0 ; i < NR_CURSEG_TYPE; i++) { mutex_init(&array [i].curseg_mutex); array [i].sum_blk = f2fs_kzalloc(sbi, PAGE_SIZE, GFP_KERNEL); if (!array [i].sum_blk) return -ENOMEM; init_rwsem(&array [i].journal_rwsem); array [i].journal = f2fs_kzalloc(sbi, sizeof (struct f2fs_journal), GFP_KERNEL); if (!array [i].journal) return -ENOMEM; array [i].segno = NULL_SEGNO; array [i].next_blkoff = 0 ; } return restore_curseg_summaries(sbi); } static int restore_curseg_summaries (struct f2fs_sb_info *sbi) { struct f2fs_journal *sit_j = CURSEG_I(sbi, CURSEG_COLD_DATA)->journal; struct f2fs_journal *nat_j = CURSEG_I(sbi, CURSEG_HOT_DATA)->journal; int type = CURSEG_HOT_DATA; int err; ... for (; type <= CURSEG_COLD_NODE; type++) { err = read_normal_summaries(sbi, type); if (err) return err; } ... return 0 ; } static int read_normal_summaries (struct f2fs_sb_info *sbi, int type) { struct f2fs_checkpoint *ckpt = F2FS_CKPT(sbi); struct f2fs_summary_block *sum ; struct curseg_info *curseg ; struct page *new ; unsigned short blk_off; unsigned int segno = 0 ; block_t blk_addr = 0 ; ... segno = le32_to_cpu(ckpt->cur_data_segno[type]); blk_off = le16_to_cpu(ckpt->cur_data_blkoff[type - CURSEG_HOT_DATA]); blk_addr = sum_blk_addr(sbi, NR_CURSEG_DATA_TYPE, type); new = f2fs_get_meta_page(sbi, blk_addr); sum = (struct f2fs_summary_block *)page_address(new); curseg = CURSEG_I(sbi, type); mutex_lock(&curseg->curseg_mutex); down_write(&curseg->journal_rwsem); memcpy (curseg->journal, &sum->journal, SUM_JOURNAL_SIZE); up_write(&curseg->journal_rwsem); memcpy (curseg->sum_blk->entries, sum->entries, SUM_ENTRY_SIZE); memcpy (&curseg->sum_blk->footer, &sum->footer, SUM_FOOTER_SIZE); curseg->next_segno = segno; reset_curseg(sbi, type, 0 ); curseg->alloc_type = ckpt->alloc_type[type]; curseg->next_blkoff = blk_off; mutex_unlock(&curseg->curseg_mutex); f2fs_put_page(new, 1 ); return 0 ; }
Segment Infomation Table 区域Segment Infomation Table,简称 SIT,是 F2FS 用于集中管理 segment 状态的结构。它的主要作用是维护的 segment 的分配信息,它的作用可以使用两个常见例子进行描述:
用户进行写操作,那么 segment 会根据用户写入的数据量分配特定数目的 block 给用户进行数据写入,SIT 会将这些已经被分配的 block 标记为"已经使用(valid 状态)",那么之后的写操作就不会再使用这些 block。 用户进行了覆盖写 操作以后,由于 F2FS 异地更新 的特性,F2FS 会分配新 block 给用户写入,同时会将旧 block 置为"无效状态(invalid 状态)",这样 gc 的时候可以根据 segment 无效的 block 的数目,采取某种策略进行回收。 综上所述,SIT 的作用是维护每一个 segment 的 block 的使用状态以及有效无效状态。
元数据的物理结构SIT 区域的结构如下。SIT 区域由 N 个 struct f2fs_sit_block 组成,每一个 struct f2fs_sit_block 包含了 55 个 struct f2fs_sit_entry,每一个 entry 对应了一个 segment 的管理状态。每一个 entry 包含了三个变量: vblocks(记录这个 segment 有多少个 block 已经被使用了),valid_map(记录这个 segment 里面的哪一些 block 是无效的),mtime(表示修改时间)。
SIT 的基本存放单元是 struct f2fs_sit_block,定义如下:
1 2 3 struct f2fs_sit_block { struct f2fs_sit_entry entries [SIT_ENTRY_PER_BLOCK ]; } __packed;
由于一个 block 的尺寸是 4 KB,因此跟根据 sizeof(struct f2fs_sit_entry entries) 的值,得到 SIT_ENTRY_PER_BLOCK 的值为 55。struct f2fs_sit_entry entries用来表示每一个 segment 的状态信息,定义如下:
1 2 3 4 5 struct f2fs_sit_entry { __le16 vblocks; __u8 valid_map[SIT_VBLOCK_MAP_SIZE]; __le64 mtime; } __packed;
第一个参数 vblocks 表示当前 segment 有多少个 block 已经被使用,第二个参数 valid_map 表示 segment 内的每一个 block 的有效无效信息; 由于一个 segment 包含了 512 个 block,因此需要用 512 个 bit 去表示每一个 block 的有效无效状态,因此 SIT_VBLOCK_MAP_SIZE 的值是 64(8*64=512)。最后一个参数 mtime 表示这个 entry 被修改的时间,用于挑选 GC 时需要使用的 segment。
内存管理结构SIT 在内存中对应的管理结构是 struct f2fs_sm_info,它在 build_segment_manager() 函数进行初始化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 struct f2fs_sm_info { struct sit_info *sit_info ; struct free_segmap_info *free_info ; struct dirty_seglist_info *dirty_info ; struct curseg_info *curseg_array ; struct f2fs_rwsem curseg_lock ; block_t seg0_blkaddr; block_t main_blkaddr; block_t ssa_blkaddr; unsigned int segment_count; unsigned int main_segments; unsigned int reserved_segments; unsigned int ovp_segments; unsigned int rec_prefree_segments; struct list_head sit_entry_set ; unsigned int ipu_policy; unsigned int min_ipu_util; unsigned int min_fsync_blocks; unsigned int min_seq_blocks; unsigned int min_hot_blocks; unsigned int min_ssr_sections; struct flush_cmd_control *fcc_info ; struct discard_cmd_control *dcc_info ; }; int build_segment_manager (struct f2fs_sb_info *sbi) { struct f2fs_super_block *raw_super = F2FS_RAW_SUPER(sbi); struct f2fs_checkpoint *ckpt = F2FS_CKPT(sbi); struct f2fs_sm_info *sm_info ; int err; sm_info = kzalloc(sizeof (struct f2fs_sm_info), GFP_KERNEL); sbi->sm_info = sm_info; INIT_LIST_HEAD(&sm_info->wblist_head); spin_lock_init(&sm_info->wblist_lock); sm_info->seg0_blkaddr = le32_to_cpu(raw_super->segment0_blkaddr); sm_info->main_blkaddr = le32_to_cpu(raw_super->main_blkaddr); sm_info->segment_count = le32_to_cpu(raw_super->segment_count); sm_info->reserved_segments = le32_to_cpu(ckpt->rsvd_segment_count); sm_info->ovp_segments = le32_to_cpu(ckpt->overprov_segment_count); sm_info->main_segments = le32_to_cpu(raw_super->segment_count_main); sm_info->ssa_blkaddr = le32_to_cpu(raw_super->ssa_blkaddr); err = build_sit_info(sbi); err = build_free_segmap(sbi); err = build_curseg(sbi); build_sit_entries(sbi); init_free_segmap(sbi); err = build_dirty_segmap(sbi); init_min_max_mtime(sbi); return 0 ; }
build_sit_info() 用于初始化内存区域的 entry,这里需要注意的是注意区分内存 entry 以及物理区域的 entry:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 static int build_sit_info (struct f2fs_sb_info *sbi) { struct f2fs_super_block *raw_super = F2FS_RAW_SUPER(sbi); struct sit_info *sit_i ; unsigned int sit_segs, start; char *src_bitmap, *bitmap; unsigned int bitmap_size, main_bitmap_size, sit_bitmap_size; unsigned int discard_map = f2fs_block_unit_discard(sbi) ? 1 : 0 ; sit_i = f2fs_kzalloc(sbi, sizeof (struct sit_info), GFP_KERNEL); if (!sit_i) return -ENOMEM; SM_I(sbi)->sit_info = sit_i; sit_i->sentries = f2fs_kvzalloc(sbi, array_size(sizeof (struct seg_entry), MAIN_SEGS(sbi)), GFP_KERNEL); if (!sit_i->sentries) return -ENOMEM; main_bitmap_size = f2fs_bitmap_size(MAIN_SEGS(sbi)); sit_i->dirty_sentries_bitmap = f2fs_kvzalloc(sbi, main_bitmap_size, GFP_KERNEL); if (!sit_i->dirty_sentries_bitmap) return -ENOMEM; #ifdef CONFIG_F2FS_CHECK_FS bitmap_size = MAIN_SEGS(sbi) * SIT_VBLOCK_MAP_SIZE * (3 + discard_map); #else bitmap_size = MAIN_SEGS(sbi) * SIT_VBLOCK_MAP_SIZE * (2 + discard_map); #endif sit_i->bitmap = f2fs_kvzalloc(sbi, bitmap_size, GFP_KERNEL); if (!sit_i->bitmap) return -ENOMEM; bitmap = sit_i->bitmap; for (start = 0 ; start < MAIN_SEGS(sbi); start++) { sit_i->sentries[start].cur_valid_map = bitmap; bitmap += SIT_VBLOCK_MAP_SIZE; sit_i->sentries[start].ckpt_valid_map = bitmap; bitmap += SIT_VBLOCK_MAP_SIZE; #ifdef CONFIG_F2FS_CHECK_FS sit_i->sentries[start].cur_valid_map_mir = bitmap; bitmap += SIT_VBLOCK_MAP_SIZE; #endif if (discard_map) { sit_i->sentries[start].discard_map = bitmap; bitmap += SIT_VBLOCK_MAP_SIZE; } } sit_i->tmp_map = f2fs_kzalloc(sbi, SIT_VBLOCK_MAP_SIZE, GFP_KERNEL); if (!sit_i->tmp_map) return -ENOMEM; if (__is_large_section(sbi)) { sit_i->sec_entries = f2fs_kvzalloc(sbi, array_size(sizeof (struct sec_entry), MAIN_SECS(sbi)), GFP_KERNEL); if (!sit_i->sec_entries) return -ENOMEM; } sit_segs = le32_to_cpu(raw_super->segment_count_sit) >> 1 ; sit_bitmap_size = __bitmap_size(sbi, SIT_BITMAP); src_bitmap = __bitmap_ptr(sbi, SIT_BITMAP); sit_i->sit_bitmap = kmemdup(src_bitmap, sit_bitmap_size, GFP_KERNEL); if (!sit_i->sit_bitmap) return -ENOMEM; #ifdef CONFIG_F2FS_CHECK_FS sit_i->sit_bitmap_mir = kmemdup(src_bitmap, sit_bitmap_size, GFP_KERNEL); if (!sit_i->sit_bitmap_mir) return -ENOMEM; sit_i->invalid_segmap = f2fs_kvzalloc(sbi, main_bitmap_size, GFP_KERNEL); if (!sit_i->invalid_segmap) return -ENOMEM; #endif sit_i->sit_base_addr = le32_to_cpu(raw_super->sit_blkaddr); sit_i->sit_blocks = SEGS_TO_BLKS(sbi, sit_segs); sit_i->written_valid_blocks = 0 ; sit_i->bitmap_size = sit_bitmap_size; sit_i->dirty_sentries = 0 ; sit_i->sents_per_block = SIT_ENTRY_PER_BLOCK; sit_i->elapsed_time = le64_to_cpu(sbi->ckpt->elapsed_time); sit_i->mounted_time = ktime_get_boottime_seconds(); init_rwsem(&sit_i->sentry_lock); return 0 ; }
build_free_segmap() 用于初始化 segment 的分配状态:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 static int build_free_segmap (struct f2fs_sb_info *sbi) { struct free_segmap_info *free_i ; unsigned int bitmap_size, sec_bitmap_size; free_i = f2fs_kzalloc(sbi, sizeof (struct free_segmap_info), GFP_KERNEL); if (!free_i) return -ENOMEM; SM_I(sbi)->free_info = free_i; bitmap_size = f2fs_bitmap_size(MAIN_SEGS(sbi)); free_i->free_segmap = f2fs_kvmalloc(sbi, bitmap_size, GFP_KERNEL); if (!free_i->free_segmap) return -ENOMEM; sec_bitmap_size = f2fs_bitmap_size(MAIN_SECS(sbi)); free_i->free_secmap = f2fs_kvmalloc(sbi, sec_bitmap_size, GFP_KERNEL); if (!free_i->free_secmap) return -ENOMEM; memset (free_i->free_segmap, 0xff , bitmap_size); memset (free_i->free_secmap, 0xff , sec_bitmap_size); free_i->start_segno = GET_SEGNO_FROM_SEG0(sbi, MAIN_BLKADDR(sbi)); free_i->free_segments = 0 ; free_i->free_sections = 0 ; spin_lock_init(&free_i->segmap_lock); return 0 ; }
build_sit_entries() 的作用是从 SIT 的物理区域存放的物理 entry 与内存的 entry 建立联系,首先看看物理 entry 和内存 entry 的差异在哪里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 static int build_sit_entries (struct f2fs_sb_info *sbi) { struct sit_info *sit_i = SIT_I(sbi); struct curseg_info *curseg = CURSEG_I(sbi, CURSEG_COLD_DATA); struct f2fs_journal *journal = curseg->journal; struct seg_entry *se ; struct f2fs_sit_entry sit ; int sit_blk_cnt = SIT_BLK_CNT(sbi); unsigned int i, start, end; unsigned int readed, start_blk = 0 ; int err = 0 ; block_t sit_valid_blocks[2 ] = {0 , 0 }; do { readed = f2fs_ra_meta_pages(sbi, start_blk, BIO_MAX_VECS, META_SIT, true ); start = start_blk * sit_i->sents_per_block; end = (start_blk + readed) * sit_i->sents_per_block; for (; start < end && start < MAIN_SEGS(sbi); start++) { struct f2fs_sit_block *sit_blk ; struct folio *folio ; se = &sit_i->sentries[start]; folio = get_current_sit_folio(sbi, start); if (IS_ERR(folio)) return PTR_ERR(folio); sit_blk = folio_address(folio); sit = sit_blk->entries[SIT_ENTRY_OFFSET(sit_i, start)]; f2fs_folio_put(folio, true ); err = check_block_count(sbi, start, &sit); if (err) return err; seg_info_from_raw_sit(se, &sit); if (se->type >= NR_PERSISTENT_LOG) { f2fs_err(sbi, "Invalid segment type: %u, segno: %u" , se->type, start); f2fs_handle_error(sbi, ERROR_INCONSISTENT_SUM_TYPE); return -EFSCORRUPTED; } sit_valid_blocks[SE_PAGETYPE(se)] += se->valid_blocks; if (!f2fs_block_unit_discard(sbi)) goto init_discard_map_done; if (is_set_ckpt_flags(sbi, CP_TRIMMED_FLAG)) { memset (se->discard_map, 0xff , SIT_VBLOCK_MAP_SIZE); goto init_discard_map_done; } memcpy (se->discard_map, se->cur_valid_map, SIT_VBLOCK_MAP_SIZE); sbi->discard_blks += BLKS_PER_SEG(sbi) - se->valid_blocks; init_discard_map_done: if (__is_large_section(sbi)) get_sec_entry(sbi, start)->valid_blocks += se->valid_blocks; } start_blk += readed; } while (start_blk < sit_blk_cnt); down_read(&curseg->journal_rwsem); for (i = 0 ; i < sits_in_cursum(journal); i++) { unsigned int old_valid_blocks; start = le32_to_cpu(segno_in_journal(journal, i)); if (start >= MAIN_SEGS(sbi)) { f2fs_err(sbi, "Wrong journal entry on segno %u" , start); err = -EFSCORRUPTED; f2fs_handle_error(sbi, ERROR_CORRUPTED_JOURNAL); break ; } se = &sit_i->sentries[start]; sit = sit_in_journal(journal, i); old_valid_blocks = se->valid_blocks; sit_valid_blocks[SE_PAGETYPE(se)] -= old_valid_blocks; err = check_block_count(sbi, start, &sit); if (err) break ; seg_info_from_raw_sit(se, &sit); if (se->type >= NR_PERSISTENT_LOG) { f2fs_err(sbi, "Invalid segment type: %u, segno: %u" , se->type, start); err = -EFSCORRUPTED; f2fs_handle_error(sbi, ERROR_INCONSISTENT_SUM_TYPE); break ; } sit_valid_blocks[SE_PAGETYPE(se)] += se->valid_blocks; if (f2fs_block_unit_discard(sbi)) { if (is_set_ckpt_flags(sbi, CP_TRIMMED_FLAG)) { memset (se->discard_map, 0xff , SIT_VBLOCK_MAP_SIZE); } else { memcpy (se->discard_map, se->cur_valid_map, SIT_VBLOCK_MAP_SIZE); sbi->discard_blks += old_valid_blocks; sbi->discard_blks -= se->valid_blocks; } } if (__is_large_section(sbi)) { get_sec_entry(sbi, start)->valid_blocks += se->valid_blocks; get_sec_entry(sbi, start)->valid_blocks -= old_valid_blocks; } } up_read(&curseg->journal_rwsem); if (__is_large_section(sbi)) { unsigned int segno; for (segno = 0 ; segno < MAIN_SEGS(sbi); segno += SEGS_PER_SEC(sbi)) { set_ckpt_valid_blocks(sbi, segno); sanity_check_valid_blocks(sbi, segno); } } if (err) return err; if (sit_valid_blocks[NODE] != valid_node_count(sbi)) { f2fs_err(sbi, "SIT is corrupted node# %u vs %u" , sit_valid_blocks[NODE], valid_node_count(sbi)); f2fs_handle_error(sbi, ERROR_INCONSISTENT_NODE_COUNT); return -EFSCORRUPTED; } if (sit_valid_blocks[DATA] + sit_valid_blocks[NODE] > valid_user_blocks(sbi)) { f2fs_err(sbi, "SIT is corrupted data# %u %u vs %u" , sit_valid_blocks[DATA], sit_valid_blocks[NODE], valid_user_blocks(sbi)); f2fs_handle_error(sbi, ERROR_INCONSISTENT_BLOCK_COUNT); return -EFSCORRUPTED; } return 0 ; } struct f2fs_sit_entry { __le16 vblocks; __u8 valid_map[SIT_VBLOCK_MAP_SIZE]; __le64 mtime; } __packed; struct seg_entry { unsigned short valid_blocks; unsigned char *cur_valid_map; unsigned short ckpt_valid_blocks; unsigned char *ckpt_valid_map; unsigned char type; unsigned long long mtime; };
两者之间的差异主要是多了表示 segment 类型的 type 变量,以及多了两个与 checkpoint 相关的内容。
其实物理 entry 也包含了 segment type 的信息,但是为了节省空间,将 segment type 于 vblocks 存放在了一起,及 vblocks 的前 10 位表示数目,后 6 位表示 segment type,关系如下:
1 2 3 4 5 6 7 #define SIT_VBLOCKS_SHIFT 10 #define SIT_VBLOCKS_MASK ((1 << SIT_VBLOCKS_SHIFT) - 1) #define GET_SIT_VBLOCKS(raw_sit) \ (le16_to_cpu((raw_sit)->vblocks) & SIT_VBLOCKS_MASK) #define GET_SIT_TYPE(raw_sit) \ ((le16_to_cpu((raw_sit)->vblocks) & ~SIT_VBLOCKS_MASK) \ >> SIT_VBLOCKS_SHIFT)
因此,内存 entry 实际上仅仅多了 2 个与 checkpoint 相关的信息,即 ckpt_valid_blocks 与 ckpt_valid_map 。在系统执行 checkpoint 的时候,会将 valid_blocks 以及 cur_valid_map 的值分别写入 ckpt_valid_blocks 与 ckpt_valid_map,当系统出现宕机的时候根据这个值恢复映射信息。
init_free_segmap() 从内存 entry 以及 checkpoint 中恢复 free segment 的信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 static void init_free_segmap (struct f2fs_sb_info *sbi) { unsigned int start; int type; struct seg_entry *sentry ; for (start = 0 ; start < MAIN_SEGS(sbi); start++) { if (f2fs_usable_blks_in_seg(sbi, start) == 0 ) continue ; sentry = get_seg_entry(sbi, start); if (!sentry->valid_blocks) __set_free(sbi, start); else SIT_I(sbi)->written_valid_blocks += sentry->valid_blocks; } for (type = CURSEG_HOT_DATA; type <= CURSEG_COLD_NODE; type++) { struct curseg_info *curseg_t = CURSEG_I(sbi, type); __set_test_and_inuse(sbi, curseg_t ->segno); } }
init_dirty_segmap() 函数恢复脏 segment 的信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 static void init_dirty_segmap (struct f2fs_sb_info *sbi) { struct dirty_seglist_info *dirty_i = DIRTY_I(sbi); struct free_segmap_info *free_i = FREE_I(sbi); unsigned int segno = 0 , offset = 0 , secno; block_t valid_blocks, usable_blks_in_seg; while (1 ) { segno = find_next_inuse(free_i, MAIN_SEGS(sbi), offset); if (segno >= MAIN_SEGS(sbi)) break ; offset = segno + 1 ; valid_blocks = get_valid_blocks(sbi, segno, false ); usable_blks_in_seg = f2fs_usable_blks_in_seg(sbi, segno); if (valid_blocks == usable_blks_in_seg || !valid_blocks) continue ; if (valid_blocks > usable_blks_in_seg) { f2fs_bug_on(sbi, 1 ); continue ; } mutex_lock(&dirty_i->seglist_lock); __locate_dirty_segment(sbi, segno, DIRTY); mutex_unlock(&dirty_i->seglist_lock); } if (!__is_large_section(sbi)) return ; mutex_lock(&dirty_i->seglist_lock); for (segno = 0 ; segno < MAIN_SEGS(sbi); segno += SEGS_PER_SEC(sbi)) { valid_blocks = get_valid_blocks(sbi, segno, true ); secno = GET_SEC_FROM_SEG(sbi, segno); if (!valid_blocks || valid_blocks == CAP_BLKS_PER_SEC(sbi)) continue ; if (is_cursec(sbi, secno)) continue ; set_bit(secno, dirty_i->dirty_secmap); } mutex_unlock(&dirty_i->seglist_lock); }
Node Address Table 区域Node Address Table,简称 NAT,是 F2FS 用于集中管理 node 的结构。**它的主要维护了一张表(如下图),记录了每一个 node 在 flash 设备的物理地址。**F2FS 给每一个 node 分配了一个 node ID(nid),系统可以根据 nid 从 NAT 查找到该 node 在 flash 设备上的物理地址,然后从 flash 设备读取出来。表的结构如下:
元数据的物理结构NAT 区域由 N 个 struct f2fs_nat_block 组成,每一个 struct f2fs_nat_block 包含了 455 个 struct f2fs_nat_entry。每一个 nid 对应了一个 entry,每一个 entry 记录了这个 node 的在 flash 设备上的物理地址 block_addr。同时 entry 也记录了一个 ino 的值,这个值用于找到这个 node 的 parent node,如果 nid == ino 则表示这个 node 是 inode,如果 nid != ino,则表示这是一个 direct_node 或者 indrect_node。version 变量用于系统恢复。
内存管理结构NAT 在内存中对应的管理结构是 struct f2fs_nm_info,它在 build_node_manager() 函数进行初始化。struct f2fs_nm_info 不会将所有的 NAT 的数据都读取出来,而是读取 NAT 的一部分,然后构建 free nid 表,用于给新的 node 分配 nid。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 struct f2fs_nm_info { block_t nat_blkaddr; nid_t max_nid; nid_t available_nids; nid_t next_scan_nid; nid_t max_rf_node_blocks; unsigned int ram_thresh; unsigned int ra_nid_pages; unsigned int dirty_nats_ratio; struct radix_tree_root nat_root ; struct radix_tree_root nat_set_root ; struct f2fs_rwsem nat_tree_lock ; struct list_head nat_entries ; spinlock_t nat_list_lock; unsigned int nat_cnt[MAX_NAT_STATE]; unsigned int nat_blocks; struct radix_tree_root free_nid_root ; struct list_head free_nid_list ; unsigned int nid_cnt[MAX_NID_STATE]; spinlock_t nid_list_lock; struct mutex build_lock ; unsigned char **free_nid_bitmap; unsigned char *nat_block_bitmap; unsigned short *free_nid_count; char *nat_bitmap; unsigned int nat_bits_blocks; unsigned char *nat_bits; unsigned char *full_nat_bits; unsigned char *empty_nat_bits; #ifdef CONFIG_F2FS_CHECK_FS char *nat_bitmap_mir; #endif int bitmap_size; }; int build_node_manager (struct f2fs_sb_info *sbi) { int err; sbi->nm_info = kzalloc(sizeof (struct f2fs_nm_info), GFP_KERNEL); if (!sbi->nm_info) return -ENOMEM; err = init_node_manager(sbi); if (err) return err; build_free_nids(sbi); return 0 ; }
init_node_manager() 函数主要用于初始化 sbi->nm_info 内的变量信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 static int init_node_manager (struct f2fs_sb_info *sbi) { struct f2fs_super_block *sb_raw = F2FS_RAW_SUPER(sbi); struct f2fs_nm_info *nm_i = NM_I(sbi); unsigned char *version_bitmap; unsigned int nat_segs; int err; nm_i->nat_blkaddr = le32_to_cpu(sb_raw->nat_blkaddr); nat_segs = le32_to_cpu(sb_raw->segment_count_nat) >> 1 ; nm_i->nat_blocks = nat_segs << le32_to_cpu(sb_raw->log_blocks_per_seg); nm_i->max_nid = NAT_ENTRY_PER_BLOCK * nm_i->nat_blocks; nm_i->available_nids = nm_i->max_nid - sbi->total_valid_node_count - F2FS_RESERVED_NODE_NUM; nm_i->nid_cnt[FREE_NID] = 0 ; nm_i->nid_cnt[PREALLOC_NID] = 0 ; nm_i->ram_thresh = DEF_RAM_THRESHOLD; nm_i->ra_nid_pages = DEF_RA_NID_PAGES; nm_i->dirty_nats_ratio = DEF_DIRTY_NAT_RATIO_THRESHOLD; nm_i->max_rf_node_blocks = DEF_RF_NODE_BLOCKS; INIT_RADIX_TREE(&nm_i->free_nid_root, GFP_ATOMIC); INIT_LIST_HEAD(&nm_i->free_nid_list); INIT_RADIX_TREE(&nm_i->nat_root, GFP_NOIO); INIT_RADIX_TREE(&nm_i->nat_set_root, GFP_NOIO); INIT_LIST_HEAD(&nm_i->nat_entries); spin_lock_init(&nm_i->nat_list_lock); mutex_init(&nm_i->build_lock); spin_lock_init(&nm_i->nid_list_lock); init_f2fs_rwsem(&nm_i->nat_tree_lock); nm_i->next_scan_nid = le32_to_cpu(sbi->ckpt->next_free_nid); nm_i->bitmap_size = __bitmap_size(sbi, NAT_BITMAP); version_bitmap = __bitmap_ptr(sbi, NAT_BITMAP); nm_i->nat_bitmap = kmemdup(version_bitmap, nm_i->bitmap_size, GFP_KERNEL); if (!nm_i->nat_bitmap) return -ENOMEM; if (!test_opt(sbi, NAT_BITS)) disable_nat_bits(sbi, true ); err = __get_nat_bitmaps(sbi); if (err) return err; #ifdef CONFIG_F2FS_CHECK_FS nm_i->nat_bitmap_mir = kmemdup(version_bitmap, nm_i->bitmap_size, GFP_KERNEL); if (!nm_i->nat_bitmap_mir) return -ENOMEM; #endif return 0 ; }
build_free_nids() 主要用于构建 free nid 表,用于给新的 node 分配 nid。 为了节省内存,F2FS 不会将 NAT 中所有的 free nid 读取出来,只会读取一部分,因此使用 nm_i->fcnt 表示缓存了多少个 free nid。然后会读取一定的数目的 f2fs_nat_block 出来,并遍历其中的每一个 f2fs_nat_entry,加入到 free nid 的管理结构中。最后还会搜索一下 log 区域的 free nid 信息,也加入到 free nid 管理结构中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 void build_free_nids (struct f2fs_sb_info *sbi) { struct f2fs_nm_info *nm_i = NM_I(sbi); struct curseg_info *curseg = CURSEG_I(sbi, CURSEG_HOT_DATA); struct f2fs_journal *journal = curseg->journal; int i = 0 ; nid_t nid = nm_i->next_scan_nid; if (nm_i->fcnt >= NAT_ENTRY_PER_BLOCK) return ; ra_meta_pages(sbi, NAT_BLOCK_OFFSET(nid), FREE_NID_PAGES, META_NAT, true ); down_read(&nm_i->nat_tree_lock); while (1 ) { struct page *page = get_current_nat_page(sbi, nid); scan_nat_page(sbi, page, nid); f2fs_put_page(page, 1 ); nid += (NAT_ENTRY_PER_BLOCK - (nid % NAT_ENTRY_PER_BLOCK)); if (unlikely(nid >= nm_i->max_nid)) nid = 0 ; if (++i >= FREE_NID_PAGES) break ; } nm_i->next_scan_nid = nid; down_read(&curseg->journal_rwsem); for (i = 0 ; i < nats_in_cursum(journal); i++) { block_t addr; nid = le32_to_cpu(nid_in_journal(journal, i)); addr = le32_to_cpu(nat_in_journal(journal, i).block_addr); if (addr == NULL_ADDR) add_free_nid(sbi, nid, true ); else remove_free_nid(nm_i, nid); } up_read(&curseg->journal_rwsem); up_read(&nm_i->nat_tree_lock); ra_meta_pages(sbi, NAT_BLOCK_OFFSET(nm_i->next_scan_nid), nm_i->ra_nid_pages, META_NAT, false ); }
scan_nat_page() 函数的作用是扫描当前的 f2fs_nat_block 的每一个 entry,并找到其中的 free nid,加入到 nm_i 的 free nid 管理结构中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 static int scan_nat_page (struct f2fs_sb_info *sbi, struct f2fs_nat_block *nat_blk, nid_t start_nid) { struct f2fs_nm_info *nm_i = NM_I(sbi); block_t blk_addr; unsigned int nat_ofs = NAT_BLOCK_OFFSET(start_nid); int i; __set_bit_le(nat_ofs, nm_i->nat_block_bitmap); i = start_nid % NAT_ENTRY_PER_BLOCK; for (; i < NAT_ENTRY_PER_BLOCK; i++, start_nid++) { if (unlikely(start_nid >= nm_i->max_nid)) break ; blk_addr = le32_to_cpu(nat_blk->entries[i].block_addr); if (blk_addr == NEW_ADDR) return -EFSCORRUPTED; if (blk_addr == NULL_ADDR) { add_free_nid(sbi, start_nid, true , true ); } else { spin_lock(&NM_I(sbi)->nid_list_lock); update_free_nid_bitmap(sbi, start_nid, false , true ); spin_unlock(&NM_I(sbi)->nid_list_lock); } } return 0 ; }
Segment Summary Area 区域Segment Summary Area,简称 SSA,是 F2FS 用于集中管理物理地址到逻辑地址的映射关系的结构,同时它也具有通过 journal 缓存 sit 或者 nat 的操作用于数据恢复的作用。映射关系的主要作用是当给出一个物理地址的时候,可以通过 SSA 索引得到对应的逻辑地址,主要应用在 GC 流程中; SSA 所包含的 journal 可以缓存一些 sit 或者 nat 的操作,用于避免频繁的元数据更新,以及宕机时候的数据恢复。
元数据的物理结构从结构图可以知道,SSA 区域由 N 个 struct f2fs_summary_block 组成,每一个 struct f2fs_summary_block 包含了 512 个 struct f2fs_summary_entry,刚好对应一个 segment。segment 里面的每一个 block(物理地址)对应一个的 struct f2fs_summary_entry,它记录了物理地址到逻辑地址的映射信息。它包含了三个变量: nid(该物理地址是属于哪一个 node 的),version(用于数据恢复),ofs_in_node(该物理地址属于该 nid 对应的 node 的第 ofs_in_node 个 block)。
f2fs_journal 属于 journal 的信息,它的作用是减少频繁地对 NAT 区域以及 SIT 区域的更新。例如,当系统写压力很大的时候,segment bitmap 更新就会很频繁,就会对 struct f2fs_sit_entry 结构进行频繁地改动。如果这个时候频繁将新的映射关系写入 SIT,就会加重写压力。此时可以将数据先写入到 journal 中,因此 journal 的作用就是维护这些经常修改的数据,等待 CP 被触发的时候才写入磁盘,从而减少写压力 。也许这里会有疑问,为什么将 journal 放在 SSA 区域而不是 NAT 区域以及 SIT 区域呢?这是因为这种存放方式可以减少元数据区域空间的占用。
SSA 的基本存放单元是 struct f2fs_summary_block,定义如下:
1 2 3 4 5 6 struct f2fs_summary_block { struct f2fs_summary entries [ENTRIES_IN_SUM ]; struct f2fs_journal journal ; struct summary_footer footer ; } __packed;
与 summary 直接相关的是 struct f2fs_summary 以及 struct summary_footer。ENTRIES_IN_SUM 的值 512,因此每一个 entry 对应一个 block,记录了从物理地址到逻辑地址的映射关系,entry 的结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct f2fs_summary { __le32 nid; union { __u8 reserved[3 ]; struct { __u8 version; __le16 ofs_in_node; } __packed; }; } __packed; struct summary_footer { unsigned char entry_type; __le32 check_sum; } __packed;
struct f2fs_summary 用了一个 union 结构进行表示,但是核心信息是 nid、version 以及 ofs_in_node。数据的索引是通过 node 来进行。文件访问某一个页的数据时,需要首先根据页的索引,找到对应的 nid 以及 offset(两者构成逻辑地址),从而根据 nid 得到 node page,再根据 offset 得到了该页的物理地址,然后从磁盘中读取出来。f2fs_summary 则是记录物理地址到逻辑地址的映射 ,即根据物理地址找到对应的 nid 以及 offset。例如,现在需要根据物理地址为 624 的 block,找到对应的 nid 以及 offset。那么物理地址为 624,可以得到该地址位于第二个 segment,然后属于第二个 segment 的第 113 个 block(block 的编址从 0 开始)。因此根据属于第二个 segment 的信息,找到第二个 struct f2fs_summary_block,然后根据偏移量为 113 的信息,找到对应的 struct f2fs_summary 结构,从而得到 nid 以及 ofs_in_node。
struct summary_footer 结构记录了校验信息,以及这个 summary 对应的 segment 是属于保存 data 数据的 segment 还是 node 数据的segment。
SSA 在内存没有单独的管理结构,summary 以及 journal 在内存中主要存在于 CURSEG 中。
文件数据组织方式文件数据的组织方式一般时被设计为 inode-data 模式,即每一个文件都具有一个 inode,这个 inode 记录 data 的组织关系,这个关系称为文件结构 。例如用户需要访问 A 文件的第 1000 个字节,系统就会先根据 A 文件的路径找到的 A 的 inode,然后从 inode 找到第 1000 个字节所在的物理地址,然后从磁盘读取出来。那么 F2FS 的文件结构是怎么样的呢?
F2FS 中的一个 inode,包含两个主要部分: metadata 部分,和数据块寻址部分。我们重点观察数据块寻址部分,分析 inode 时如何将数据块索引出来。在图中,数据块寻址部分包含 direct pointers,single-indirect,double-indirect,以及 triple-indirect。它们的含义分别是:
direct pointer: inode 内直接指向数据块(图右上角 Data)的地址数组,即 inode->data模式 。single-indirect pointer: inode 记录了两个 single-indirect pointer(图右上角 Direct node),每一个 single-indirect pointer 存储了多个数据块的地址,即 inode->direct_node->data 模式 。double-indirect: inode 记录了两个 double-indirect pointer(图右上角 indirect node),每一个 double-indirect pointer 记录了许多 single-indirect pointer,每一个 single-indirect pointer 指向了数据块,即 inode->indirect_node->direct_node->data 模式 。triple-indirect: inode 记录了一个 triple-indirect pointer(图右上角 indirect node),每一个 triple-indirect pointer 记录了许多 double-indirect pointer,每一个 double-indirect pointer 记录了许多 single-indirect pointer,最后每一个 single-indirect pointer 指向了数据块。即 inode->indirect_node->indirect_node->direct_node->data 模式 。可以发现,F2FS 的 inode 结构采取 indirect_node,首先在 inode 内部寻找物理地址,如果找不到再去 direct_node 找,层层深入。
f2fs_node 结构及其作用对于一个较大的文件,它可能包含 inode 以外的 node,去保存一些间接寻址的信息。single-indirect pointer 记录的是数据块的地址,而 double-indirect pointer 记录的是 single-indirect pointer 的地址,triple-indirect pointer 记录的 double-indirect pointer 地址。在F2FS中:
inode 对应的是 f2fs_inode 结构,包含了多个 direct pointer 指向数据块物理地址; single-indirect pointer 对应的是 direct_node 结构,包含了多个 direct pointer 指向物理地址; double-indirect pointer 对应的是 indirect_node 结构,包含了多个指向 direct_node 的地址; triple-indirect pointer 对应的也是 indirect_node 结构,包含了多个指向 indirect_node 的地址。 基本 node 结构为了方便 F2FS 的对 node 的区分和管理,f2fs_inode 和 direct_node 以及 indirect_node 都使用了同一个数据结构 f2fs_node 进行描述,并通过 union 的方式,将 f2fs_node 初始化成不同的 node 形式。定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 struct f2fs_node { union { struct f2fs_inode i ; struct direct_node dn ; struct indirect_node in ; }; struct node_footer footer ; } __packed; struct node_footer { __le32 nid; __le32 ino; __le32 flag; __le64 cp_ver; __le32 next_blkaddr; } __packed;
其中起到区分是哪一种 node 的关键数据结构是 node_footer。如果 node_footer 的 nid 和 ino 相等,则表示这是一个 f2fs_inode 结构,如果不相等,则表示这是一个 direct_node 或者 indirect_node。
f2fs_inode 结构考虑 f2fs_inode 的结构,省略其他元数据的信息,重点关注文件如何索引的,结构如下:
1 2 3 4 5 6 struct f2fs_inode { ... __le32 i_addr[DEF_ADDRS_PER_INODE]; __le32 i_nid[DEF_NIDS_PER_INODE]; ... } __packed;
i_addr 数组就是前面提及的 direct pointer,数组的下标是文件的逻辑位置,数组的值就是 flash 设备的物理地址。例如文件的第一个页就对应 i_addr[0],第二个页就对应 i_addr[1],而 i_addr[0] 和 i_addr[1] 所记录的物理地址,就是文件第一个页(page)和第二个页的数据的物理地址,系统可以将两个物理地址提交到 flash 设备,将数据读取出来。
我们可以发现 i_addr 的数组长度只有 923,即一个 f2fs_inode 只能直接索引到 923 个页/块的地址(约 3.6 MB),对于大于 3.6 MB的文件,就需要使用间接寻址 。f2fs_inode 的 i_nid 数组就是为了间接寻址而设计,i_nid 数组是一个长度为 5 的数组,可以记录 5 个 node 的地址。其中:
i_nid[0] 和 i_nid[1] 记录的是 direct_node 的地址,即对应前述的 single-indirect pointer。i_nid[2] 和 i_nid[3] 记录的是 indirect_node 的地址,这两个 indirect_node 记录的是 direct_node 的地址,即对应前述的 double-indirect pointer。i_nid[4] 记录的是 indirect_node 的地址,但是这个 indirect_node 记录的是 indirect_node 的地址,即前述的 triple-indirect pointer。 direct_node 和 indirect_node 结构direct_node 记录的是数据块的地址,indirect_inode 记录的是 node 的 id,系统可以通过 nid 找到对应的 node 的地址。
1 2 3 4 5 6 7 struct direct_node { __le32 addr[ADDRS_PER_BLOCK]; } __packed; struct indirect_node { __le32 nid[NIDS_PER_BLOCK]; } __packed;
Wandering Tree 问题F2FS 的设计是为了解决 wandering tree 的问题,那么现在的设计是如何解决这个问题的呢。假设一个文件发生更改,修改了 direct_node 里面的某一个 block 的数据,根据 LFS 的异地更新特性,我们需要给更改后的数据一个新的 block。传统的 LFS 需要将这个新的 block 的地址一层层网上传递,直到 inode 结构。而 F2FS 的设计是只需要将 direct_node 对应位置的 addr 的值更新为新 block 的地址,从而没必要往上传递,因此解决了 wandering tree 的问题。
普通文件数据的保存一个文件由一个 f2fs_inode 和多个 direct_inode 或者 indirect_inode 所组成。当系统创建一个文件的时候,它会首先创建一个 f2fs_inode 写入到 flash 设备,然后用户往该文件写入第一个 page 的时候,会将数据写入到 main area 的一个 block 中,然后将该 block 的物理地址赋值到 f2fs_inode->i_addr[0] 中,这样就完成了 Node-Data 的管理关系。随着对同一文件写入的数据的增多,会逐渐使用到其他类型的 node 去保存文件的数据。
内联数据文件的保存文件的实际数据是保存在 f2fs_inode->i_addr 对应的物理块当中,因此即使一个很小的文件,如 1 个字节的小文件,也需要一个 node 和 data block 才能实现正常的保存和读写,也就是需要 8 KB 的磁盘空间去保存一个尺寸为 1 字节的小文件。而且 f2fs_inode->i_addr[923] 里面除了 f2fs_inode->i_addr[0] 保存了一个物理地址,其余的 922 个 i_addr 都被闲置,造成了空间的浪费。
F2FS 为了减少空间的使用量,使用内联(inline)文件减少这些空间的浪费。它的核心思想是当文件足够小的时候,使用 f2fs_inode->i_addr 数组直接保存数据本身,而不单独写入一个 block 中,再进行寻址。因此,如上面的例子,只有 1 个字节大小的文件,只需要一个 f2fs_inode 结构,即 4 KB,就可以同时将 node 信息和 data 信息同时保存,减少了一半的空间使用量。
根据上述定义,可以计算得到多大的文件可以使用内联的方式进行保存,f2fs_inode 有尺寸为 923 的用于保存数据物理地址的数组 i_addr,它的数据类型是 __le32,即 4 个字节。保留一个数组成员另做它用,因此内联文件最大尺寸为: 922 * 4 = 3688 字节。
文件访问的实际例子Linux 的文件是通过 page 进行组织起来的,默认 page 的 size 是 4 KB,使用 index 作为编号。
一个小文件访问例子:例如一个 size = 10 KB 的文件,需要 3 个 page 去保存数据,这 3 个 page 的编号是 0,1,2。当用户访问这个文件的第 2~6kb 的数据的时候,系统就会计算出数据保存在 page index = 0 和 1 的 page 中,然后根据文件的路径找到对应的 f2fs_inode 结构,page index = 0 和 1 即对应 f2fs_inode 的 i_addr[0] 和 i_addr[1]。系统进而从这两个 i_addr 读取物理地址,提交到 flash 设备将数据读取出来。
一个大文件访问例子:假设用户需要读取文件第 4000 个页(page index = 3999)的数据,第一步: 那么首先系统会根据文件路径找到对应的 f2fs_inode 结构。第二步:由于 4000 >(923 + 1018 + 1018),f2fs_inode->i_addr和 f2fs_inode->nid[0]和nid[1] 都无法满足需求,因此系统根据 f2fs_inode->nid[2] 找到对应的 indirect_node 的地址。第三步:indirect_node 保存的是 direct_node 的nid数组,由于 4000 - 923 - 1018 - 1018 = 1041,而一个 direct_node 只能保存 1018 个 block,因此可以知道数据位于 indirect_node->nid[1] 对应的 direct_node 中。第四步:计算剩下的的偏移(4000-923-1018-1018-1018=23)找到数据的物理地址位于该 direct_node 的 direct_node->addr[23] 中。
读流程F2FS 的读流程包含了以下几个子流程:
vfs_read 函数。 generic_file_read_iter 函数: 根据访问类型执行不同的处理。 generic_file_buffered_read 函数: 根据用户传入的文件偏移,读取尺寸等信息,计算起始位置和页数,然后遍历每一个 page,通过预读或者单个读取的方式从磁盘中读取出来。 f2fs_read_data_page&f2fs_read_data_pages 函数: 从磁盘读取 1 个 page 或者多个 page。 f2fs_mpage_readpages 函数: f2fs 读取数据的主流程。 第一步的 vfs_read 函数是 VFS 层面的流程,下面仅针对涉及 F2FS 的读流程,且经过简化的主要流程进行分析。
generic_file_read_iter 函数这个函数的作用是处理普通方式访问以及 direct 方式访问的读行为,这里仅针对普通方式的读访问进行分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ssize_t generic_file_read_iter (struct kiocb *iocb, struct iov_iter *iter) { size_t count = iov_iter_count(iter); ssize_t retval = 0 ; if (!count) goto out; if (iocb->ki_flags & IOCB_DIRECT) { ... } retval = generic_file_buffered_read(iocb, iter, retval); out: return retval; }
generic_file_buffered_read 函数在介绍这两个之前,需要先介绍一种 VFS 提高读取速度的机制: 预读(readahead)机制。它的核心原理是,当用户访问 page 1,系统就会将 page 1 后续的 page 2,page 3,page 4 一起读取到 page cache(减少与磁盘这种速度慢设备的交互次数,提高读性能)。之后用户再连续读取 page 2,page 3,page 4 时,由于已经读取到内存中,因此可以快速地返回给用户。
generic_file_buffered_read() 函数的主要作用是循环地从磁盘或者内存读取用户需要的 page,同时也会在某些情况调用 page_cache_sync_readahead() 函数进行预读,由于函数比较复杂,且很多 goto 语句,简化后的步骤如下:
情况 1:预读(readahead)机制成功预读到用户需要接下来访问的 page
ind_get_page: 系统无法在 cache 中找到用户需要的 page。 page_cache_sync_readahead: 系统执行该函数进行预读,一次性读取多个 page。 find_get_page: 再重新在 cache 获取一次 page,获取成功后跳转到 page ok 区域。 page_ok: 复制 page 的数据去用户传入的 buffer 中,然后判读是否为最后一个 page,如果是则退出读流程。 情况 2:预读(readahead)机制错误预读到用户需要接下来访问的 page
find_get_page: 系统无法在 cache 中找到用户需要的 page。 page_cache_sync_readahead: 系统执行该函数进行预读,一次性读取多个 page。 find_get_page: 再重新在 cache 获取一次 page,获取失败,跳转到 no_cached_page 区域。 no_cached_page: 创建一个 page cache 结构,加入到 LRU 后,跳转到 readpage 区域。 readpage: 执行 mapping->a_ops->readpage 函数从磁盘读取数据,成功后跳转到 page ok 区域。 page_ok: 复制 page 的数据去用户传入的 buffer 中,然后判读是否为最后一个 page,如果是则退出读流程。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 static ssize_t generic_file_buffered_read (struct kiocb *iocb, struct iov_iter *iter, ssize_t written) { index = *ppos >> PAGE_SHIFT; prev_index = ra->prev_pos >> PAGE_SHIFT; prev_offset = ra->prev_pos & (PAGE_SIZE-1 ); last_index = (*ppos + iter->count + PAGE_SIZE-1 ) >> PAGE_SHIFT; offset = *ppos & ~PAGE_MASK; for (;;) { find_page: page = find_get_page(mapping, index); if (!page) { page_cache_sync_readahead(mapping, ra, filp, index, last_index - index); page = find_get_page(mapping, index); if (unlikely(page == NULL )) goto no_cached_page; } page_ok: isize = i_size_read(inode); end_index = (isize - 1 ) >> PAGE_SHIFT; nr = PAGE_SIZE; if (index == end_index) { nr = ((isize - 1 ) & ~PAGE_MASK) + 1 ; if (nr <= offset) { put_page(page); goto out; } } nr = nr - offset; ret = copy_page_to_iter(page, offset, nr, iter); offset += ret; index += offset >> PAGE_SHIFT; offset &= ~PAGE_MASK; prev_offset = offset; put_page(page); written += ret; if (!iov_iter_count(iter)) goto out; if (ret < nr) { error = -EFAULT; goto out; } continue ; readpage: ClearPageError(page); error = mapping->a_ops->readpage(filp, page); goto page_ok; no_cached_page: page = page_cache_alloc(mapping); error = add_to_page_cache_lru(page, mapping, index, mapping_gfp_constraint(mapping, GFP_KERNEL)); goto readpage; } out: ra->prev_pos = prev_index; ra->prev_pos <<= PAGE_SHIFT; ra->prev_pos |= prev_offset; *ppos = ((loff_t )index << PAGE_SHIFT) + offset; file_accessed(filp); return written ? written : error; }
预读函数 page_cache_sync_readahead() 的分析由于篇幅有限无法全部展示,这里仅分析它的核心调用函数 __do_page_cache_readahead():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 unsigned int __do_page_cache_readahead(struct address_space *mapping, struct file *filp, pgoff_t offset, unsigned long nr_to_read, unsigned long lookahead_size) { end_index = ((isize - 1 ) >> PAGE_SHIFT); for (page_idx = 0 ; page_idx < nr_to_read; page_idx++) { pgoff_t page_offset = offset + page_idx; if (page_offset > end_index) break ; page = __page_cache_alloc(gfp_mask); page->index = page_offset; list_add(&page->lru, &page_pool); nr_pages++; } if (nr_pages) read_pages(mapping, filp, &page_pool, nr_pages, gfp_mask); BUG_ON(!list_empty(&page_pool)); out: return nr_pages; } static int read_pages (struct address_space *mapping, struct file *filp, struct list_head *pages, unsigned int nr_pages, gfp_t gfp) { struct blk_plug plug ; unsigned page_idx; int ret; blk_start_plug(&plug); if (mapping->a_ops->readpages) { ret = mapping->a_ops->readpages(filp, mapping, pages, nr_pages); put_pages_list(pages); goto out; } ret = 0 ; out: blk_finish_plug(&plug); return ret; }
f2fs_read_data_page & f2fs_read_data_pages 函数当预读机制会调用 mapping->a_ops->readpages 函数一次性读取多个 page。而当预读失败时,也会调用 mapping->a_ops->readpage 读取单个 page。这两个函数在 f2fs 中对应的就是 f2fs_read_page 和 f2fs_read_pages,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 static int f2fs_read_data_page (struct file *file, struct page *page) { struct inode *inode = page->mapping->host; int ret = -EAGAIN; trace_f2fs_readpage(page, DATA); if (f2fs_has_inline_data(inode)) ret = f2fs_read_inline_data(inode, page); ret = f2fs_mpage_readpages(page->mapping, NULL , page, 1 ); return ret; } static int f2fs_read_data_pages (struct file *file, struct address_space *mapping, struct list_head *pages, unsigned nr_pages) { struct inode *inode = mapping->host; struct page *page = list_last_entry(pages, struct page, lru); trace_f2fs_readpages(inode, page, nr_pages); if (f2fs_has_inline_data(inode)) return 0 ; return f2fs_mpage_readpages(mapping, pages, NULL , nr_pages); }
f2fs_mpage_readpages函数无论是 f2fs_read_page 函数还是 f2fs_read_pages 函数,都是调用 f2fs_mpage_readpages 函数进行读取,区别仅在于传入参数。f2fs_mpage_readpages 的定义为:
1 2 static int f2fs_mpage_readpages (struct address_space *mapping, struct list_head *pages, struct page *page, unsigned nr_pages) ;
第二个参数表示一个链表头,这个链表保存了多个 page,因此需要写入多个 page 的时候,就要传入一个 List。 第三个参数表示单个 page,在写入单个 page 的时候,通过这个函数写入。 第四个参数表示需要写入 page 的数目。 因此:
在写入多个 page 的时候,需要设定第二个参数,和第四个参数,然后设定第三个参数为 NULL。 在写入单个 page 的时候,需要设定第三个参数,和第四个参数,然后设定第二个参数为 NULL。 然后分析这个函数的执行流程:
遍历传入的 page,得到每一个 page 的 index 以及 inode。 将 page 的 inode 以及 index 传入 f2fs_map_blocks() 函数获取到该 page 的物理地址。 将物理地址通过 submit_bio() 读取该 page 在磁盘中的数据。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 static int f2fs_mpage_readpages (struct address_space *mapping, struct list_head *pages, struct page *page, unsigned nr_pages) { struct f2fs_map_blocks map ; map .m_pblk = 0 ; map .m_lblk = 0 ; map .m_len = 0 ; map .m_flags = 0 ; map .m_next_pgofs = NULL ; for (page_idx = 0 ; nr_pages; page_idx++, nr_pages--) { if (pages) { page = list_entry(pages->prev, struct page, lru); list_del(&page->lru); if (add_to_page_cache_lru(page, mapping, page->index, GFP_KERNEL)) goto next_page; } if ((map .m_flags & F2FS_MAP_MAPPED) && block_in_file > map .m_lblk && block_in_file < (map .m_lblk + map .m_len)) goto got_it; map .m_flags = 0 ; if (block_in_file < last_block) { map .m_lblk = block_in_file; map .m_len = last_block - block_in_file; if (f2fs_map_blocks(inode, &map , 0 , F2FS_GET_BLOCK_READ)) goto set_error_page; } got_it: if ((map .m_flags & F2FS_MAP_MAPPED)) { block_nr = map .m_pblk + block_in_file - map .m_lblk; SetPageMappedToDisk(page); if (!PageUptodate(page) && !cleancache_get_page(page)) { SetPageUptodate(page); goto confused; } } else { zero_user_segment(page, 0 , PAGE_SIZE); SetPageUptodate(page); unlock_page(page); goto next_page; } if (bio && (last_block_in_bio != block_nr - 1 )) { submit_and_realloc: submit_bio(READ, bio); bio = NULL ; } if (bio == NULL ) { struct fscrypt_ctx *ctx = NULL ; if (f2fs_encrypted_inode(inode) && S_ISREG(inode->i_mode)) { ctx = fscrypt_get_ctx(inode, GFP_NOFS); if (IS_ERR(ctx)) goto set_error_page; f2fs_wait_on_encrypted_page_writeback( F2FS_I_SB(inode), block_nr); } bio = bio_alloc(GFP_KERNEL, min_t (int , nr_pages, BIO_MAX_PAGES)); if (!bio) { if (ctx) fscrypt_release_ctx(ctx); goto set_error_page; } bio->bi_bdev = bdev; bio->bi_iter.bi_sector = SECTOR_FROM_BLOCK(block_nr); bio->bi_end_io = f2fs_read_end_io; bio->bi_private = ctx; } if (bio_add_page(bio, page, blocksize, 0 ) < blocksize) goto submit_and_realloc; set_error_page: SetPageError(page); zero_user_segment(page, 0 , PAGE_SIZE); unlock_page(page); goto next_page; confused: if (bio) { submit_bio(READ, bio); bio = NULL ; } unlock_page(page); next_page: if (pages) put_page(page); } BUG_ON(pages && !list_empty(pages)); if (bio) submit_bio(READ, bio); return 0 ; }
写流程F2FS 的写流程主要包含了以下几个子流程:
调用 vfs_write 函数。 调用 f2fs_file_write_iter 函数: 初始化 f2fs_node 的信息。 调用 f2fs_write_begin 函数: 创建 page cache,并填充数据。 写入到 page cache: 等待系统触发 writeback 回写到磁盘。 调用 f2fs_write_end 函数: 将 page 设置为最新状态。 调用 f2fs_write_data_pages 函数: 系统 writeback 或者 fsync 触发的时候执行这个函数写入到磁盘。 第一步的 vfs_write 函数是 VFS 层面的流程,下面仅针对涉及 F2FS 的写流程,且经过简化的主要流程进行分析。
f2fs_file_write_iter函数这个函数的主要作用是在数据写入文件之前进行预处理,核心流程就是将该文件对应 f2fs_inode 或者 direct_node 对应写入位置的 i_addr 或者 addr的值进行初始化。例如用户需要在第 4 个 page 的位置写入数据,那么 f2fs_file_write_iter 函数会首先找到该文件对应的 f2fs_inode,然后找到第 4 个 page 对应的数据块地址记录,即 f2fs_inode->i_addr[3]。如果该位置的值是 NULL_ADDR 则表示当前是添加写(Append Write) ,因此将值初始化为 NEW_ADDR。如果是该位置的值是一个具体的 block 号,那么表示为覆盖写(Overwrite) ,不需要做处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 static ssize_t f2fs_file_write_iter (struct kiocb *iocb, struct iov_iter *from) { struct file *file = iocb->ki_filp; struct inode *inode = file_inode(file); ssize_t ret; ... err = f2fs_preallocate_blocks(iocb, from); ... ret = __generic_file_write_iter(iocb, from); ... return ret; }
下面继续分析 f2fs_preallocate_blocks()。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int f2fs_preallocate_blocks (struct kiocb *iocb, struct iov_iter *from) { struct inode *inode = file_inode(iocb->ki_filp); struct f2fs_map_blocks map ; map .m_lblk = F2FS_BLK_ALIGN(iocb->ki_pos); map .m_len = F2FS_BYTES_TO_BLK(iocb->ki_pos + iov_iter_count(from)); map .m_next_pgofs = NULL ; map .m_next_extent = NULL ; map .m_seg_type = NO_CHECK_TYPE; flag = F2FS_GET_BLOCK_PRE_AIO; map_blocks: err = f2fs_map_blocks(inode, &map , 1 , flag); return err; }
f2fs_map_blocks() 函数的作用非常广泛,主要作用是通过逻辑地址(文件偏移指针)找到对应的物理地址(block 号)。因此在读写流程中都有作用。在写流程中,该函数的主要作用是初始化地址信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 int f2fs_map_blocks (struct inode *inode, struct f2fs_map_blocks *map , int create, int flag) { unsigned int maxblocks = map ->m_len; struct f2fs_sb_info *sbi = F2FS_I_SB(inode); int mode = create ? ALLOC_NODE : LOOKUP_NODE; map ->m_len = 0 ; map ->m_flags = 0 ; pgofs = (pgoff_t )map ->m_lblk; end = pgofs + maxblocks; next_dnode: set_new_dnode(&dn, inode, NULL , NULL , 0 ); err = f2fs_get_dnode_of_data(&dn, pgofs, mode); start_pgofs = pgofs; prealloc = 0 ; last_ofs_in_node = ofs_in_node = dn.ofs_in_node; end_offset = ADDRS_PER_PAGE(dn.node_page, inode); next_block: blkaddr = datablock_addr(dn.inode, dn.node_page, dn.ofs_in_node); ... if (!is_valid_blkaddr(blkaddr)) { if (create) { if (flag == F2FS_GET_BLOCK_PRE_AIO) { if (blkaddr == NULL_ADDR) { prealloc++; last_ofs_in_node = dn.ofs_in_node; } } map ->m_flags |= F2FS_MAP_NEW; blkaddr = dn.data_blkaddr; } } ... dn.ofs_in_node++; pgofs++; ... if (flag == F2FS_GET_BLOCK_PRE_AIO && (pgofs == end || dn.ofs_in_node == end_offset)) { dn.ofs_in_node = ofs_in_node; err = f2fs_reserve_new_blocks(&dn, prealloc); map ->m_len += dn.ofs_in_node - ofs_in_node; dn.ofs_in_node = end_offset; } ... if (pgofs >= end) goto sync_out; else if (dn.ofs_in_node < end_offset) goto next_block; ... sync_out: ... out: return err; }
然后分析 f2fs_reserve_new_blocks()。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int f2fs_reserve_new_blocks (struct dnode_of_data *dn, blkcnt_t count) { struct f2fs_sb_info *sbi = F2FS_I_SB(dn->inode); int err; ... for (; count > 0 ; dn->ofs_in_node++) { block_t blkaddr = datablock_addr(dn->inode, dn->node_page, dn->ofs_in_node); if (blkaddr == NULL_ADDR) { dn->data_blkaddr = NEW_ADDR; __set_data_blkaddr(dn); count--; } } ... return 0 ; }
f2fs_write_begin 和 f2fs_write_end 函数VFS 中 write_begin 和 write_end 函数分别是数据写入 page cache 前以及写入后的处理。写入 page cache 后,系统会维护一段时间,直到满足一定条件后(如 fsync 和 writeback 会写),VFS 会调用 writepages 函数,将这些缓存在内存中的 page 一次性写入到磁盘中。write_begin 和 write_end 函数的调用可以参考 VFS 的 generic_perform_write 函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 ssize_t generic_perform_write (struct file *file, struct iov_iter *i, loff_t pos) { struct address_space *mapping = file->f_mapping; const struct address_space_operations *a_ops = mapping->a_ops; long status = 0 ; ssize_t written = 0 ; unsigned int flags = 0 ; do { struct page *page ; unsigned long offset; unsigned long bytes; size_t copied; void *fsdata; offset = (pos & (PAGE_SIZE - 1 )); bytes = min_t (unsigned long , PAGE_SIZE - offset, iov_iter_count(i)); again: status = a_ops->write_begin(file, mapping, pos, bytes, flags, &page, &fsdata); copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes); flush_dcache_page(page); status = a_ops->write_end(file, mapping, pos, bytes, copied, page, fsdata); copied = status; iov_iter_advance(i, copied); pos += copied; written += copied; balance_dirty_pages_ratelimited(mapping); } while (iov_iter_count(i)); return written ? written : status; }
然后分析 VFS 的 write_begin 和 write_end 对应的功能,write_begin 在 F2FS 中对应的是 f2fs_write_begin,它的作用是将根据用户需要写入的数据类型,对 page 进行初始化,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 static int f2fs_write_begin (struct file *file, struct address_space *mapping, loff_t pos, unsigned len, unsigned flags, struct page **pagep, void **fsdata) { struct inode *inode = mapping->host; struct f2fs_sb_info *sbi = F2FS_I_SB(inode); struct page *page = NULL ; pgoff_t index = ((unsigned long long ) pos) >> PAGE_SHIFT; bool need_balance = false , drop_atomic = false ; block_t blkaddr = NULL_ADDR; int err = 0 ; repeat: page = f2fs_pagecache_get_page(mapping, index, FGP_LOCK | FGP_WRITE | FGP_CREAT, GFP_NOFS); *pagep = page; err = prepare_write_begin(sbi, page, pos, len, &blkaddr, &need_balance); if (blkaddr == NEW_ADDR) { zero_user_segment(page, 0 , PAGE_SIZE); SetPageUptodate(page); } else { err = f2fs_submit_page_read(inode, page, blkaddr); lock_page(page); if (unlikely(page->mapping != mapping)) { f2fs_put_page(page, 1 ); goto repeat; } if (unlikely(!PageUptodate(page))) { err = -EIO; goto fail; } } return 0 ; }
通过 flush_dcache_page() 函数将用户数据写入到 page cache 之后,进行 write_end 处理,在 F2FS 中它对应的是 f2fs_write_end() 函数,它的作用是,如下所述:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 static int f2fs_write_end (struct file *file, struct address_space *mapping, loff_t pos, unsigned len, unsigned copied, struct page *page, void *fsdata) { struct inode *inode = page->mapping->host; if (!PageUptodate(page)) { if (unlikely(copied != len)) copied = 0 ; else SetPageUptodate(page); } if (!copied) goto unlock_out; set_page_dirty(page); if (pos + copied > i_size_read(inode)) f2fs_i_size_write(inode, pos + copied); unlock_out: f2fs_put_page(page, 1 ); f2fs_update_time(F2FS_I_SB(inode), REQ_TIME); return copied; }
f2fs_write_data_pages 函数系统会将用户写入的数据先写入到 page cache,然后等待时机回写到磁盘中。page cache 的回写是通过 f2fs_write_data_pages() 函数进行。系统会将 page cache 中 dirty 的 pages 加入到一个 list 当中,然后传入到 f2fs_write_data_pages() 进行处理。它包含如下步骤:
f2fs_write_data_pages & __f2fs_write_data_pages 函数: 做一些不那么重要的预处理。 f2fs_write_cache_pages 函数: 从 inode->mapping 的 radix tree 中取出 page。 __write_data_page 函数: 判断文件类型(内联文件,目录文件,普通文件)进行不同的写入。 f2fs_do_write_data_page: 根据 F2FS 的状态选择进行就地回写(在原物理地址更新)还是异地回写(在其他物理地址更新)。 f2fs_outplace_write_data: 执行回写,更新 f2fs_inode 的状态。 do_write_page: 从 CURSEG 分配物理地址,然后写入到磁盘。 f2fs_write_data_pages & __f2fs_write_data_pages 函数这两个函数只是包含了一些不太重要的预处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 static int f2fs_write_data_pages (struct address_space *mapping, struct writeback_control *wbc) { struct inode *inode = mapping->host; return __f2fs_write_data_pages(mapping, wbc, F2FS_I(inode)->cp_task == current ? FS_CP_DATA_IO : FS_DATA_IO); } static int __f2fs_write_data_pages(struct address_space *mapping, struct writeback_control *wbc, enum iostat_type io_type) { struct inode *inode = mapping->host; struct f2fs_sb_info *sbi = F2FS_I_SB(inode); struct blk_plug plug ; int ret; blk_start_plug(&plug); ret = f2fs_write_cache_pages(mapping, wbc, io_type); blk_finish_plug(&plug); f2fs_remove_dirty_inode(inode); return ret; skip_write: wbc->pages_skipped += get_dirty_pages(inode); trace_f2fs_writepages(mapping->host, wbc, DATA); return 0 ; }
f2fs_write_cache_pages 函数这个函数的主要作用是从 inode 对应的 mapping(radix tree 的 root)中,取出所有需要回写的 page,然后通过一个循环,逐个写入到磁盘。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 static int f2fs_write_cache_pages (struct address_space *mapping, struct writeback_control *wbc, enum iostat_type io_type) { struct pagevec pvec ; pagevec_init(&pvec); if (wbc->sync_mode == WB_SYNC_ALL || wbc->tagged_writepages) tag = PAGECACHE_TAG_TOWRITE; else tag = PAGECACHE_TAG_DIRTY; retry: if (wbc->sync_mode == WB_SYNC_ALL || wbc->tagged_writepages) tag_pages_for_writeback(mapping, index, end); done_index = index; while (!done && (index <= end)) { int i; nr_pages = pagevec_lookup_range_tag(&pvec, mapping, &index, end, tag); for (i = 0 ; i < nr_pages; i++) { struct page *page = pvec.pages[i]; bool submitted = false ; ret = __write_data_page(page, &submitted, wbc, io_type); if (--wbc->nr_to_write <= 0 && wbc->sync_mode == WB_SYNC_NONE) { done = 1 ; break ; } } pagevec_release(&pvec); cond_resched(); } if (wbc->range_cyclic || (range_whole && wbc->nr_to_write > 0 )) mapping->writeback_index = done_index; if (last_idx != ULONG_MAX) f2fs_submit_merged_write_cond(F2FS_M_SB(mapping), mapping->host, 0 , last_idx, DATA); return ret; }
__write_data_page 函数这个函数的作用是判断文件类型(目录文件,内联文件,普通文件)进行不同的写入。F2FS 针对普通文件,有两种保存方式,分别是内联方式(inline)和普通方式。这里主要介绍普通文件的写流程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 static int __write_data_page(struct page *page, bool *submitted, struct writeback_control *wbc, enum iostat_type io_type) { struct inode *inode = page->mapping->host; struct f2fs_sb_info *sbi = F2FS_I_SB(inode); loff_t i_size = i_size_read(inode); const pgoff_t end_index = ((unsigned long long ) i_size) >> PAGE_SHIFT; struct f2fs_io_info fio = { .sbi = sbi, .ino = inode->i_ino, .type = DATA, .op = REQ_OP_WRITE, .op_flags = wbc_to_write_flags(wbc), .old_blkaddr = NULL_ADDR, .page = page, .encrypted_page = NULL , .submitted = false , .need_lock = LOCK_RETRY, .io_type = io_type, .io_wbc = wbc, }; if (page->index < end_index) goto write; write: if (S_ISDIR(inode->i_mode)) { err = f2fs_do_write_data_page(&fio); goto done; } err = -EAGAIN; if (f2fs_has_inline_data(inode)) { err = f2fs_write_inline_data(inode, page); if (!err) goto out; } if (err == -EAGAIN) { err = f2fs_do_write_data_page(&fio); } done: if (err && err != -ENOENT) goto redirty_out; out: inode_dec_dirty_pages(inode); if (err) ClearPageUptodate(page); unlock_page(page); if (submitted) *submitted = fio.submitted; return 0 ; redirty_out: redirty_page_for_writepage(wbc, page); if (!err || wbc->for_reclaim) return AOP_WRITEPAGE_ACTIVATE; unlock_page(page); return err; }
f2fs_do_write_data_page 函数这个函数的作用是根据系统的状态选择就地更新数据(inplace update)还是异地更新数据(outplace update)。一般情况下,系统只会在磁盘空间比较满的时候选择就地更新策略,避免触发过多的 gc 影响性能。因此,这里主要介绍异地更新的写流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 int f2fs_do_write_data_page (struct f2fs_io_info *fio) { struct page *page = fio->page; struct inode *inode = page->mapping->host; struct dnode_of_data dn ; struct extent_info ei = {0 ,0 ,0 }; bool ipu_force = false ; int err = 0 ; set_new_dnode(&dn, inode, NULL , NULL , 0 ); err = f2fs_get_dnode_of_data(&dn, page->index, LOOKUP_NODE); fio->old_blkaddr = dn.data_blkaddr; if (fio->old_blkaddr == NULL_ADDR) { ClearPageUptodate(page); goto out_writepage; } got_it: if (ipu_force || (is_valid_blkaddr(fio->old_blkaddr) && need_inplace_update(fio))) { err = encrypt_one_page(fio); if (err) goto out_writepage; set_page_writeback(page); ClearPageError(page); f2fs_put_dnode(&dn); if (fio->need_lock == LOCK_REQ) f2fs_unlock_op(fio->sbi); err = f2fs_inplace_write_data(fio); trace_f2fs_do_write_data_page(fio->page, IPU); set_inode_flag(inode, FI_UPDATE_WRITE); return err; } err = encrypt_one_page(fio); set_page_writeback(page); ClearPageError(page); f2fs_outplace_write_data(&dn, fio); set_inode_flag(inode, FI_APPEND_WRITE); if (page->index == 0 ) set_inode_flag(inode, FI_FIRST_BLOCK_WRITTEN); out_writepage: f2fs_put_dnode(&dn); out: if (fio->need_lock == LOCK_REQ) f2fs_unlock_op(fio->sbi); return err; }
f2fs_outplace_write_data 函数这个函数主要用作异地更新,所谓异地更新即不在原先的物理地址更新数据,因此包含了如下四个步骤:
分配一个新的物理地址。 将数据写入新的物理地址。 将旧的物理地址无效掉,然后等 GC 回收。 更新逻辑地址和物理地址的映射关系。 本函数即完成以上四个步骤:
1 2 3 4 5 6 7 8 9 10 11 12 13 void f2fs_outplace_write_data (struct dnode_of_data *dn, struct f2fs_io_info *fio) { struct f2fs_sb_info *sbi = fio->sbi; struct f2fs_summary sum ; struct node_info ni ; f2fs_get_node_info(sbi, dn->nid, &ni); set_summary(&sum, dn->nid, dn->ofs_in_node, ni.version); do_write_page(&sum, fio); f2fs_update_data_blkaddr(dn, fio->new_blkaddr); }
struct dnode_of_data dn 的作用是根据文件 inode,找到 f2fs_inode 或者 direct_node,然后再通过文件偏移得到物理地址,因此 f2fs_update_data_blkaddr() 也是通过 dnode_of_data 将新的物理地址更新到 f2fs_inode 或者 direct_node 对应的位置中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 void f2fs_update_data_blkaddr (struct dnode_of_data *dn, block_t blkaddr) { dn->data_blkaddr = blkaddr; f2fs_set_data_blkaddr(dn); f2fs_update_extent_cache(dn); } void f2fs_set_data_blkaddr (struct dnode_of_data *dn) { f2fs_wait_on_page_writeback(dn->node_page, NODE, true ); __set_data_blkaddr(dn); if (set_page_dirty(dn->node_page)) dn->node_changed = true ; } static void __set_data_blkaddr(struct dnode_of_data *dn){ struct f2fs_node *rn = F2FS_NODE(dn->node_page); __le32 *addr_array; int base = 0 ; addr_array = blkaddr_in_node(rn); addr_array[base + dn->ofs_in_node] = cpu_to_le32(dn->data_blkaddr); } static inline __le32 *blkaddr_in_node (struct f2fs_node *node) { return RAW_IS_INODE(node) ? node->i.i_addr : node->dn.addr; }
do_write_page 函数上一节提及到异地更新的 1,2,3 步骤都是在这里完成,分别是 f2fs_allocate_data_block() 函数完成新物理地址的分配,以及旧物理地址的回收; f2fs_submit_page_write() 函数完成最后一步,将数据提交到磁盘。下面进行分析:
1 2 3 4 5 6 7 8 9 10 static void do_write_page (struct f2fs_summary *sum, struct f2fs_io_info *fio) { int type = __get_segment_type(fio); f2fs_allocate_data_block(fio->sbi, fio->page, fio->old_blkaddr, &fio->new_blkaddr, sum, type, fio, true ); f2fs_submit_page_write(fio); }
f2fs_allocate_data_block() 函数首先会根据 type 获得 CURSEG。然后在 CURSEG 分配一个新的物理块,然后将旧的物理块无效掉。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 void f2fs_allocate_data_block (struct f2fs_sb_info *sbi, struct page *page, block_t old_blkaddr, block_t *new_blkaddr, struct f2fs_summary *sum, int type, struct f2fs_io_info *fio, bool add_list) { struct sit_info *sit_i = SIT_I(sbi); struct curseg_info *curseg = CURSEG_I(sbi, type); *new_blkaddr = NEXT_FREE_BLKADDR(sbi, curseg); __add_sum_entry(sbi, type, sum); __refresh_next_blkoff(sbi, curseg); update_sit_entry(sbi, *new_blkaddr, 1 ); if (GET_SEGNO(sbi, old_blkaddr) != NULL_SEGNO) update_sit_entry(sbi, old_blkaddr, -1 ); if (!__has_curseg_space(sbi, type)) sit_i->s_ops->allocate_segment(sbi, type, false ); locate_dirty_segment(sbi, GET_SEGNO(sbi, old_blkaddr)); locate_dirty_segment(sbi, GET_SEGNO(sbi, *new_blkaddr)); }
f2fs_submit_page_write() 完成最后的提交到磁盘的任务,先创建一个 bio,然后将 page 加入到 bio 中,如果 bio 满了就提交到磁盘。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 void f2fs_submit_page_write (struct f2fs_io_info *fio) { struct f2fs_sb_info *sbi = fio->sbi; enum page_type btype = PAGE_TYPE_OF_BIO(fio->type); struct f2fs_bio_info *io = sbi->write_io[btype] + fio->temp; struct page *bio_page ; down_write(&io->io_rwsem); next: if (fio->encrypted_page) bio_page = fio->encrypted_page; else bio_page = fio->page; fio->submitted = true ; alloc_new: if (io->bio == NULL ) { io->bio = __bio_alloc(sbi, fio->new_blkaddr, fio->io_wbc, BIO_MAX_PAGES, false , fio->type, fio->temp); io->fio = *fio; } if (bio_add_page(io->bio, bio_page, PAGE_SIZE, 0 ) < PAGE_SIZE) { __submit_merged_bio(io); goto alloc_new; } out: up_write(&io->io_rwsem); }
需要注意的是,在这个函数,当 bio 还没有填满 page 的时候是不会被提交到磁盘的,这是因为 F2FS 通过增大 bio 的 size 提高了写性能。因此,在用户 fsync 或者系统 writeback 的时候,为了保证这些 page 都可以刷写到磁盘,会如 f2fs_write_cache_pages() 函数所介绍一样,通过 f2fs_submit_merged_write_cond() 函数或者其他函数强行提交这个 page 未满的 bio。
文件创建流程linux 的文件的创建可以抽象为两个流程。
创建一个 inode,使得包含文件的元数据信息; 将这个新创建的 inode 加入父目录的管理当中,可以理解建立父目录与这个新 inode 的关系。 到具体代码,上述两个抽象流程在 F2FS 中主要包含了以下几个子流程:
调用 vfs_open 函数。 调用 f2fs_create 函数: 创建文件 inode,并链接到父目录。f2fs_new_inode 函数创建 inode。 f2fs_add_link 函数链接到父目录。 第一步的 vfs_open 函数是 VFS 层面的流程,下面仅针对涉及 F2FS 的文件创建流程,且经过简化的主要流程进行分析。
inode 和 f2fs_inode_infoinode 结构是 linux 的 vfs 层最核心的结构之一,反应了文件的应该具有的基础信息,但是对于一些文件系统,原生的 inode 结构的信息并不够,还需要增加一些额外的变量去支持文件系统的某些功能,同时为了保证 vfs 层对所有文件系统的兼容性,我们直接修改 inode 结构不是一个明智的方法。针对这种场景,f2fs 使用了一种叫 f2fs_inode_info 的结构去扩展原有的 inode 的功能。
相互转换从 inode 到 f2fs_inode_info:
1 2 3 4 static inline struct f2fs_inode_info *F2FS_I (struct inode *inode) { return container_of(inode, struct f2fs_inode_info, vfs_inode); }
从 f2fs_inode_info 到 inode:
1 2 3 4 5 6 7 8 9 struct f2fs_inode_info { struct inode vfs_inode ; ... }; struct f2fs_inode_info *fi = F2FS_I(inode);fi->vfs_inode
从上面代码我们可以看出,f2fs 中的 inode 是 f2fs_inode_info 当中的一个内部变量,因此可以用 container_of 这个函数直接获得,也可以通过指针获得。
VFS inode 的创建和销毁我们一般使用 VFS 提供的 new_inode 函数创建一个新 inode。这个 new_inode 函数内部会调用 new_inode_pseudo 函数,然后再调用 alloc_inode 函数,最后调用 f2fs_alloc_inode 函数,我们从这里开始分析:
如下代码,显然就是通过内存分配函数先创建一个 f2fs_inode_info 然后返回给上层:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static struct inode *f2fs_alloc_inode (struct super_block *sb) { struct f2fs_inode_info *fi ; fi = kmem_cache_alloc(f2fs_inode_cachep, GFP_F2FS_ZERO); if (!fi) return NULL ; init_once((void *) fi); atomic_set (&fi->dirty_pages, 0 ); init_rwsem(&fi->i_sem); ... return &fi->vfs_inode; }
当 vfs inode 的 link 是 0 的时候,它应当被销毁。由于 vfs inode 是 f2fs_inode_info 的内部变量:
1 2 3 4 5 6 static void f2fs_destroy_inode (struct inode *inode) { call_rcu(&inode->i_rcu, f2fs_i_callback); }
同样简单直接,free 掉这块内存就行。
1 2 3 4 5 static void f2fs_i_callback (struct rcu_head *head) { struct inode *inode = container_of(head, struct inode, i_rcu); kmem_cache_free(f2fs_inode_cachep, F2FS_I(inode)); }
f2fs_create 函数这个函数的主要作用是创建 vfs_inode,并链接到对应的目录下,核心流程就是先创建该文件的基于 f2fs 的 inode 结构,以及对应的 f2fs 的 inode page,即 f2fs_inode。然后设置函数指针,最后将 f2fs 的 inode page 链接到对应的目录下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 static int f2fs_create (struct inode *dir, struct dentry *dentry, umode_t mode, bool excl) { struct f2fs_sb_info *sbi = F2FS_I_SB(dir); struct inode *inode ; nid_t ino = 0 ; int err; inode = f2fs_new_inode(dir, mode); inode->i_op = &f2fs_file_inode_operations; inode->i_fop = &f2fs_file_operations; inode->i_mapping->a_ops = &f2fs_dblock_aops; ino = inode->i_ino; err = f2fs_add_link(dentry, inode); if (err) goto out; f2fs_alloc_nid_done(sbi, ino); return 0 ; }
f2fs_new_inode 函数下面继续分析 f2fs_new_inode 函数(只显示主干部分),这个函数创建 inode 结构,还没 创建对应的 f2fs inode page。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 static struct inode *f2fs_new_inode (struct inode *dir, umode_t mode) { struct f2fs_sb_info *sbi = F2FS_I_SB(dir); nid_t ino; struct inode *inode ; bool nid_free = false ; int xattr_size = 0 ; int err; inode = new_inode(dir->i_sb); if (!f2fs_alloc_nid(sbi, &ino)) { goto fail; } nid_free = true ; inode_init_owner(inode, dir, mode); inode->i_ino = ino; inode->i_blocks = 0 ; inode->i_mtime = inode->i_atime = inode->i_ctime = current_time(inode); F2FS_I(inode)->i_crtime = inode->i_mtime; inode->i_generation = sbi->s_next_generation++; err = insert_inode_locked(inode); set_inode_flag(inode, FI_NEW_INODE); ...... f2fs_set_inode_flags(inode); return inode; }
f2fs_add_link 函数经过上面的函数,我们已经创建了一个 f2fs 使用的 vfs inode,接下来我们要将这个 inode 链接到父目录的 inode 当中,建立联系,f2fs_add_link 函数直接会调用 f2fs_do_add_link 函数,因此我们直接分析这个函数。其中 f2fs_dir_entry 代表是目录项,可以理解为父目录包含了多个子文件/目录项,每一个目录项对应一个子文件/子目录的关联信息。我们将新创建的 inode 加入到父目录的管理,也就是在父目录中为这个新 inode 下创建一个目录项。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 static inline int f2fs_add_link (struct dentry *dentry, struct inode *inode) { return f2fs_do_add_link(d_inode(dentry->d_parent), &dentry->d_name, inode, inode->i_ino, inode->i_mode); } int f2fs_do_add_link (struct inode *dir, const struct qstr *name, struct inode *inode, nid_t ino, umode_t mode) { struct f2fs_dir_entry *de = NULL ; int err; err = fscrypt_setup_filename(dir, name, 0 , &fname); if (de) { f2fs_put_page(page, 0 ); err = -EEXIST; } else if (IS_ERR(page)) { err = PTR_ERR(page); } else { err = f2fs_add_dentry(dir, &fname, inode, ino, mode); } return err; }
f2fs_add_dentry 函数提取了文件名字的字符串以及字符串长度:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int f2fs_add_dentry (struct inode *dir, struct fscrypt_name *fname, struct inode *inode, nid_t ino, umode_t mode) { struct qstr new_name ; int err = -EAGAIN; new_name.name = fname_name(fname); new_name.len = fname_len(fname); err = f2fs_add_regular_entry(dir, &new_name, fname->usr_fname, inode, ino, mode); f2fs_update_time(F2FS_I_SB(dir), REQ_TIME); return err; }
新 inode 的 f2fs_dir_entry 应该是不存在的,注意 FI_NEW_INODE 的 flag。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 int f2fs_add_regular_entry (struct inode *dir, const struct qstr *new_name, const struct qstr *orig_name, struct inode *inode, nid_t ino, umode_t mode) { ... if (inode) { page = f2fs_init_inode_metadata(inode, dir, new_name, orig_name, NULL ); } f2fs_update_dentry(ino, mode, &d, new_name, dentry_hash, bit_pos); set_page_dirty(dentry_page); f2fs_update_parent_metadata(dir, inode, current_depth); return err; }
由于新 inode 设置了 FI_NEW_INODE,因此 f2fs_init_inode_metadata 函数就是完成了两个功能:
创建一个新的 inode page,然后初始化 acl、security 等信息。 然后初始化新创建的 inode page 的名字。 再增加 inode 的引入链接。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 struct page *f2fs_init_inode_metadata (struct inode *inode, struct inode *dir, const struct qstr *new_name, const struct qstr *orig_name, struct page *dpage) { struct page *page ; int err; if (is_inode_flag_set(inode, FI_NEW_INODE)) { page = f2fs_new_inode_page(inode); err = f2fs_init_acl(inode, dir, page, dpage); if (err) goto put_error; err = f2fs_init_security(inode, dir, orig_name, page); if (err) goto put_error; } } else { page = f2fs_get_node_page(F2FS_I_SB(dir), inode->i_ino); if (IS_ERR(page)) return page; } if (new_name) { init_dent_inode(new_name, page); if (f2fs_encrypted_inode(dir)) file_set_enc_name(inode); } if (is_inode_flag_set(inode, FI_INC_LINK)) f2fs_i_links_write(inode, true ); return page; }
将新的 inode 链接到父目录后,后续用户访问时,可以通过父目录找到新创建的文件的 inode,即完成了整个文件的创建流程。
重要数据结构和函数分析 f2fs_summary 和 f2fs_summary_block 介绍因为每一个 segment 需要管理 512 个 Block 的地址,而且很多场合需要通过 block 地址找到这个 block 是属于哪一个 node,以及属于这个 node 的第几个 block。f2fs_summary 主要保存了 block->node 的映射信息:
1 2 3 4 5 6 7 8 9 10 struct f2fs_summary { __le32 nid; union { __u8 reserved[3 ]; struct { __u8 version; __le16 ofs_in_node; } __packed; }; } __packed;
一个 segment 对应的 512 个 f2fs_summary 是通过一个 4 KB 的 block 保存,f2fs_summary_block 保存在元数据区域的 SSA 区域:
1 2 3 4 5 6 7 8 9 10 struct f2fs_summary_block { struct f2fs_summary entries [ENTRIES_IN_SUM ]; struct f2fs_journal journal ; struct summary_footer footer ; } __packed; struct summary_footer { unsigned char entry_type; __le32 check_sum; } __packed;
其中 summary_footer 记录了这个 f2fs_summary_block 的一些属性,如校验信息,以及这个 f2fs_summary_block 对应的 segment 所管理的 block 是属于 node 还是 data。
应用场景GC 基本流程: 选一个无效 block 最多的当选择出需要 gc 的 victim segment,然后将这个 victim segment 的 block 迁移插入到其他 segment 中,这样就可以制造出一个全部 block 都可以用的 segment。f2fs_summary 在 GC 的作用: 当选择出需要 gc 的 victim segment 之后,可以通过这个 victim segment 的 segno,在 SSA 区域找到 f2fs_summary_block。对 victim segment 的每一个 block 进行迁移的时候,会根据 block 的地址在 f2fs_summary_block 找到 它所对应的 f2fs_summary 然后根据它所记录的 f2fs_summary->nid 以及 f2fs_summary->ofs_in_node 找到对应的具体的 block 的数据,然后将这些数据设置为 dirty,然后等待 vfs 的 writeback 机制完成页迁移。 seg_entry 和 sit_info seg_entry 结构 介绍因为每一个 segment 需要管理 512 个 Block 的地址,因此需要通过某种方式去标记一个 segment 下的 block,哪些是已经使用的,哪些 block 是处于无效状态等待回收。在 F2FS 中,通过结构体 seg_entry 去管理一个 segment 下的所有 block 的使用信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 struct seg_entry { unsigned int type:6 ; unsigned int valid_blocks:10 ; unsigned int ckpt_valid_blocks:10 ; unsigned int padding:6 ; unsigned char *cur_valid_map; #ifdef CONFIG_F2FS_CHECK_FS unsigned char *cur_valid_map_mir; #endif unsigned char *ckpt_valid_map; unsigned char *discard_map; unsigned long long mtime; };
seg_entry 由于跟磁盘空间大小有关,因此初始化时以动态分配的方式,保存在元数据区域的 SIT 区域当中,代码的具体实现为 sbi->sit_info->sentries 中。
应用场景写流程: 当文件的修改某一个 block 的数据时,需要经过的流程是:
分配一个新的 block; 将数据写入到新分配的 block 中; 将旧 block 置为无效,等待回收; 将新 block 写入到磁盘中。 这一个流程需要更新的 segment 的管理信息,因为新 block 和旧 block 可能来自不同的 segment,因此需要更新 segment 的统计信息,具体流程是: 根据新 block 的地址,找到对应 segment number 和 seg_entry,然后在 seg_entry 的根据新 block 在 segment 的 bitmap 对应位置设为 1,然后给 seg_entry->valid_blocks 加一,表示这个 segment 新增加了一个被使用 block;对于旧 block,一样是根据 block 地址找到 segment number 和 seg_entry,然后执行相反操作对 bitmap 设为 0,然后 seg_entry->valid_blocks 减一。
curseg_info 结构 介绍curseg_info 在 F2FS 中表示的是当前使用的 segment 的信息。一般情况下,F2FS 同时运行着 6 个 curseg_info ,分别表示 (NODE,DATA) X (HOT,WARM,COLD) 这些不同类型的 segment。它的基本结构和关联数据结构是:
1 2 3 4 5 6 7 8 9 10 11 struct curseg_info { struct mutex curseg_mutex ; struct f2fs_summary_block *sum_blk ; struct rw_semaphore journal_rwsem ; struct f2fs_journal *journal ; unsigned char alloc_type; unsigned int segno; unsigned short next_blkoff; unsigned int zone; unsigned int next_segno; };
f2fs_summary_block: curseg_info 表示一个 segment,因此通过 f2fs_summary_block 管理这个 segment 下的所有 block。 f2fs_summary_block 包含 512 个 f2fs_summary,每个 summary 代表一个这个 segment 里面的一个 block,它的结构是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 struct f2fs_summary_block { struct f2fs_summary entries [ENTRIES_IN_SUM ]; struct f2fs_journal journal ; struct summary_footer footer ; } __packed; struct f2fs_summary { __le32 nid; union { __u8 reserved[3 ]; struct { __u8 version; __le16 ofs_in_node; } __packed; }; } __packed;
可以看到每一个 f2fs_summary 用来描述这个 segment 里面的 block 是属于哪一个 node,而且是这个 node 里面的第几个 block。
f2fs_journal: curseg_info 管理着 512 个 block,需要一种机制去记录每一个它所管理的 block 是否已经被分配出去。因此 f2fs_journal 的作用就是记录每一个 block 是否是有效。它的结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 struct f2fs_journal { union { __le16 n_nats; __le16 n_sits; }; union { struct nat_journal nat_j ; struct sit_journal sit_j ; struct f2fs_extra_info info ; }; } __packed; struct sit_journal { struct sit_journal_entry entries [SIT_JOURNAL_ENTRIES ]; __u8 reserved[SIT_JOURNAL_RESERVED]; } __packed; struct sit_journal_entry { __le32 segno; struct f2fs_sit_entry se ; } __packed; struct f2fs_sit_entry { __le16 vblocks; __u8 valid_map[SIT_VBLOCK_MAP_SIZE]; __le64 mtime; } __packed;
f2fs_journal 可以记录 NAT 和 SIT 的 journal。通过 f2fs_sit_entry 可以发现,f2fs_journal 保存的是有效 block 的数目 vblocks 以及它的 bitmap valid_map。
curseg_info 的作用curseg_info 的作用主要是当一个 Node 或者 Data 需要分配一个新的 block 的时候,就会根据这个 block 的类型,在 curseg_info 取出一个 segment,然后在这个 segment 分配出一个新的 block,然后将新的 block 的映射信息,写入 curseg_info 的 f2fs_summary_block 和 f2fs_journal 中。这样设计的原因是,将大部分更新元数据的操作都放在 curseg_info 完成,避免了频繁读写磁盘。
F2FS Journal 机制 介绍当 F2FS 进行文件读写的时候,根据 f2fs_node 的设计以及闪存设备异地更新的特性,每修改一个数据块,都需要改动 f2fs_node 的地址映射,以及 NAT,SIT 等信息。但是如果仅仅因为一个小改动,例如修改一个块,就需要改动这么多数据,然后再写入磁盘,这样既会导致性能下降,也会导致 SSD 寿命的下降。故 F2FS 设计了 journal 机制,用于将这些对数据的修改会暂存在 f2fs_journal,等系统进行 checkpoint 的时候,再写入磁盘当中。
部分内容参考: https://blog.csdn.net/sunwukong54/article/details/45669017
涉及到的数据结构1 2 3 4 5 6 7 8 9 10 11 12 struct f2fs_journal { union { __le16 n_nats; __le16 n_sits; }; union { struct nat_journal nat_j ; struct sit_journal sit_j ; struct f2fs_extra_info info ; }; } __packed;
f2fs_journal 可以保存 NAT 的 journal 也可以保存 SIT 的 journal,以下分别分析:
NAT Journal NAT 类型的 journal 主要保存的每一个 node 是属于哪一个 inode,以及它的地址是什么,这样设计的原始访问某一个 node 的时候,只要根据 nid 找到对应的 nat_journal_entry,然后就可以找到 f2fs_nat_entry,最后找到 blkaddr。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct nat_journal { struct nat_journal_entry entries [NAT_JOURNAL_ENTRIES ]; __u8 reserved[NAT_JOURNAL_RESERVED]; } __packed; struct nat_journal_entry { __le32 nid; struct f2fs_nat_entry ne ; } __packed; struct f2fs_nat_entry { __u8 version; __le32 ino; __le32 block_addr; } __packed;
SIT Journal SIT 类型的 Journal 和 segment 一一对应。segment 管理着 512 个 block,需要一种机制去记录每一个它所管理的 block 是否已经被分配出去。通过 f2fs_sit_entry 可以发现,f2fs_journal 保存的是有效 block 的数目 vblocks 以及它的 bitmap valid_map。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct sit_journal { struct sit_journal_entry entries [SIT_JOURNAL_ENTRIES ]; __u8 reserved[SIT_JOURNAL_RESERVED]; } __packed; struct sit_journal_entry { __le32 segno; struct f2fs_sit_entry se ; } __packed; struct f2fs_sit_entry { __le16 vblocks; __u8 valid_map[SIT_VBLOCK_MAP_SIZE]; __le64 mtime; } __packed;
一些机制的具体实现 通过 Journal 获取 Node 的地址1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 void f2fs_get_node_info (struct f2fs_sb_info *sbi, nid_t nid, struct node_info *ni) { struct f2fs_nm_info *nm_i = NM_I(sbi); struct curseg_info *curseg = CURSEG_I(sbi, CURSEG_HOT_DATA); struct f2fs_journal *journal = curseg->journal; nid_t start_nid = START_NID(nid); struct f2fs_nat_block *nat_blk ; struct page *page = NULL ; struct f2fs_nat_entry ne ; struct nat_entry *e ; pgoff_t index; int i; ni->nid = nid; down_read(&nm_i->nat_tree_lock); e = __lookup_nat_cache(nm_i, nid); if (e) { ni->ino = nat_get_ino(e); ni->blk_addr = nat_get_blkaddr(e); ni->version = nat_get_version(e); up_read(&nm_i->nat_tree_lock); return ; } memset (&ne, 0 , sizeof (struct f2fs_nat_entry)); down_read(&curseg->journal_rwsem); i = f2fs_lookup_journal_in_cursum(journal, NAT_JOURNAL, nid, 0 ); if (i >= 0 ) { ne = nat_in_journal(journal, i); node_info_from_raw_nat(ni, &ne); } up_read(&curseg->journal_rwsem); if (i >= 0 ) { up_read(&nm_i->nat_tree_lock); goto cache; } index = current_nat_addr(sbi, nid); up_read(&nm_i->nat_tree_lock); page = f2fs_get_meta_page(sbi, index); nat_blk = (struct f2fs_nat_block *)page_address(page); ne = nat_blk->entries[nid - start_nid]; node_info_from_raw_nat(ni, &ne); f2fs_put_page(page, 1 ); cache: cache_nat_entry(sbi, nid, &ne); }
通过 Checkpoint 将 journal 的信息写入到磁盘中简略的流程如下:
f2fs_flush_nat_entries 和 f2fs_flush_sit_entries 函数将 entry 都写入到 curseg_info->f2fs_summary->journal 的变量中。do_checkpoint() 函数读取 curseg_info->f2fs_summary,然后通过函数 f2fs_write_node_summaries 或 f2fs_write_data_summaries 刷写到磁盘中。 f2fs_map_blocks 的作用与源码分析函数 f2fs_map_blocks() 启到了地址映射的作用,主要作用是通过逻辑地址找到可以访问磁盘的物理地址。
读写流程的作用对读的作用: 通过该函数根据逻辑地址找到物理地址,然后从磁盘读取出数据。 对写的作用: 文件在写入数据之前,会执行一个 preallocate 的过程,这个过程会调用 f2fs_map_blocks 函数对即将要写入数据的逻辑块进行预处理,如果是 append 的方式写入数据,则将物理地址初始化为 NEW_ADDR; 如果是 rewrite 的方式写入数据,则不作改变。 核心数据结构f2fs_map_blocks() 函数的核心是 f2fs_map_blocks 数据结构,保存了一系列映射信息。
1 2 3 4 5 6 7 struct f2fs_map_blocks { block_t m_pblk; block_t m_lblk; unsigned int m_len; unsigned int m_flags; pgoff_t *m_next_pgofs; };
读流程的核心逻辑一般的读流程,会进行如下的数据结构初始化:
1 2 3 map .m_lblk = block_in_file; map .m_len = len; f2fs_map_blocks(inode, &map , 0 , F2FS_GET_BLOCK_READ);
即通过逻辑地址和读取长度找到对应的物理地址,与读流程相关的核心逻辑 如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 int f2fs_map_blocks (struct inode *inode, struct f2fs_map_blocks *map , int create, int flag) { unsigned int maxblocks = map ->m_len; int mode = create ? ALLOC_NODE : LOOKUP_NODE_RA; map ->m_len = 0 ; map ->m_flags = 0 ; pgofs = (pgoff_t )map ->m_lblk; if (!create && f2fs_lookup_extent_cache(inode, pgofs, &ei)) { map ->m_pblk = ei.blk + pgofs - ei.fofs; map ->m_len = min((pgoff_t )maxblocks, ei.fofs + ei.len - pgofs); map ->m_flags = F2FS_MAP_MAPPED; goto out; } set_new_dnode(&dn, inode, NULL , NULL , 0 ); err = get_dnode_of_data(&dn, pgofs, mode); blkaddr = datablock_addr(dn.node_page, dn.ofs_in_node); ... map ->m_pblk = blkaddr; ... return err; }
写流程的核心逻辑一般的读流程,会进行如下的数据结构初始化:
1 2 3 map .m_lblk = F2FS_BLK_ALIGN(iocb->ki_pos); map .m_len = F2FS_BYTES_TO_BLK(iocb->ki_pos + iov_iter_count(from)); f2fs_map_blocks(inode, &map , 1 , F2FS_GET_BLOCK_PRE_AIO);
写流程下的 f2fs_map_blocks() 函数作用是先根据逻辑地址读取物理地址出来,如果这个物理地址没有被分配过(NULL_ADDR),则初始化为新地址(NEW_ADDR),用于下一步的写入磁盘的操作,与写流程相关的核心逻辑 如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 int f2fs_map_blocks (struct inode *inode, struct f2fs_map_blocks *map , int create, int flag) { unsigned int maxblocks = map ->m_len; int mode = create ? ALLOC_NODE : LOOKUP_NODE; map ->m_len = 0 ; map ->m_flags = 0 ; pgofs = (pgoff_t )map ->m_lblk; end = pgofs + maxblocks; set_new_dnode(&dn, inode, NULL , NULL , 0 ); err = f2fs_get_dnode_of_data(&dn, pgofs, mode); blkaddr = datablock_addr(dn.inode, dn.node_page, dn.ofs_in_node); if (flag == F2FS_GET_BLOCK_PRE_AIO) { if (blkaddr == NULL_ADDR) { prealloc++; last_ofs_in_node = dn.ofs_in_node; } } if (flag == F2FS_GET_BLOCK_PRE_AIO && (pgofs == end || dn.ofs_in_node == end_offset)) { dn.ofs_in_node = ofs_in_node; err = f2fs_reserve_new_blocks(&dn, prealloc); if (err) goto sync_out; map ->m_len += dn.ofs_in_node - ofs_in_node; if (prealloc && dn.ofs_in_node != last_ofs_in_node + 1 ) { err = -ENOSPC; goto sync_out; } dn.ofs_in_node = end_offset; } ... return err; }
物理地址寻址的实现VFS 的读写都依赖于物理地址的寻址。经典的读流程,VFS 会传入 inode 以及 page index 信息给文件系统,然后文件系统需要根据以上信息,找到物理地址,然后访问磁盘将其读取出来。F2FS 的物理地址寻址,是通过 f2fs_get_dnode_of_data() 函数实现。
在执行这个 f2fs_get_dnode_of_data() 函数之前,需要通过 set_new_dnode() 函数进行对数据结构 struct dnode_of_data 进行初始化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 struct dnode_of_data { struct inode *inode ; struct page *inode_page ; struct page *node_page ; nid_t nid; unsigned int ofs_in_node; bool inode_page_locked; bool node_changed; char cur_level; char max_level; block_t data_blkaddr; }; static inline void set_new_dnode (struct dnode_of_data *dn, struct inode *inode, struct page *ipage, struct page *npage, nid_t nid) { memset (dn, 0 , sizeof (*dn)); dn->inode = inode; dn->inode_page = ipage; dn->node_page = npage; dn->nid = nid; }
大部分情况下,仅需要传入 inode 进行初始化:
1 set_new_dnode(&dn, inode, NULL , NULL , 0 );
然后根据需要访问的 page index,执行 f2fs_get_dnode_of_data() 函数寻找:
1 2 err = f2fs_get_dnode_of_data(&dn, page->index, type); blockt blkaddr = dn.data_blkaddr;
接下来分析,函数是如何寻址,由于函数比较长和复杂,先分析一个比较重要的函数 get_node_path() 函数的作用,它的用法是:
概念在分析之前,我们要明确几个概念。f2fs 有三种 node 的类型,f2fs_inode、direct_node 和 indirect node。其中 f2fs_inode 和 direct_node 都是直接保存数据的地址指针,因此一般统称为 direct node,若有下横线,例如 direct_node,则表示数据结构 struct direct_node,如果没有下横线,则表示直接保存数据的地址指针的 node,即 f2fs_inode 和 direct_node。另外 indirect node 保存的是间接寻址的 node 的 nid,因此一般直接称为 indirect node。
函数用法1 2 3 4 int level;int offset[4 ];unsigned int noffset[4 ];level = get_node_path(inode, page->index, offset, noffset);
这里 offset 和 noffset 分别表示 block offset 和 node offset,返回的 level 表示寻址的深度,一共有 4 个深度,使用 0~3 表示:
level=0: 表示可以直接在 f2fs_inode 找到物理地址。 level=1: 表示可以在 f2fs_inode->i_nid[0~1] 对应的 direct_node能够找到物理地址。 level=2: 表示可以在 f2fs_inode->i_nid[2~3]对应的 indirect_node 下的 nid 对应的 direct_node能够找到物理地址。 level=3: 表示只能在 f2fs_inode->i_nid[4] 对应 indirect_node的 nid 对应的 indirect_node 的 nid 对应的 direct_node 才能找到地址。 由于 offset 和 noffset,表示的是物理地址寻址信息,分别表示 block 偏移和 direct node 偏移来表示,它们是长度为 4 的数组,代表不同 level 0~3 的寻址信息。之后的函数可以通过 offset 和 noffset 将数据块计算出来。
寻址原理给定 page->index,计算出 level 之后,offset[level] 表示该 page 在所对应的 direct node 里面的 block 的偏移,noffset[level] 表示当前的 node 是属于这个文件的第几个 node(包括 f2fs_node, direct_node, indirect_node),下面用几个例子展示一下(注意下面计算的是不使用 xattr 的 f2fs 版本,如果使用了 xattr 结果会不同,但是表示的含义是一样的):
例子 1: 物理地址位于 f2fs_inode例如我们要寻找 page->index = 665 的数据块所在的位置,显然 655 是位于 f2fs_inode 内,因此 level=0,因此我们只需要看 offset[0] 以及 noffset[0] 的信息。offset[0] = 665 表示这个数据块在当前 direct node(注意: f2fs_inode 也是 direct node 的一种)的位置;noffset[0] 表示当前 direct node 是属于这个文件的第几个 node,由于 f2fs_inode 是第一个 node,所以 noffset[0] = 0。
1 2 3 level = 0 offset[0 ] = 665 noffset[0 ] = 0
例子 2: 物理地址位于 direct_node例如我们要寻找 page->index = 2113 的数据块所在的位置,它位于第二个 direct_node,所以 level=1。我们只需要看 offset[1] 以及 noffset[1] 的信息。offset[1] = 172 表示这个数据块在当前 direct node 的位置,即 direct_node->addr[172];noffset[1] 表示当前 direct node 是属于这个文件的第几个 node,由于它位于第二个 direct_node,前面还有一个 f2fs_inode 以及一个 direct node,所以这是第三个 node,因此 noffset[1] = 2。
1 2 3 level = 1 offset[1 ] = 172 noffset[1 ] = 2
例子 3: 物理地址位于 indirect_node例如我们要寻找 page->index = 4000 的数据块所在的位置,它位于第 1 个 indirect_node 的第 2 个 direct_node中,所以 level=2。我们只需要看 offset[2] 以及 noffset[2] 的信息。offset[2] = 23 表示这个数据块在当前 direct node 的位置;noffset[2] 表示当前 direct node 是属于这个文件的第几个 direct node,即这是第 6 个 node。(1 * f2fs_inode + 2 * direct_node + 1 * indirect_node + 2 * direct node)。
1 2 offset[2 ] = 23 noffset[2 ] = 5
例子 4: 物理地址位于 indirect_node 再 indiret_node 中 (double indirect node)例如我们要寻找 page->index = 2075624 的数据块所在的位置,它位于第一个 double indirect_node 的第一个 indirect_node 的第一个 direct_node 中,所以 level=3。同理我们只需要看 offset[3] 以及 noffset[3] 的信息,如下,可以自己计算一下:
1 2 offset[3 ] = 17 noffset[3 ] = 2043
从上面可以知道 get_node_path() 函数以后,执行可以根据 offset 和 noffset 直接知道 page->index 对应的物理地址,位于第几个 node page 的第几个 offset 对应的物理地址中。下面分析 f2fs_get_dnode_of_data() 的原理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 int f2fs_get_dnode_of_data (struct dnode_of_data *dn, pgoff_t index, int mode) { struct f2fs_sb_info *sbi = F2FS_I_SB(dn->inode); struct page *npage [4]; struct page *parent = NULL ; int offset[4 ]; unsigned int noffset[4 ]; nid_t nids[4 ]; int level, i = 0 ; int err = 0 ; level = get_node_path(dn->inode, index, offset, noffset); if (level < 0 ) return level; nids[0 ] = dn->inode->i_ino; npage[0 ] = dn->inode_page; if (!npage[0 ]) { npage[0 ] = f2fs_get_node_page(sbi, nids[0 ]); } parent = npage[0 ]; if (level != 0 ) nids[1 ] = get_nid(parent, offset[0 ], true ); dn->inode_page = npage[0 ]; dn->inode_page_locked = true ; for (i = 1 ; i <= level; i++) { bool done = false ; if (!nids[i] && mode == ALLOC_NODE) { if (!f2fs_alloc_nid(sbi, &(nids[i]))) { err = -ENOSPC; goto release_pages; } dn->nid = nids[i]; npage[i] = f2fs_new_node_page(dn, noffset[i]); set_nid(parent, offset[i - 1 ], nids[i], i == 1 ); f2fs_alloc_nid_done(sbi, nids[i]); done = true ; } else if (mode == LOOKUP_NODE_RA && i == level && level > 1 ) { npage[i] = f2fs_get_node_page_ra(parent, offset[i - 1 ]); done = true ; } if (i == 1 ) { dn->inode_page_locked = false ; unlock_page(parent); } else { f2fs_put_page(parent, 1 ); } if (!done) { npage[i] = f2fs_get_node_page(sbi, nids[i]); } if (i < level) { parent = npage[i]; nids[i + 1 ] = get_nid(parent, offset[i], false ); } } dn->nid = nids[level]; dn->ofs_in_node = offset[level]; dn->node_page = npage[level]; dn->data_blkaddr = datablock_addr(dn->inode, dn->node_page, dn->ofs_in_node); return 0 ; }
footer 是 F2FS 中记录 node 的属性的一个数据,定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct f2fs_node { union { struct f2fs_inode i ; struct direct_node dn ; struct indirect_node in ; }; struct node_footer footer ; } __packed; struct node_footer { __le32 nid; __le32 ino; __le32 flag; __le64 cp_ver; __le32 next_blkaddr; } __packed;
F2FS 有三种类型的 node,分别是 f2fs_inode、direct_node、indirect_node,每一种类型的 node 都有对应的 footer。
每一个 node 都有一个独特的 nid,它被记录在 footer 中,如果是 direct_node 或者 indirect_node,它们都有一个对应的 f2fs_inode,因此为了记录从属关系,还需要 footer 记录它所属于的 f2fs_inode 的 nid,即 ino。因此,如果 footer->nid == footer->ino,那么这个 node 就是 inode,反正这个 node 是 direct_node 或者 indirect_node。
footer->flag 的作用是标记当前的 node 的属性。目前 F2FS 给 node 定义了三种属性:
1 2 3 4 5 6 7 8 enum { COLD_BIT_SHIFT = 0 , FSYNC_BIT_SHIFT, DENT_BIT_SHIFT, OFFSET_BIT_SHIFT }; #define OFFSET_BIT_MASK (0x07)
其中 footer->flag:
第 0 位表示这个 node 是否是 cold node。 第 1 位表示这个 node 是否执行了完整的 fsync。F2FS 为了 fsync 的效率做了一些改进,F2FS 不会在 fsync 刷写所有脏的 node page 进去磁盘,只会刷写一些根据 data 直接相关的 node page 进入磁盘,例如 f2fs_inode 和 direct_node。因此这个标志位是用来记录这个 node 是否执行了完整的 fsync,以便系统在 crash 中恢复。 第 3 位表示这个 node 是是用来保存文件数据,还是目录数据的,也是用于数据恢复。 footer->cp_ver 分别用来记录当前的 checkpoint 的 version,恢复的时候比较 version 版本确定如何进行数据恢复。
footer->next_blkaddr 则是用来记录这个 node 对应下一个 node page 的地址,也是用来恢复数据。
rename 流程 流程介绍sys_rename 函数。 do_renameat2 函数。 vfs_rename 函数。 f2fs_rename 函数。 sys_rename 函数sys_rename 函数是一个系统调用,是 rename 函数进入内核层的第一个函数:
1 2 3 4 5 SYSCALL_DEFINE2(rename, const char __user *, oldname, const char __user *, newname) { return do_renameat2(AT_FDCWD, oldname, AT_FDCWD, newname, 0 ); }
do_renameat2 函数do_renameat2 函数比较长,考虑多个输入 flag 的作用,这里只考虑 sys_rename 函数 rename 一个文件的情形,即 flag=0,并以此精简函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 static int do_renameat2 (int olddfd, const char __user *oldname, int newdfd, const char __user *newname, unsigned int flags) { struct dentry *old_dentry , *new_dentry ; struct dentry *trap ; struct path old_path , new_path ; struct qstr old_last , new_last ; int old_type, new_type; struct inode *delegated_inode = NULL ; struct filename *from ; struct filename *to ; unsigned int lookup_flags = 0 , target_flags = LOOKUP_RENAME_TARGET; bool should_retry = false ; int error; retry: from = filename_parentat(olddfd, getname(oldname), lookup_flags, &old_path, &old_last, &old_type); to = filename_parentat(newdfd, getname(newname), lookup_flags, &new_path, &new_last, &new_type); retry_deleg: trap = lock_rename(new_path.dentry, old_path.dentry); old_dentry = __lookup_hash(&old_last, old_path.dentry, lookup_flags); new_dentry = __lookup_hash(&new_last, new_path.dentry, lookup_flags | target_flags); error = vfs_rename(old_path.dentry->d_inode, old_dentry, new_path.dentry->d_inode, new_dentry, &delegated_inode, flags); dput(new_dentry); dput(old_dentry); unlock_rename(new_path.dentry, old_path.dentry); path_put(&new_path); putname(to); path_put(&old_path); putname(from); exit : return error; }
vfs_rename 函数vfs_rename 函数也会做简化,简化的情形是将文件 A 重命名到文件 B(B 可能已经存在,或者不存在),flags=0。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 int vfs_rename (struct inode *old_dir, struct dentry *old_dentry, struct inode *new_dir, struct dentry *new_dentry, struct inode **delegated_inode, unsigned int flags) { int error; bool is_dir = d_is_dir(old_dentry); struct inode *source = old_dentry->d_inode; struct inode *target = new_dentry->d_inode; bool new_is_dir = false ; unsigned max_links = new_dir->i_sb->s_max_links; struct name_snapshot old_name ; dget(new_dentry); if (target) inode_lock(target); error = old_dir->i_op->rename(old_dir, old_dentry, new_dir, new_dentry, flags); out: if (target) inode_unlock(target); dput(new_dentry); return error; }
f2fs_rename函数f2fs_rename 函数也会做简化,简化的情形是将文件A 重命名到文件 B(B 可能已经存在,或者不存在),flags=0。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 static int f2fs_rename (struct inode *old_dir, struct dentry *old_dentry, struct inode *new_dir, struct dentry *new_dentry, unsigned int flags) { struct f2fs_sb_info *sbi = F2FS_I_SB(old_dir); struct inode *old_inode = d_inode(old_dentry); struct inode *new_inode = d_inode(new_dentry); struct inode *whiteout = NULL ; struct page *old_dir_page ; struct page *old_page , *new_page = NULL ; struct f2fs_dir_entry *old_dir_entry = NULL ; struct f2fs_dir_entry *old_entry ; struct f2fs_dir_entry *new_entry ; bool is_old_inline = f2fs_has_inline_dentry(old_dir); int err; old_entry = f2fs_find_entry(old_dir, &old_dentry->d_name, &old_page); if (new_inode) { new_entry = f2fs_find_entry(new_dir, &new_dentry->d_name, &new_page); f2fs_lock_op(sbi); err = f2fs_acquire_orphan_inode(sbi); f2fs_set_link(new_dir, new_entry, new_page, old_inode); new_inode->i_ctime = current_time(new_inode); down_write(&F2FS_I(new_inode)->i_sem); f2fs_i_links_write(new_inode, false ); up_write(&F2FS_I(new_inode)->i_sem); if (!new_inode->i_nlink) f2fs_add_orphan_inode(new_inode); else f2fs_release_orphan_inode(sbi); } else { f2fs_lock_op(sbi); err = f2fs_add_link(new_dentry, old_inode); } down_write(&F2FS_I(old_inode)->i_sem); if (!old_dir_entry || whiteout) file_lost_pino(old_inode); else F2FS_I(old_inode)->i_pino = new_dir->i_ino; up_write(&F2FS_I(old_inode)->i_sem); old_inode->i_ctime = current_time(old_inode); f2fs_mark_inode_dirty_sync(old_inode, false ); f2fs_delete_entry(old_entry, old_page, old_dir, NULL ); f2fs_unlock_op(sbi); f2fs_update_time(sbi, REQ_TIME); return 0 ; }
参考文档RiweiPan/F2FS-NOTES: F2FS的学习笔记以及源码分析